├── .gitignore ├── LICENSE ├── README.md ├── _example ├── color_ring │ └── main.go └── motion │ └── main.go ├── brush.go ├── characteristics.md ├── color.go ├── discovery.go ├── go.mod ├── go.sum ├── mode.go ├── mode_string.go ├── quadrant.go ├── state.go ├── state_string.go ├── status_configure.go └── status_control.go /.gitignore: -------------------------------------------------------------------------------- 1 | services.txt 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Raqbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goral-B 2 | 3 | A WIP Go library for connecting to and getting data from (Braun) Oral-B Bluetooth enabled electric toothbrushes. 4 | 5 | ## Todo 6 | 7 | * [x] Parsing BLE advertisements 8 | * [x] Connecting to brush 9 | * [x] Reading/Writing new right light colors 10 | * [x] Enabling/Disabling ring light 11 | * [ ] Change mode order / activate hidden modes 12 | 13 | ### Debugging 14 | Using `gatttool` it is possible to manually send commands to the brush. 15 | 16 | Connecting to the brush: `gatttool --device= -I` 17 | 18 | #### Examples 19 | 20 | **Setting the color to green (#00ff00)** 21 | ``` 22 | connect # Connect to the brush 23 | char-write-req 0x0071 00ff0000 # Set color to 00ff00 24 | char-write-req 0x0052 372f # "Configure" color 25 | char-write-req 0x0052 1031 # "Control" color enable 26 | char-write-req 0x0052 1032 # "Control" color enable 27 | ``` 28 | -------------------------------------------------------------------------------- /_example/color_ring/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/Raqbit/goralb" 8 | "image/color" 9 | "os" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | if len(os.Args) != 2 { 15 | panic("please give hexcode as argument") 16 | } 17 | 18 | var newColor color.RGBA 19 | 20 | if _, err := fmt.Sscanf(os.Args[1], "#%2x%2x%2x", &newColor.R, &newColor.G, &newColor.B); err != nil { 21 | panic(errors.New("could not parse input color_ring")) 22 | } 23 | 24 | discoverTimeout, cancel := context.WithTimeout(context.Background(), time.Second*5) 25 | defer cancel() 26 | 27 | bs, err := goralb.NewScanner() 28 | 29 | if err != nil { 30 | panic(fmt.Errorf("could not create brush scanner: %w", err)) 31 | } 32 | 33 | defer bs.Close() 34 | 35 | fmt.Println("Turn on brush. This is also possible bluetooth-only using the mode select button") 36 | brush, err := bs.FindBrush(discoverTimeout) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | fmt.Printf("Found device, %s\n", brush) 42 | 43 | connectTimeout, cancel := context.WithTimeout(context.Background(), time.Second*5) 44 | defer cancel() 45 | 46 | fmt.Println("Connecting...") 47 | 48 | if err = brush.Connect(connectTimeout); err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println("Retrieving current color_ring...") 53 | 54 | prevColor, err := brush.GetColor() 55 | 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | fmt.Printf("Current color_ring: (R: %d, G: %d, B: %d)\n", prevColor.R, prevColor.G, prevColor.B) 61 | fmt.Printf("New color_ring: (R: %d, G: %d, B: %d)\n", newColor.R, newColor.G, newColor.B) 62 | 63 | fmt.Println("Setting new color_ring...") 64 | 65 | if err = brush.SetColor(newColor); err != nil { 66 | panic(err) 67 | } 68 | 69 | fmt.Println("Turning on...") 70 | 71 | if err = brush.SetRingEnabled(true); err != nil { 72 | panic(err) 73 | } 74 | 75 | time.Sleep(time.Second * 2) 76 | 77 | fmt.Println("Turning off...") 78 | 79 | if err = brush.SetRingEnabled(false); err != nil { 80 | panic(err) 81 | } 82 | 83 | fmt.Println("Disconnecting...") 84 | 85 | brush.Disconnect() 86 | 87 | fmt.Println("Done!") 88 | } 89 | -------------------------------------------------------------------------------- /_example/motion/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/Raqbit/goralb" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | discoverTimeout, cancel := context.WithTimeout(context.Background(), time.Second*5) 12 | defer cancel() 13 | 14 | bs, err := goralb.NewScanner() 15 | 16 | if err != nil { 17 | panic(fmt.Errorf("could not create brush scanner: %w", err)) 18 | } 19 | 20 | defer bs.Close() 21 | 22 | fmt.Println("Turn on brush. This is also possible bluetooth-only using the mode select button") 23 | brush, err := bs.FindBrush(discoverTimeout) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | fmt.Printf("Found device, %s\n", brush) 29 | 30 | connectTimeout, cancel := context.WithTimeout(context.Background(), time.Second*5) 31 | defer cancel() 32 | 33 | fmt.Println("Connecting...") 34 | 35 | if err = brush.Connect(connectTimeout); err != nil { 36 | panic(err) 37 | } 38 | 39 | fmt.Println("Enabling motion") 40 | 41 | if err = brush.SetMotionEnabled(true); err != nil { 42 | panic(err) 43 | } 44 | 45 | time.Sleep(time.Second * 3) 46 | 47 | fmt.Println("Disabling motion") 48 | 49 | if err = brush.SetMotionEnabled(false); err != nil { 50 | panic(err) 51 | } 52 | 53 | fmt.Println("Disconnecting...") 54 | 55 | brush.Disconnect() 56 | 57 | fmt.Println("Done!") 58 | } 59 | -------------------------------------------------------------------------------- /brush.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/godbus/dbus/v5" 8 | "github.com/muka/go-bluetooth/bluez/profile/device" 9 | "github.com/muka/go-bluetooth/bluez/profile/gatt" 10 | "image/color" 11 | "time" 12 | ) 13 | 14 | type ( 15 | Brush interface { 16 | Connect(timeout context.Context) error 17 | Disconnect() error 18 | SetRingEnabled(enabled bool) error 19 | SetMotionEnabled(enabled bool) error 20 | GetColor() (color.RGBA, error) 21 | SetColor(rgba color.RGBA) error 22 | Info() *BrushInfo 23 | String() string 24 | } 25 | 26 | BrushInfo struct { 27 | ProtocolVersion int 28 | TypeID int 29 | FirmwareVersion int 30 | State BrushState 31 | PressureDetected bool 32 | HasReducedMotorSpeed bool 33 | HasProfessionalTimer bool 34 | BrushTime time.Duration 35 | Mode BrushMode 36 | Quadrant BrushQuadrant 37 | Smiley int 38 | } 39 | 40 | brush struct { 41 | btDev *device.Device1 42 | info *BrushInfo 43 | 44 | status *gatt.GattCharacteristic1 45 | color *gatt.GattCharacteristic1 46 | } 47 | ) 48 | 49 | const ( 50 | statusChar string = "a0f0ff21-5047-4d53-8208-4f72616c2d42" 51 | rtcChar string = "a0f0ff22-5047-4d53-8208-4f72616c2d42" 52 | timezoneChar string = "a0f0ff23-5047-4d53-8208-4f72616c2d42" 53 | brushingTimerChar string = "a0f0ff24-5047-4d53-8208-4f72616c2d42" 54 | brushingModesChar string = "a0f0ff25-5047-4d53-8208-4f72616c2d42" 55 | quadrantTimesChar string = "a0f0ff26-5047-4d53-8208-4f72616c2d42" 56 | tongueTimeChar string = "a0f0ff27-5047-4d53-8208-4f72616c2d42" 57 | pressureChar string = "a0f0ff28-5047-4d53-8208-4f72616c2d42" 58 | dataChar string = "a0f0ff29-5047-4d53-8208-4f72616c2d42" 59 | flightModeChar string = "a0f0ff2a-5047-4d53-8208-4f72616c2d42" 60 | colorChar string = "a0f0ff2b-5047-4d53-8208-4f72616c2d42" 61 | 62 | statusControlId byte = 0x10 63 | statusConfigureId byte = 0x37 64 | ) 65 | 66 | func (b *brush) Connect(timeout context.Context) error { 67 | var err error 68 | 69 | if err = b.btDev.Connect(); err != nil { 70 | return fmt.Errorf("could not connect to brush: %w", err) 71 | } 72 | 73 | if err = b.waitForServicesResolved(timeout); err != nil { 74 | return fmt.Errorf("could not resolve services: %w", err) 75 | } 76 | 77 | b.status, err = b.btDev.GetCharByUUID(statusChar) 78 | 79 | if err != nil { 80 | return fmt.Errorf("could not get status characteristic: %w", err) 81 | } 82 | 83 | b.color, err = b.btDev.GetCharByUUID(colorChar) 84 | 85 | if err != nil { 86 | return fmt.Errorf("could get color characteristic: %w", err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (b *brush) Disconnect() error { 93 | if err := b.btDev.Disconnect(); err != nil { 94 | return fmt.Errorf("could not disconnect from brush: %w", err) 95 | } 96 | return nil 97 | } 98 | 99 | func (b *brush) SetRingEnabled(enabled bool) error { 100 | option := MyColorDisable 101 | 102 | if enabled { 103 | option = MyColorEnable 104 | } 105 | 106 | return b.control(option) 107 | } 108 | 109 | func (b *brush) SetMotionEnabled(enabled bool) error { 110 | option := MotionDisable 111 | 112 | if enabled { 113 | option = MotionEnable 114 | } 115 | 116 | return b.control(option) 117 | } 118 | 119 | func (b *brush) GetColor() (color.RGBA, error) { 120 | data, err := b.color.ReadValue(nil) 121 | 122 | if err != nil { 123 | return color.RGBA{}, fmt.Errorf("could not read color: %w", err) 124 | } 125 | 126 | return color.RGBA{ 127 | R: data[0], 128 | G: data[1], 129 | B: data[2], 130 | }, nil 131 | } 132 | 133 | func (b *brush) SetColor(rgba color.RGBA) error { 134 | var err error 135 | 136 | err = b.color.WriteValue([]byte{ 137 | rgba.R, 138 | rgba.G, 139 | rgba.B, 140 | 0x00, 141 | }, nil) 142 | 143 | if err != nil { 144 | return fmt.Errorf("could not set brush color: %w", err) 145 | } 146 | 147 | err = b.configure(MyColor) 148 | 149 | if err != nil { 150 | return fmt.Errorf("could not configure brush color: %w", err) 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func (b brush) Info() *BrushInfo { 157 | return b.info 158 | } 159 | 160 | func (b brush) String() string { 161 | info := b.Info() 162 | 163 | return fmt.Sprintf( 164 | `Oral-B ToothbrushType: %d, Firmware version: v%d, Mode: %s, State: %s, Quadrant: %s, Pressure: %v`, 165 | info.TypeID, info.FirmwareVersion, info.Mode, info.State, info.Quadrant, info.PressureDetected) 166 | } 167 | 168 | func (b brush) control(option statusControl) error { 169 | return b.status.WriteValue([]byte{ 170 | statusControlId, 171 | byte(option), 172 | }, nil) 173 | } 174 | 175 | func (b *brush) configure(option statusConfigure) error { 176 | return b.status.WriteValue([]byte{ 177 | statusConfigureId, 178 | byte(option), 179 | }, nil) 180 | } 181 | 182 | func (b *brush) waitForServicesResolved(timeout context.Context) error { 183 | changed, err := b.btDev.WatchProperties() 184 | //defer b.btDev.UnwatchProperties(changed) 185 | 186 | if err != nil { 187 | fmt.Println(err) 188 | } 189 | 190 | for { 191 | select { 192 | case change := <-changed: 193 | if change.Name == "ServicesResolved" && change.Value == true { 194 | return nil 195 | } 196 | case <-timeout.Done(): 197 | return errors.New("services did not resolve within given timeout") 198 | } 199 | } 200 | } 201 | 202 | func NewBrush(dev *device.Device1) Brush { 203 | mfd := dev.Properties.ManufacturerData[PGCompanyID] 204 | mfBytes := mfd.(dbus.Variant).Value().([]byte) 205 | 206 | b := &brush{ 207 | btDev: dev, 208 | info: &BrushInfo{ 209 | ProtocolVersion: int(mfBytes[0]), 210 | TypeID: int(mfBytes[1]), 211 | FirmwareVersion: int(mfBytes[2]), 212 | State: BrushState(mfBytes[3]), 213 | PressureDetected: mfBytes[4]&0x80 != 0, 214 | HasReducedMotorSpeed: mfBytes[4]&0x40 != 0, 215 | HasProfessionalTimer: mfBytes[4]&0x1 == 0, 216 | BrushTime: time.Duration(mfBytes[5]*60+mfBytes[6]) * time.Second, // (mins * secs_in_min + secs) * nanos_in_sec 217 | Mode: BrushMode(mfBytes[7]), 218 | Quadrant: BrushQuadrant(int(mfBytes[8])), 219 | Smiley: int(mfBytes[8] & 0x38 >> 3), 220 | }, 221 | } 222 | 223 | return b 224 | } 225 | -------------------------------------------------------------------------------- /characteristics.md: -------------------------------------------------------------------------------- 1 | ## Status characteristic 2 | 3 | UUID: `a0f0ff21-5047-4d53-8208-4f72616c2d42` 4 | 5 | The status characteristic is used to update settings or control functions of the brush. 6 | There are currently two main types of values I've figured out: control values and configure values. 7 | 8 | ### Properties 9 | 10 | - Notify 11 | - Read 12 | - Write 13 | 14 | ### Format 15 | 16 | #### Control (0x10) 17 | 18 | Control values are used to control a function on the brush, like turning the ring light on/off or turning the brush on. 19 | 20 | |Value|Description| 21 | |---|---| 22 | |`0x10 0x30`|Disable the ring light| 23 | |`0x10 0x31`|Enable the ring light| 24 | 25 | #### Configure (0x37) 26 | 27 | Configure values are used to update settings of the brush, like for instance making it save the value of the color characteristic. 28 | 29 | |Value|Description| 30 | |---|---| 31 | |`0x37 0x2f`|Configure the color of the right light| 32 | 33 | 34 | ## Brushing modes characteristic 35 | 36 | UUID: `a0f0ff25-5047-4d53-8208-4f72616c2d42` 37 | 38 | Seems to have a list of [modes](./mode.go) which might be used to define the order of the modes that the brush 39 | cycles through when you press the mode-switch button. It might also be possible this way to activate modes which 40 | are not normally available on the specific brush model. 41 | 42 | More research needed. 43 | 44 | ### Properties 45 | 46 | - Read 47 | - Write 48 | 49 | ## Color characteristic 50 | 51 | UUID: `a0f0ff2b-5047-4d53-8208-4f72616c2d42` 52 | 53 | The color characteristic can be used to read/write the color of the LED ring light. 54 | 55 | ### Properties 56 | 57 | - Read 58 | - Write 59 | 60 | ### Format 61 | 62 | |byte|Purpose| 63 | |---|---| 64 | |1|Red value| 65 | |2|Green value| 66 | |3|Blue value| 67 | |4|??| 68 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | type Color int 4 | 5 | func (c Color) String() string { 6 | panic("implement me") 7 | } 8 | -------------------------------------------------------------------------------- /discovery.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/muka/go-bluetooth/api" 8 | "github.com/muka/go-bluetooth/bluez/profile/adapter" 9 | "github.com/muka/go-bluetooth/bluez/profile/device" 10 | ) 11 | 12 | const PGCompanyID = 0xDC 13 | 14 | type ( 15 | BrushScanner interface { 16 | FindBrush(ctx context.Context) (Brush, error) 17 | FindBrushes(ctx context.Context, count int) ([]Brush, error) 18 | Close() error 19 | } 20 | 21 | brushScanner struct { 22 | adapter *adapter.Adapter1 23 | } 24 | ) 25 | 26 | func NewScanner() (*brushScanner, error) { 27 | btAdapter, err := adapter.GetDefaultAdapter() 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &brushScanner{ 34 | adapter: btAdapter, 35 | }, nil 36 | } 37 | 38 | // Searches for a single brush 39 | func (bm brushScanner) FindBrush(ctx context.Context) (Brush, error) { 40 | brushes, err := bm.FindBrushes(ctx, 1) 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | if len(brushes) == 0 { 47 | return nil, errors.New("could not find brush") 48 | } 49 | 50 | return brushes[0], nil 51 | } 52 | 53 | // Searches for specified amount of brushes 54 | func (bm brushScanner) FindBrushes(ctx context.Context, count int) ([]Brush, error) { 55 | err := bm.flushBrushDiscoveries() 56 | 57 | if err != nil { 58 | return nil, fmt.Errorf("could not flush brush discoveries: %w", err) 59 | } 60 | 61 | adverts, err := bm.discoverBrushes(ctx, count) 62 | 63 | if err != nil { 64 | return nil, fmt.Errorf("could not discover brushes: %w", err) 65 | } 66 | 67 | return adverts, nil 68 | } 69 | 70 | // Same as Adapter1#FlushDevices() except specifically for PG company ID 71 | func (bm brushScanner) flushBrushDiscoveries() error { 72 | devices, err := bm.adapter.GetDevices() 73 | 74 | if err != nil { 75 | return err 76 | } 77 | 78 | for _, dev := range devices { 79 | // Do not try flush connected devices 80 | if dev.Properties.Connected { 81 | continue 82 | } 83 | 84 | // There should only be one companyId per device, but the data is exposed as a map 85 | for companyId := range dev.Properties.ManufacturerData { 86 | if companyId == PGCompanyID { 87 | // Remove device, ignore when unsuccessful 88 | err = bm.adapter.RemoveDevice(dev.Path()) 89 | 90 | if err != nil { 91 | return fmt.Errorf("could not remove %s from brush cache: %w", dev.Properties.Address, err) 92 | } 93 | 94 | // We only care about this companyID 95 | break 96 | } 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (bm brushScanner) discoverBrushes(ctx context.Context, count int) ([]Brush, error) { 104 | // Only discover LE devices and do not give duplicates 105 | filter := &adapter.DiscoveryFilter{ 106 | Transport: adapter.DiscoveryFilterTransportLE, 107 | DuplicateData: false, 108 | } 109 | 110 | // Discover new devices 111 | discoveries, cancel, err := api.Discover(bm.adapter, filter) 112 | 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | // FIXME: this sometimes hangs? 118 | defer cancel() 119 | 120 | brushes := make([]Brush, 0, count) 121 | 122 | for { 123 | select { 124 | // When a new device has been discovered 125 | case discovery := <-discoveries: 126 | 127 | // Ignore devices which have been removed 128 | if discovery.Type == adapter.DeviceRemoved { 129 | continue 130 | } 131 | 132 | // Create device handle 133 | dev, err := device.NewDevice1(discovery.Path) 134 | 135 | if err != nil || dev == nil { 136 | continue 137 | } 138 | 139 | // Check if device has the right companyID 140 | if _, exists := dev.Properties.ManufacturerData[PGCompanyID]; !exists { 141 | continue 142 | } 143 | 144 | // Create brush from found device, append to list 145 | brushes = append(brushes, NewBrush(dev)) 146 | 147 | if len(brushes) == count { 148 | return brushes, nil 149 | } 150 | case <-ctx.Done(): 151 | return brushes, nil 152 | } 153 | } 154 | } 155 | 156 | func (bm brushScanner) Close() error { 157 | return api.Exit() 158 | } 159 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Raqbit/goralb 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/godbus/dbus v4.1.0+incompatible 7 | github.com/godbus/dbus/v5 v5.0.3 8 | github.com/muka/go-bluetooth v0.0.0-20201211051136-07f31c601d33 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 6 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 7 | github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= 8 | github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= 9 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 10 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 11 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 13 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/muka/go-bluetooth v0.0.0-20201211051136-07f31c601d33 h1:p3srutpE8TpQmOUQ5Qw94jYFUdoG2jBbILeYLroQNoI= 18 | github.com/muka/go-bluetooth v0.0.0-20201211051136-07f31c601d33/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0= 19 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 20 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 21 | github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk= 22 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 26 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 29 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 30 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8= 32 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 33 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 34 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 35 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 36 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 37 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 38 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 39 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 40 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 45 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 46 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE= 47 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 51 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 57 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 59 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | -------------------------------------------------------------------------------- /mode.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | //go:generate stringer -type=BrushMode -output mode_string.go -linecomment 4 | type BrushMode int 5 | 6 | const ( 7 | ModeOff BrushMode = 0x00 // Off 8 | ModeDailyClean BrushMode = 0x01 // Daily 9 | ModeSensitive BrushMode = 0x02 // Sensitive 10 | ModeMassage BrushMode = 0x03 // Massage 11 | ModeWhitening BrushMode = 0x04 // Whitening 12 | ModeDeepClean BrushMode = 0x05 // Deep Clean 13 | ModeTongueCleaning BrushMode = 0x06 // Tongue Cleaning 14 | ModeTurbo BrushMode = 0x07 // Turbo 15 | ModeUnknown BrushMode = 0xFF // Unknown 16 | ) 17 | -------------------------------------------------------------------------------- /mode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=BrushMode -output mode_string.go -linecomment"; DO NOT EDIT. 2 | 3 | package goralb 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ModeOff-0] 12 | _ = x[ModeDailyClean-1] 13 | _ = x[ModeSensitive-2] 14 | _ = x[ModeMassage-3] 15 | _ = x[ModeWhitening-4] 16 | _ = x[ModeDeepClean-5] 17 | _ = x[ModeTongueCleaning-6] 18 | _ = x[ModeTurbo-7] 19 | _ = x[ModeUnknown-255] 20 | } 21 | 22 | const ( 23 | _BrushMode_name_0 = "OffDailySensitiveMassageWhiteningDeep CleanTongue CleaningTurbo" 24 | _BrushMode_name_1 = "Unknown" 25 | ) 26 | 27 | var ( 28 | _BrushMode_index_0 = [...]uint8{0, 3, 8, 17, 24, 33, 43, 58, 63} 29 | ) 30 | 31 | func (i BrushMode) String() string { 32 | switch { 33 | case 0 <= i && i <= 7: 34 | return _BrushMode_name_0[_BrushMode_index_0[i]:_BrushMode_index_0[i+1]] 35 | case i == 255: 36 | return _BrushMode_name_1 37 | default: 38 | return "BrushMode(" + strconv.FormatInt(int64(i), 10) + ")" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /quadrant.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type BrushQuadrant int 8 | 9 | func (b BrushQuadrant) String() string { 10 | return fmt.Sprintf("%d", b) 11 | } 12 | 13 | const ( 14 | Sector1 BrushQuadrant = 0x01 15 | Sector2 BrushQuadrant = 0x02 16 | Sector3 BrushQuadrant = 0x03 17 | Sector4 BrushQuadrant = 0x04 18 | Sector5 BrushQuadrant = 0x05 19 | Sector6 BrushQuadrant = 0x06 20 | Sector7 BrushQuadrant = 0x07 21 | Sector8 BrushQuadrant = 0x08 22 | ) 23 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | type BrushState int 4 | 5 | //go:generate stringer -type=BrushState -output state_string.go -linecomment 6 | const ( 7 | StateUnknown BrushState = 0x00 // Unknown 8 | StateIdle BrushState = 0x02 // Idle 9 | StateRun BrushState = 0x03 // Run 10 | StateCharge BrushState = 0x04 // Charge 11 | ) 12 | -------------------------------------------------------------------------------- /state_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=BrushState -output state_string.go -linecomment"; DO NOT EDIT. 2 | 3 | package goralb 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[StateUnknown-0] 12 | _ = x[StateIdle-2] 13 | _ = x[StateRun-3] 14 | _ = x[StateCharge-4] 15 | } 16 | 17 | const ( 18 | _BrushState_name_0 = "Unknown" 19 | _BrushState_name_1 = "IdleRunCharge" 20 | ) 21 | 22 | var ( 23 | _BrushState_index_1 = [...]uint8{0, 4, 7, 13} 24 | ) 25 | 26 | func (i BrushState) String() string { 27 | switch { 28 | case i == 0: 29 | return _BrushState_name_0 30 | case 2 <= i && i <= 4: 31 | i -= 2 32 | return _BrushState_name_1[_BrushState_index_1[i]:_BrushState_index_1[i+1]] 33 | default: 34 | return "BrushState(" + strconv.FormatInt(int64(i), 10) + ")" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /status_configure.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | type statusConfigure byte 4 | 5 | const ( 6 | ExtendConnection statusConfigure = 0x00 7 | ReadParam statusConfigure = 0x01 8 | ReadData statusConfigure = 0x02 9 | CalibrationRead statusConfigure = 0x04 10 | ReadMetadata statusConfigure = 0x05 11 | RTC statusConfigure = 0x26 12 | BrushTimer statusConfigure = 0x28 13 | BrushModes statusConfigure = 0x29 14 | QuadrantTimers statusConfigure = 0x2a 15 | TongueTime statusConfigure = 0x2b 16 | MyColor statusConfigure = 0x2f 17 | Dashboard statusConfigure = 0x30 18 | RefillReminder statusConfigure = 0x31 19 | FactoryReset statusConfigure = 0x32 20 | SmartGuideDisable statusConfigure = 0x50 21 | SmartGuideEnable statusConfigure = 0x51 22 | ) 23 | -------------------------------------------------------------------------------- /status_control.go: -------------------------------------------------------------------------------- 1 | package goralb 2 | 3 | type statusControl byte 4 | 5 | const ( 6 | SetMode statusControl = 0x01 7 | StopTimerSignal statusControl = 0x20 8 | ResetMemoryTimer statusControl = 0x29 9 | MyColorDisable statusControl = 0x30 10 | MyColorEnable statusControl = 0x31 11 | MotionDisable statusControl = 0x40 12 | MotionEnable statusControl = 0x41 13 | ) 14 | --------------------------------------------------------------------------------