package infinitime import ( "bytes" "encoding/binary" "errors" "reflect" "strings" "time" "github.com/fxamacker/cbor/v2" 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" "go.arsenm.dev/infinitime/blefs" ) const BTName = "InfiniTime" const ( NewAlertChar = "00002a46-0000-1000-8000-00805f9b34fb" NotifEventChar = "00020001-78fc-48fe-8e23-433b3a1942d0" StepCountChar = "00030001-78fc-48fe-8e23-433b3a1942d0" MotionValChar = "00030002-78fc-48fe-8e23-433b3a1942d0" FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb" CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb" BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb" HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb" FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572" FSVersionChar = "adaf0100-4669-6c65-5472-616e73666572" WeatherDataChar = "00040001-78fc-48fe-8e23-433b3a1942d0" ) type Device struct { opts *Options device *device.Device1 newAlertChar *gatt.GattCharacteristic1 notifEventChar *gatt.GattCharacteristic1 stepCountChar *gatt.GattCharacteristic1 motionValChar *gatt.GattCharacteristic1 fwVersionChar *gatt.GattCharacteristic1 currentTimeChar *gatt.GattCharacteristic1 battLevelChar *gatt.GattCharacteristic1 heartRateChar *gatt.GattCharacteristic1 fsVersionChar *gatt.GattCharacteristic1 fsTransferChar *gatt.GattCharacteristic1 weatherDataChar *gatt.GattCharacteristic1 notifEventCh chan uint8 notifEventDone bool onReconnect func() Music MusicCtrl DFU DFU } var ( ErrNoDevices = errors.New("no InfiniTime devices found") ErrNotFound = errors.New("could not find any advertising InfiniTime devices") ErrNotConnected = errors.New("not connected") ErrCharNotAvail = errors.New("required characteristic is not available") ErrNoTimelineHeader = errors.New("events must contain the timeline header") ) type Options struct { AttemptReconnect bool WhitelistEnabled bool Whitelist []string } var DefaultOptions = &Options{ AttemptReconnect: true, WhitelistEnabled: false, } // 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(opts) // If such device does not exist if errors.Is(err, ErrNoDevices) { // Attempt to pair device dev, err = pair(opts) } 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(opts *Options) (*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 { if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) { continue } // 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 } out.device.Properties.Connected = true // Resolve characteristics err = out.resolveChars() if err != nil { return nil, err } return out, nil } func contains(ss []string, s string) bool { for _, str := range ss { if strings.EqualFold(str, s) { return true } } return false } // Pair attempts to discover and pair an InfiniTime device func pair(opts *Options) (*Device, error) { // Create new device out := &Device{} // Start bluetooth discovery // Ignore the cancel function as it blocks forever discovery, _, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"}) if err != nil { return nil, err } // For every discovery event for event := range 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 { if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) { continue } // Set output device out.device = dev break } } if out.device == nil { return nil, ErrNotFound } // Pair device out.device.Pair() out.device.Properties.Connected = true // 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 } out.device.Properties.Connected = true // 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 NotifEventChar: i.notifEventChar = char case StepCountChar: i.stepCountChar = char case MotionValChar: i.motionValChar = 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 case FSTransferChar: i.fsTransferChar = char case FSVersionChar: i.fsVersionChar = char case WeatherDataChar: i.weatherDataChar = 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 err := i.checkStatus(i.fwVersionChar); err != nil { return "", err } 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 err := i.checkStatus(i.battLevelChar); err != nil { return 0, err } battLevel, err := i.battLevelChar.ReadValue(nil) if err != nil { return 0, err } return uint8(battLevel[0]), nil } func (i *Device) StepCount() (uint32, error) { if err := i.checkStatus(i.stepCountChar); err != nil { return 0, err } stepCountData, err := i.stepCountChar.ReadValue(nil) if err != nil { return 0, err } return binary.LittleEndian.Uint32(stepCountData), nil } type MotionValues struct { X int16 Y int16 Z int16 } func (i *Device) Motion() (MotionValues, error) { out := MotionValues{} if err := i.checkStatus(i.motionValChar); err != nil { return out, err } motionVals, err := i.motionValChar.ReadValue(nil) if err != nil { return out, nil } motionValReader := bytes.NewReader(motionVals) err = binary.Read(motionValReader, binary.LittleEndian, &out) if err != nil { return out, err } return out, nil } func (i *Device) HeartRate() (uint8, error) { if err := i.checkStatus(i.heartRateChar); err != nil { return 0, err } heartRate, err := i.heartRateChar.ReadValue(nil) if err != nil { return 0, err } return uint8(heartRate[1]), nil } func (i *Device) WatchHeartRate() (<-chan uint8, func(), error) { if err := i.checkStatus(i.heartRateChar); err != nil { return nil, nil, err } // Start notifications on heart rate characteristic err := i.heartRateChar.StartNotify() if err != nil { return nil, nil, err } // Watch characteristics of heart rate characteristic ch, err := i.heartRateChar.WatchProperties() if err != nil { return nil, nil, err } out := make(chan uint8, 2) currentHeartRate, err := i.HeartRate() if err != nil { return nil, nil, err } out <- currentHeartRate cancel, done := cancelFunc() go func() { // For every event for event := range ch { select { case <-done: close(out) close(done) i.heartRateChar.StopNotify() return default: // If value changed if event.Name == "Value" { // Send heart rate to channel out <- uint8(event.Value.([]byte)[1]) } } } }() return out, cancel, nil } func (i *Device) WatchBatteryLevel() (<-chan uint8, func(), error) { if err := i.checkStatus(i.battLevelChar); err != nil { return nil, nil, err } // Start notifications on heart rate characteristic err := i.battLevelChar.StartNotify() if err != nil { return nil, nil, err } // Watch characteristics of heart rate characteristic ch, err := i.battLevelChar.WatchProperties() if err != nil { return nil, nil, err } out := make(chan uint8, 2) currentBattLevel, err := i.BatteryLevel() if err != nil { return nil, nil, err } out <- currentBattLevel cancel, done := cancelFunc() go func() { // For every event for event := range ch { select { case <-done: close(out) close(done) i.battLevelChar.StopNotify() return default: // If value changed if event.Name == "Value" { // Send heart rate to channel out <- uint8(event.Value.([]byte)[0]) } } } }() return out, cancel, nil } func (i *Device) WatchStepCount() (<-chan uint32, func(), error) { if err := i.checkStatus(i.stepCountChar); err != nil { return nil, nil, err } // Start notifications on step count characteristic err := i.stepCountChar.StartNotify() if err != nil { return nil, nil, err } // Watch properties of step count characteristic ch, err := i.stepCountChar.WatchProperties() if err != nil { return nil, nil, err } out := make(chan uint32, 2) currentStepCount, err := i.StepCount() if err != nil { return nil, nil, err } out <- currentStepCount cancel, done := cancelFunc() go func() { // For every event for event := range ch { select { case <-done: close(out) close(done) i.stepCountChar.StopNotify() return default: // If value changed if event.Name == "Value" { // Send step count to channel out <- binary.LittleEndian.Uint32(event.Value.([]byte)) } } } }() return out, cancel, nil } func (i *Device) WatchMotion() (<-chan MotionValues, func(), error) { if err := i.checkStatus(i.motionValChar); err != nil { return nil, nil, err } // Start notifications on motion characteristic err := i.motionValChar.StartNotify() if err != nil { return nil, nil, err } // Watch properties of motion characteristic ch, err := i.motionValChar.WatchProperties() if err != nil { return nil, nil, err } out := make(chan MotionValues, 2) motionVals, err := i.Motion() if err != nil { return nil, nil, err } out <- motionVals cancel, done := cancelFunc() go func() { // For every event for event := range ch { select { case <-done: close(out) close(done) i.motionValChar.StopNotify() return default: // If value changed if event.Name == "Value" { // Read binary into MotionValues struct binary.Read(bytes.NewReader(event.Value.([]byte)), binary.LittleEndian, &motionVals) // Send step count to channel out <- motionVals } } } }() return out, cancel, nil } func cancelFunc() (func(), chan struct{}) { done := make(chan struct{}, 1) return func() { done <- struct{}{} }, done } // SetTime sets the watch's time using the Current Time Service func (i *Device) SetTime(t time.Time) error { if err := i.checkStatus(i.currentTimeChar); err != nil { return err } 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 err := i.checkStatus(i.newAlertChar); err != nil { return err } return i.newAlertChar.WriteValue( append([]byte{0x00, 0x01, 0x00}, []byte(title+"\x00"+body)...), nil, ) } // These constants represent the possible call statuses selected by the user const ( CallStatusDeclined uint8 = iota CallStatusAccepted CallStatusMuted ) // NotifyCall sends a call notification to the PineTime and returns a channel. // This channel will contain the user's response to the call notification. func (i *Device) NotifyCall(from string) (<-chan uint8, error) { if err := i.checkStatus(i.newAlertChar); err != nil { return nil, err } // Write call notification to new alert characteristic err := i.newAlertChar.WriteValue( append([]byte{0x03, 0x01, 0x00}, []byte(from)...), nil, ) if err != nil { return nil, err } if !i.notifEventDone { err = i.initNotifEvent() if err != nil { return nil, err } i.notifEventDone = true } return i.notifEventCh, nil } // initNotifEvent initializes the notification event channel func (i *Device) initNotifEvent() error { // Start notifications on notification event characteristic err := i.notifEventChar.StartNotify() if err != nil { return err } // Watch properties of notification event characteristic ch, err := i.notifEventChar.WatchProperties() if err != nil { return err } // Create new output channel for status i.notifEventCh = make(chan uint8, 1) go func() { // For every event for event := range ch { // If value changed if event.Name == "Value" { // Send status to channel i.notifEventCh <- uint8(event.Value.([]byte)[0]) } } }() return nil } // FS creates and returns a new filesystem from the device func (i *Device) FS() (*blefs.FS, error) { if err := i.checkStatus(i.fsTransferChar); err != nil { return nil, err } return blefs.New(i.fsTransferChar) } // AddWeatherEvent adds one of the event structs from // the weather package to the timeline. Input must be // a struct containing TimelineHeader. func (i *Device) AddWeatherEvent(event interface{}) error { if err := i.checkStatus(i.weatherDataChar); err != nil { return err } // Get type of input inputType := reflect.TypeOf(event) // Check if input contains TimelineHeader _, hdrExists := inputType.FieldByName("TimelineHeader") // If header does not exist or input is not struct if !hdrExists || inputType.Kind() != reflect.Struct { return ErrNoTimelineHeader } // Encode event as CBOR data, err := cbor.Marshal(event) if err != nil { return err } // Write data to weather data characteristic return i.weatherDataChar.WriteValue(data, nil) } func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error { if !i.device.Properties.Connected { return ErrNotConnected } if char == nil { return ErrCharNotAvail } return nil }