From 9ed74726c47893b94221e3e109fe17095eb36a62 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Wed, 11 May 2022 13:22:57 -0700 Subject: [PATCH] Add contexts and improve error handling --- infinitime.go | 214 +++++++++++++++++++++++++++----------------------- 1 file changed, 116 insertions(+), 98 deletions(-) diff --git a/infinitime.go b/infinitime.go index f2ab1b0..d48e902 100644 --- a/infinitime.go +++ b/infinitime.go @@ -2,6 +2,7 @@ package infinitime import ( "bytes" + "context" "encoding/binary" "errors" "reflect" @@ -39,6 +40,20 @@ const ( WeatherDataChar = "00040001-78fc-48fe-8e23-433b3a1942d0" ) +var charNames = map[string]string{ + NewAlertChar: "New Alert", + NotifEventChar: "Notification Event", + StepCountChar: "Step Count", + MotionValChar: "Motion Values", + FirmwareVerChar: "Firmware Version", + CurrentTimeChar: "Current Time", + BatteryLvlChar: "Battery Level", + HeartRateChar: "Heart Rate", + FSTransferChar: "Filesystem Transfer", + FSVersionChar: "Filesystem Version", + WeatherDataChar: "Weather Data", +} + type Device struct { device *device.Device1 newAlertChar *gatt.GattCharacteristic1 @@ -62,11 +77,18 @@ 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") ErrPairTimeout = errors.New("reached timeout while pairing") ) +type ErrCharNotAvail struct { + uuid string +} + +func (e ErrCharNotAvail) Error() string { + return "characteristic " + e.uuid + " (" + charNames[e.uuid] + ") not available" +} + type Options struct { AttemptReconnect bool WhitelistEnabled bool @@ -90,7 +112,7 @@ var DefaultOptions = &Options{ // // It will also attempt to reconnect to the device // if it disconnects and that is enabled in the options. -func Connect(opts *Options) (*Device, error) { +func Connect(ctx context.Context, opts *Options) (*Device, error) { if opts == nil { opts = DefaultOptions } @@ -101,7 +123,7 @@ func Connect(opts *Options) (*Device, error) { setOnPasskeyReq(opts.OnReqPasskey) // Connect to bluetooth device - btDev, err := connect(opts, true) + btDev, err := connect(ctx, opts, true) if err != nil { return nil, err } @@ -119,7 +141,7 @@ func Connect(opts *Options) (*Device, error) { } // connect connects to the InfiniTime bluez device -func connect(opts *Options, first bool) (dev *device.Device1, err error) { +func connect(ctx context.Context, opts *Options, first bool) (dev *device.Device1, err error) { // Get devices devs, err := defaultAdapter.GetDevices() if err != nil { @@ -161,42 +183,49 @@ func connect(opts *Options, first bool) (dev *device.Device1, err error) { return nil, err } - // For every discovery event - for event := range discoverCh { - // If event type is not device added, skip - if event.Type != adapter.DeviceAdded { - continue - } + discoverLoop: + for { + select { + case event := <-discoverCh: + // If event type is not device added, skip + if event.Type != adapter.DeviceAdded { + continue + } - // Create new device from event path - discovered, err := device.NewDevice1(event.Path) - if err != nil { - return nil, err - } + // Create new device from event path + discovered, err := device.NewDevice1(event.Path) + if err != nil { + return nil, err + } + + // If device name does not match, skip + if discovered.Properties.Name != BTName { + continue + } + // If whitelist enabled and doesn't contain + // device, skip + if opts.WhitelistEnabled && + !contains(opts.Whitelist, discovered.Properties.Address) { + log.Debug(). + Str("mac", discovered.Properties.Address). + Msg("Discovered InfiniTime device skipped as it is not in whitelist") + continue + } + + // Set device + dev = discovered - // If device name does not match, skip - if discovered.Properties.Name != BTName { - continue - } - // If whitelist enabled and doesn't contain - // device, skip - if opts.WhitelistEnabled && - !contains(opts.Whitelist, discovered.Properties.Address) { log.Debug(). - Str("mac", discovered.Properties.Address). - Msg("Discovered InfiniTime device skipped as it is not in whitelist") - continue + Str("mac", dev.Properties.Address). + Msg("InfiniTime device discovered") + break discoverLoop + case <-ctx.Done(): + break discoverLoop + } - - // Set device - dev = discovered - - log.Debug(). - Str("mac", dev.Properties.Address). - Msg("InfiniTime device discovered") - break } - // Stop discovery + + // Cancel discovery cancel() } @@ -233,7 +262,7 @@ func connect(opts *Options, first bool) (dev *device.Device1, err error) { // If this is the first connection and reconnect // is enabled, start reconnect goroutine if first && opts.AttemptReconnect { - go reconnect(opts, dev) + go reconnect(ctx, opts, dev) } // If this is not the first connection, a reonnect @@ -248,7 +277,7 @@ func connect(opts *Options, first bool) (dev *device.Device1, err error) { } // reconnect reconnects to a device if it disconnects -func reconnect(opts *Options, dev *device.Device1) { +func reconnect(ctx context.Context, opts *Options, dev *device.Device1) { // Watch device properties propCh := watchProps(dev) @@ -288,7 +317,7 @@ func reconnect(opts *Options, dev *device.Device1) { opts.Logger.Warn().Msg("Multiple connection attempts have failed. If this continues, try removing the InfiniTime device from bluetooth.") } // Connect to device - newDev, err := connect(opts, false) + newDev, err := connect(ctx, opts, false) if err != nil { time.Sleep(time.Second) continue @@ -407,6 +436,7 @@ func (i *Device) resolveChars() error { if charResolved { log.Debug(). Str("uuid", char.Properties.UUID). + Str("name", charNames[char.Properties.UUID]). Msg("Resolved characteristic") } } @@ -420,7 +450,7 @@ func (i *Device) Address() string { // Version returns InfiniTime's reported firmware version string func (i *Device) Version() (string, error) { - if err := i.checkStatus(i.fwVersionChar); err != nil { + if err := i.checkStatus(i.fwVersionChar, FirmwareVerChar); err != nil { return "", err } ver, err := i.fwVersionChar.ReadValue(nil) @@ -429,7 +459,7 @@ func (i *Device) Version() (string, error) { // 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 { + if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil { return 0, err } battLevel, err := i.battLevelChar.ReadValue(nil) @@ -440,7 +470,7 @@ func (i *Device) BatteryLevel() (uint8, error) { } func (i *Device) StepCount() (uint32, error) { - if err := i.checkStatus(i.stepCountChar); err != nil { + if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil { return 0, err } stepCountData, err := i.stepCountChar.ReadValue(nil) @@ -458,7 +488,7 @@ type MotionValues struct { func (i *Device) Motion() (MotionValues, error) { out := MotionValues{} - if err := i.checkStatus(i.motionValChar); err != nil { + if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil { return out, err } motionVals, err := i.motionValChar.ReadValue(nil) @@ -474,7 +504,7 @@ func (i *Device) Motion() (MotionValues, error) { } func (i *Device) HeartRate() (uint8, error) { - if err := i.checkStatus(i.heartRateChar); err != nil { + if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil { return 0, err } heartRate, err := i.heartRateChar.ReadValue(nil) @@ -484,35 +514,33 @@ func (i *Device) HeartRate() (uint8, error) { 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 +func (i *Device) WatchHeartRate(ctx context.Context) (<-chan uint8, error) { + if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil { + return nil, err } // Start notifications on heart rate characteristic err := i.heartRateChar.StartNotify() if err != nil { - return nil, nil, err + return nil, err } // Watch characteristics of heart rate characteristic ch, err := i.heartRateChar.WatchProperties() if err != nil { - return nil, nil, err + return nil, err } out := make(chan uint8, 2) currentHeartRate, err := i.HeartRate() if err != nil { - return nil, nil, err + return nil, err } out <- currentHeartRate - cancel, done := cancelFunc() go func() { // For every event for { select { - case <-done: + case <-ctx.Done(): log.Debug().Str("func", "WatchMotion").Msg("Received done signal") close(out) - close(done) i.heartRateChar.StopNotify() return case event := <-ch: @@ -527,38 +555,36 @@ func (i *Device) WatchHeartRate() (<-chan uint8, func(), error) { } } }() - return out, cancel, nil + return out, nil } -func (i *Device) WatchBatteryLevel() (<-chan uint8, func(), error) { - if err := i.checkStatus(i.battLevelChar); err != nil { - return nil, nil, err +func (i *Device) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) { + if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil { + return nil, err } // Start notifications on heart rate characteristic err := i.battLevelChar.StartNotify() if err != nil { - return nil, nil, err + return nil, err } // Watch characteristics of heart rate characteristic ch, err := i.battLevelChar.WatchProperties() if err != nil { - return nil, nil, err + return nil, err } out := make(chan uint8, 2) currentBattLevel, err := i.BatteryLevel() if err != nil { - return nil, nil, err + return nil, err } out <- currentBattLevel - cancel, done := cancelFunc() go func() { // For every event for { select { - case <-done: + case <-ctx.Done(): log.Debug().Str("func", "WatchMotion").Msg("Received done signal") close(out) - close(done) i.battLevelChar.StopNotify() return case event := <-ch: @@ -573,38 +599,36 @@ func (i *Device) WatchBatteryLevel() (<-chan uint8, func(), error) { } } }() - return out, cancel, nil + return out, nil } -func (i *Device) WatchStepCount() (<-chan uint32, func(), error) { - if err := i.checkStatus(i.stepCountChar); err != nil { - return nil, nil, err +func (i *Device) WatchStepCount(ctx context.Context) (<-chan uint32, error) { + if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil { + return nil, err } // Start notifications on step count characteristic err := i.stepCountChar.StartNotify() if err != nil { - return nil, nil, err + return nil, err } // Watch properties of step count characteristic ch, err := i.stepCountChar.WatchProperties() if err != nil { - return nil, nil, err + return nil, err } out := make(chan uint32, 2) currentStepCount, err := i.StepCount() if err != nil { - return nil, nil, err + return nil, err } out <- currentStepCount - cancel, done := cancelFunc() go func() { // For every event for { select { - case <-done: + case <-ctx.Done(): log.Debug().Str("func", "WatchMotion").Msg("Received done signal") close(out) - close(done) i.stepCountChar.StopNotify() return case event := <-ch: @@ -619,38 +643,36 @@ func (i *Device) WatchStepCount() (<-chan uint32, func(), error) { } } }() - return out, cancel, nil + return out, nil } -func (i *Device) WatchMotion() (<-chan MotionValues, func(), error) { - if err := i.checkStatus(i.motionValChar); err != nil { - return nil, nil, err +func (i *Device) WatchMotion(ctx context.Context) (<-chan MotionValues, error) { + if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil { + return nil, err } // Start notifications on motion characteristic err := i.motionValChar.StartNotify() if err != nil { - return nil, nil, err + return nil, err } // Watch properties of motion characteristic ch, err := i.motionValChar.WatchProperties() if err != nil { - return nil, nil, err + return nil, err } out := make(chan MotionValues, 2) motionVals, err := i.Motion() if err != nil { - return nil, nil, err + return nil, err } out <- motionVals - cancel, done := cancelFunc() go func() { // For every event for { select { - case <-done: + case <-ctx.Done(): log.Debug().Str("func", "WatchMotion").Msg("Received done signal") close(out) - close(done) i.motionValChar.StopNotify() return case event := <-ch: @@ -668,19 +690,12 @@ func (i *Device) WatchMotion() (<-chan MotionValues, func(), error) { } }() - return out, cancel, nil -} - -func cancelFunc() (func(), chan struct{}) { - done := make(chan struct{}, 1) - return func() { - done <- struct{}{} - }, done + return out, nil } // 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 { + if err := i.checkStatus(i.currentTimeChar, CurrentTimeChar); err != nil { return err } buf := &bytes.Buffer{} @@ -699,7 +714,7 @@ func (i *Device) SetTime(t time.Time) error { // 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 { + if err := i.checkStatus(i.newAlertChar, NewAlertChar); err != nil { return err } return i.newAlertChar.WriteValue( @@ -718,7 +733,7 @@ const ( // 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 { + if err := i.checkStatus(i.newAlertChar, NewAlertChar); err != nil { return nil, err } // Write call notification to new alert characteristic @@ -770,7 +785,7 @@ func (i *Device) initNotifEvent() error { // 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 { + if err := i.checkStatus(i.fsTransferChar, FSTransferChar); err != nil { return nil, err } return blefs.New(i.fsTransferChar) @@ -780,7 +795,7 @@ func (i *Device) FS() (*blefs.FS, error) { // 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 { + if err := i.checkStatus(i.weatherDataChar, WeatherDataChar); err != nil { return err } // Get type of input @@ -803,7 +818,7 @@ func (i *Device) AddWeatherEvent(event interface{}) error { return i.weatherDataChar.WriteValue(data, nil) } -func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error { +func (i *Device) checkStatus(char *gatt.GattCharacteristic1, uuid string) error { log.Debug().Msg("Checking characteristic status") connected, err := i.device.GetConnected() if err != nil { @@ -814,8 +829,11 @@ func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error { } if char == nil { log.Debug().Msg("Characteristic not available (nil)") - return ErrCharNotAvail + return ErrCharNotAvail{uuid} } - log.Debug().Str("uuid", char.Properties.UUID).Msg("Characteristic available") + log.Debug(). + Str("uuid", char.Properties.UUID). + Str("name", charNames[char.Properties.UUID]). + Msg("Characteristic available") return nil }