package infinitime import ( "bytes" "encoding/binary" "errors" "fmt" "time" bt "github.com/muka/go-bluetooth/api" "github.com/muka/go-bluetooth/bluez/profile/adapter" "github.com/muka/go-bluetooth/bluez/profile/device" "github.com/muka/go-bluetooth/bluez/profile/gatt" ) const BTName = "InfiniTime" const ( NewAlertChar = "00002a46-0000-1000-8000-00805f9b34fb" FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb" CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb" BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb" HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb" ) type Device struct { opts *Options device *device.Device1 newAlertChar *gatt.GattCharacteristic1 fwVersionChar *gatt.GattCharacteristic1 currentTimeChar *gatt.GattCharacteristic1 battLevelChar *gatt.GattCharacteristic1 heartRateChar *gatt.GattCharacteristic1 onReconnect func() Music MusicCtrl DFU DFU } var ErrNoDevices = errors.New("no InfiniTime devices found") var ErrNotFound = errors.New("could not find any advertising InfiniTime devices") type Options struct { AttemptReconnect bool PairTimeout time.Duration } var DefaultOptions = &Options{ AttemptReconnect: true, PairTimeout: time.Minute, } // Connect will attempt to connect to a // paired InfiniTime device. If none are paired, // it will attempt to discover and pair one. // // It will also attempt to reconnect to the device // if it disconnects. func Connect(opts *Options) (*Device, error) { if opts == nil { opts = DefaultOptions } // Attempt to connect to paired device by name dev, err := connectByName() // If such device does not exist if errors.Is(err, ErrNoDevices) { // Attempt to pair device dev, err = pair(opts.PairTimeout) } if err != nil { return nil, err } dev.opts = opts dev.onReconnect = func() {} // Watch device properties devEvtCh, err := dev.device.WatchProperties() if err != nil { return nil, err } // If AttemptReconnect enabled if dev.opts.AttemptReconnect { go func() { disconnEvtNum := 0 // For every event for evt := range devEvtCh { // If device disconnected if evt.Name == "Connected" && evt.Value == false { // Increment disconnect event number disconnEvtNum++ // If more than one disconnect event if disconnEvtNum > 1 { // Decrement disconnect event number disconnEvtNum-- // Skip loop continue } // Set connected to false dev.device.Properties.Connected = false // While not connected for !dev.device.Properties.Connected { // Attempt to connect via bluetooth address reConnDev, err := ConnectByAddress(dev.device.Properties.Address) if err != nil { // Decrement disconnect event number disconnEvtNum-- // Skip rest of loop continue } // Store onReconn callback onReconn := dev.onReconnect // Set device to new device *dev = *reConnDev // Run on reconnect callback onReconn() // Assign callback to new device dev.onReconnect = onReconn } // Decrement disconnect event number disconnEvtNum-- } } }() } return dev, nil } // OnReconnect sets the callback that runs on reconnect func (i *Device) OnReconnect(f func()) { i.onReconnect = f } // Connect connects to a paired InfiniTime device func connectByName() (*Device, error) { // Create new device out := &Device{} // Get devices from default adapter devs, err := defaultAdapter.GetDevices() if err != nil { return nil, err } // For every device for _, dev := range devs { // If device name is InfiniTime if dev.Properties.Name == BTName { // Set outout device to discovered device out.device = dev break } } if out.device == nil { return nil, ErrNoDevices } // Connect to device err = out.device.Connect() if err != nil { return nil, err } // Resolve characteristics err = out.resolveChars() if err != nil { return nil, err } return out, nil } // Pair attempts to discover and pair an InfiniTime device func pair(timeout time.Duration) (*Device, error) { // Create new device out := &Device{} // Start bluetooth discovery discovery, cancelDiscover, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"}) if err != nil { return nil, err } // Cancel discovery at end of function defer cancelDiscover() discoveryLoop: for { select { case event := <-discovery: // If device removed, skip event if event.Type == adapter.DeviceRemoved { continue } // Create new device with discovered path dev, err := device.NewDevice1(event.Path) if err != nil { return nil, err } // If device name is InfiniTime if dev.Properties.Name == BTName { // Set output device out.device = dev // Break out of discoveryLoop break discoveryLoop } case <-time.After(timeout): break discoveryLoop } } if out.device == nil { return nil, ErrNotFound } // Pair device out.device.Pair() // Set connected to true out.device.Properties.Connected = true // Resolve characteristics err = out.resolveChars() if err != nil { return nil, err } return out, nil } // ConnectByAddress tries to connect to an InifiniTime at // the specified InfiniTime address func ConnectByAddress(addr string) (*Device, error) { var err error // Create new device out := &Device{} // Get device from bluetooth address out.device, err = defaultAdapter.GetDeviceByAddress(addr) if err != nil { return nil, err } // Connect to device err = out.device.Connect() if err != nil { return nil, err } // Resolve characteristics err = out.resolveChars() if err != nil { return nil, err } return out, nil } // resolveChars attempts to set all required // characteristics in an InfiniTime struct func (i *Device) resolveChars() error { // Get device characteristics chars, err := i.device.GetCharacteristics() if err != nil { return err } // While no characteristics found for len(chars) == 0 { // Sleep one second time.Sleep(time.Second) // Attempt to retry getting characteristics chars, err = i.device.GetCharacteristics() if err != nil { return err } } // For every discovered characteristics for _, char := range chars { // Set correct characteristics switch char.Properties.UUID { case NewAlertChar: i.newAlertChar = char case FirmwareVerChar: i.fwVersionChar = char case CurrentTimeChar: i.currentTimeChar = char case BatteryLvlChar: i.battLevelChar = char case HeartRateChar: i.heartRateChar = char case MusicEventChar: i.Music.eventChar = char case MusicStatusChar: i.Music.statusChar = char case MusicArtistChar: i.Music.artistChar = char case MusicTrackChar: i.Music.trackChar = char case MusicAlbumChar: i.Music.albumChar = char case DFUCtrlPointChar: i.DFU.ctrlPointChar = char case DFUPacketChar: i.DFU.packetChar = char } } return nil } // Address returns the InfiniTime's bluetooth address func (i *Device) Address() string { return i.device.Properties.Address } // Version returns InfiniTime's reported firmware version string func (i *Device) Version() (string, error) { if !i.device.Properties.Connected { return "", nil } ver, err := i.fwVersionChar.ReadValue(nil) return string(ver), err } // BatteryLevel gets the watch's battery level via the Battery Service func (i *Device) BatteryLevel() (uint8, error) { if !i.device.Properties.Connected { return 0, nil } battLevel, err := i.battLevelChar.ReadValue(nil) if err != nil { return 0, err } return uint8(battLevel[0]), nil } func (i *Device) HeartRate() (uint8, error) { if !i.device.Properties.Connected { return 0, nil } heartRate, err := i.heartRateChar.ReadValue(nil) if err != nil { return 0, err } return uint8(heartRate[1]), nil } func (i *Device) WatchHeartRate() (<-chan uint8, error) { if !i.device.Properties.Connected { return make(<-chan uint8), nil } // Start notifications on heart rate characteristic err := i.heartRateChar.StartNotify() if err != nil { return nil, err } // Watch characteristics of heart rate characteristic ch, err := i.heartRateChar.WatchProperties() if err != nil { return nil, err } out := make(chan uint8, 2) go func() { // For every event for event := range ch { // If value changed if event.Name == "Value" { // Send heart rate to channel out <- uint8(event.Value.([]byte)[1]) } } }() return out, nil } // SetTime sets the watch's time using the Current Time Service func (i *Device) SetTime(t time.Time) error { if !i.device.Properties.Connected { return nil } buf := &bytes.Buffer{} binary.Write(buf, binary.LittleEndian, uint16(t.Year())) binary.Write(buf, binary.LittleEndian, uint8(t.Month())) binary.Write(buf, binary.LittleEndian, uint8(t.Day())) binary.Write(buf, binary.LittleEndian, uint8(t.Hour())) binary.Write(buf, binary.LittleEndian, uint8(t.Minute())) binary.Write(buf, binary.LittleEndian, uint8(t.Second())) binary.Write(buf, binary.LittleEndian, uint8(t.Weekday())) binary.Write(buf, binary.LittleEndian, uint8((t.Nanosecond()/1000)/1e6*256)) binary.Write(buf, binary.LittleEndian, uint8(0b0001)) return i.currentTimeChar.WriteValue(buf.Bytes(), nil) } // Notify sends a notification to InfiniTime via // the Alert Notification Service (ANS) func (i *Device) Notify(title, body string) error { if !i.device.Properties.Connected { return nil } return i.newAlertChar.WriteValue( []byte(fmt.Sprintf("00\x00%s\x00%s", title, body)), nil, ) }