Add contexts and improve error handling

This commit is contained in:
Elara 2022-05-11 13:22:57 -07:00
parent 1533865e4c
commit 9ed74726c4

View File

@ -2,6 +2,7 @@ package infinitime
import ( import (
"bytes" "bytes"
"context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"reflect" "reflect"
@ -39,6 +40,20 @@ const (
WeatherDataChar = "00040001-78fc-48fe-8e23-433b3a1942d0" 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 { type Device struct {
device *device.Device1 device *device.Device1
newAlertChar *gatt.GattCharacteristic1 newAlertChar *gatt.GattCharacteristic1
@ -62,11 +77,18 @@ var (
ErrNoDevices = errors.New("no InfiniTime devices found") ErrNoDevices = errors.New("no InfiniTime devices found")
ErrNotFound = errors.New("could not find any advertising InfiniTime devices") ErrNotFound = errors.New("could not find any advertising InfiniTime devices")
ErrNotConnected = errors.New("not connected") ErrNotConnected = errors.New("not connected")
ErrCharNotAvail = errors.New("required characteristic is not available")
ErrNoTimelineHeader = errors.New("events must contain the timeline header") ErrNoTimelineHeader = errors.New("events must contain the timeline header")
ErrPairTimeout = errors.New("reached timeout while pairing") 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 { type Options struct {
AttemptReconnect bool AttemptReconnect bool
WhitelistEnabled bool WhitelistEnabled bool
@ -90,7 +112,7 @@ var DefaultOptions = &Options{
// //
// It will also attempt to reconnect to the device // It will also attempt to reconnect to the device
// if it disconnects and that is enabled in the options. // 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 { if opts == nil {
opts = DefaultOptions opts = DefaultOptions
} }
@ -101,7 +123,7 @@ func Connect(opts *Options) (*Device, error) {
setOnPasskeyReq(opts.OnReqPasskey) setOnPasskeyReq(opts.OnReqPasskey)
// Connect to bluetooth device // Connect to bluetooth device
btDev, err := connect(opts, true) btDev, err := connect(ctx, opts, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -119,7 +141,7 @@ func Connect(opts *Options) (*Device, error) {
} }
// connect connects to the InfiniTime bluez device // 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 // Get devices
devs, err := defaultAdapter.GetDevices() devs, err := defaultAdapter.GetDevices()
if err != nil { if err != nil {
@ -161,42 +183,49 @@ func connect(opts *Options, first bool) (dev *device.Device1, err error) {
return nil, err return nil, err
} }
// For every discovery event discoverLoop:
for event := range discoverCh { for {
// If event type is not device added, skip select {
if event.Type != adapter.DeviceAdded { case event := <-discoverCh:
continue // If event type is not device added, skip
} if event.Type != adapter.DeviceAdded {
continue
}
// Create new device from event path // Create new device from event path
discovered, err := device.NewDevice1(event.Path) discovered, err := device.NewDevice1(event.Path)
if err != nil { if err != nil {
return nil, err 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(). log.Debug().
Str("mac", discovered.Properties.Address). Str("mac", dev.Properties.Address).
Msg("Discovered InfiniTime device skipped as it is not in whitelist") Msg("InfiniTime device discovered")
continue 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() cancel()
} }
@ -233,7 +262,7 @@ func connect(opts *Options, first bool) (dev *device.Device1, err error) {
// If this is the first connection and reconnect // If this is the first connection and reconnect
// is enabled, start reconnect goroutine // is enabled, start reconnect goroutine
if first && opts.AttemptReconnect { if first && opts.AttemptReconnect {
go reconnect(opts, dev) go reconnect(ctx, opts, dev)
} }
// If this is not the first connection, a reonnect // 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 // 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 // Watch device properties
propCh := watchProps(dev) 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.") opts.Logger.Warn().Msg("Multiple connection attempts have failed. If this continues, try removing the InfiniTime device from bluetooth.")
} }
// Connect to device // Connect to device
newDev, err := connect(opts, false) newDev, err := connect(ctx, opts, false)
if err != nil { if err != nil {
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
@ -407,6 +436,7 @@ func (i *Device) resolveChars() error {
if charResolved { if charResolved {
log.Debug(). log.Debug().
Str("uuid", char.Properties.UUID). Str("uuid", char.Properties.UUID).
Str("name", charNames[char.Properties.UUID]).
Msg("Resolved characteristic") Msg("Resolved characteristic")
} }
} }
@ -420,7 +450,7 @@ func (i *Device) Address() string {
// Version returns InfiniTime's reported firmware version string // Version returns InfiniTime's reported firmware version string
func (i *Device) Version() (string, error) { 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 return "", err
} }
ver, err := i.fwVersionChar.ReadValue(nil) 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 // BatteryLevel gets the watch's battery level via the Battery Service
func (i *Device) BatteryLevel() (uint8, error) { 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 return 0, err
} }
battLevel, err := i.battLevelChar.ReadValue(nil) battLevel, err := i.battLevelChar.ReadValue(nil)
@ -440,7 +470,7 @@ func (i *Device) BatteryLevel() (uint8, error) {
} }
func (i *Device) StepCount() (uint32, 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 return 0, err
} }
stepCountData, err := i.stepCountChar.ReadValue(nil) stepCountData, err := i.stepCountChar.ReadValue(nil)
@ -458,7 +488,7 @@ type MotionValues struct {
func (i *Device) Motion() (MotionValues, error) { func (i *Device) Motion() (MotionValues, error) {
out := MotionValues{} out := MotionValues{}
if err := i.checkStatus(i.motionValChar); err != nil { if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil {
return out, err return out, err
} }
motionVals, err := i.motionValChar.ReadValue(nil) motionVals, err := i.motionValChar.ReadValue(nil)
@ -474,7 +504,7 @@ func (i *Device) Motion() (MotionValues, error) {
} }
func (i *Device) HeartRate() (uint8, 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 return 0, err
} }
heartRate, err := i.heartRateChar.ReadValue(nil) heartRate, err := i.heartRateChar.ReadValue(nil)
@ -484,35 +514,33 @@ func (i *Device) HeartRate() (uint8, error) {
return uint8(heartRate[1]), nil return uint8(heartRate[1]), nil
} }
func (i *Device) WatchHeartRate() (<-chan uint8, func(), error) { func (i *Device) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
if err := i.checkStatus(i.heartRateChar); err != nil { if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil {
return nil, nil, err return nil, err
} }
// Start notifications on heart rate characteristic // Start notifications on heart rate characteristic
err := i.heartRateChar.StartNotify() err := i.heartRateChar.StartNotify()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// Watch characteristics of heart rate characteristic // Watch characteristics of heart rate characteristic
ch, err := i.heartRateChar.WatchProperties() ch, err := i.heartRateChar.WatchProperties()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out := make(chan uint8, 2) out := make(chan uint8, 2)
currentHeartRate, err := i.HeartRate() currentHeartRate, err := i.HeartRate()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out <- currentHeartRate out <- currentHeartRate
cancel, done := cancelFunc()
go func() { go func() {
// For every event // For every event
for { for {
select { select {
case <-done: case <-ctx.Done():
log.Debug().Str("func", "WatchMotion").Msg("Received done signal") log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
close(out) close(out)
close(done)
i.heartRateChar.StopNotify() i.heartRateChar.StopNotify()
return return
case event := <-ch: 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) { func (i *Device) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
if err := i.checkStatus(i.battLevelChar); err != nil { if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil {
return nil, nil, err return nil, err
} }
// Start notifications on heart rate characteristic // Start notifications on heart rate characteristic
err := i.battLevelChar.StartNotify() err := i.battLevelChar.StartNotify()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// Watch characteristics of heart rate characteristic // Watch characteristics of heart rate characteristic
ch, err := i.battLevelChar.WatchProperties() ch, err := i.battLevelChar.WatchProperties()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out := make(chan uint8, 2) out := make(chan uint8, 2)
currentBattLevel, err := i.BatteryLevel() currentBattLevel, err := i.BatteryLevel()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out <- currentBattLevel out <- currentBattLevel
cancel, done := cancelFunc()
go func() { go func() {
// For every event // For every event
for { for {
select { select {
case <-done: case <-ctx.Done():
log.Debug().Str("func", "WatchMotion").Msg("Received done signal") log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
close(out) close(out)
close(done)
i.battLevelChar.StopNotify() i.battLevelChar.StopNotify()
return return
case event := <-ch: 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) { func (i *Device) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
if err := i.checkStatus(i.stepCountChar); err != nil { if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil {
return nil, nil, err return nil, err
} }
// Start notifications on step count characteristic // Start notifications on step count characteristic
err := i.stepCountChar.StartNotify() err := i.stepCountChar.StartNotify()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// Watch properties of step count characteristic // Watch properties of step count characteristic
ch, err := i.stepCountChar.WatchProperties() ch, err := i.stepCountChar.WatchProperties()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out := make(chan uint32, 2) out := make(chan uint32, 2)
currentStepCount, err := i.StepCount() currentStepCount, err := i.StepCount()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out <- currentStepCount out <- currentStepCount
cancel, done := cancelFunc()
go func() { go func() {
// For every event // For every event
for { for {
select { select {
case <-done: case <-ctx.Done():
log.Debug().Str("func", "WatchMotion").Msg("Received done signal") log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
close(out) close(out)
close(done)
i.stepCountChar.StopNotify() i.stepCountChar.StopNotify()
return return
case event := <-ch: 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) { func (i *Device) WatchMotion(ctx context.Context) (<-chan MotionValues, error) {
if err := i.checkStatus(i.motionValChar); err != nil { if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil {
return nil, nil, err return nil, err
} }
// Start notifications on motion characteristic // Start notifications on motion characteristic
err := i.motionValChar.StartNotify() err := i.motionValChar.StartNotify()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
// Watch properties of motion characteristic // Watch properties of motion characteristic
ch, err := i.motionValChar.WatchProperties() ch, err := i.motionValChar.WatchProperties()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out := make(chan MotionValues, 2) out := make(chan MotionValues, 2)
motionVals, err := i.Motion() motionVals, err := i.Motion()
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
out <- motionVals out <- motionVals
cancel, done := cancelFunc()
go func() { go func() {
// For every event // For every event
for { for {
select { select {
case <-done: case <-ctx.Done():
log.Debug().Str("func", "WatchMotion").Msg("Received done signal") log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
close(out) close(out)
close(done)
i.motionValChar.StopNotify() i.motionValChar.StopNotify()
return return
case event := <-ch: case event := <-ch:
@ -668,19 +690,12 @@ func (i *Device) WatchMotion() (<-chan MotionValues, func(), error) {
} }
}() }()
return out, cancel, nil return out, 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 // SetTime sets the watch's time using the Current Time Service
func (i *Device) SetTime(t time.Time) error { 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 return err
} }
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
@ -699,7 +714,7 @@ func (i *Device) SetTime(t time.Time) error {
// Notify sends a notification to InfiniTime via // Notify sends a notification to InfiniTime via
// the Alert Notification Service (ANS) // the Alert Notification Service (ANS)
func (i *Device) Notify(title, body string) error { 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 err
} }
return i.newAlertChar.WriteValue( return i.newAlertChar.WriteValue(
@ -718,7 +733,7 @@ const (
// NotifyCall sends a call notification to the PineTime and returns a channel. // NotifyCall sends a call notification to the PineTime and returns a channel.
// This channel will contain the user's response to the call notification. // This channel will contain the user's response to the call notification.
func (i *Device) NotifyCall(from string) (<-chan uint8, error) { 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 return nil, err
} }
// Write call notification to new alert characteristic // 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 // FS creates and returns a new filesystem from the device
func (i *Device) FS() (*blefs.FS, error) { 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 nil, err
} }
return blefs.New(i.fsTransferChar) 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 // the weather package to the timeline. Input must be
// a struct containing TimelineHeader. // a struct containing TimelineHeader.
func (i *Device) AddWeatherEvent(event interface{}) error { 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 return err
} }
// Get type of input // Get type of input
@ -803,7 +818,7 @@ func (i *Device) AddWeatherEvent(event interface{}) error {
return i.weatherDataChar.WriteValue(data, nil) 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") log.Debug().Msg("Checking characteristic status")
connected, err := i.device.GetConnected() connected, err := i.device.GetConnected()
if err != nil { if err != nil {
@ -814,8 +829,11 @@ func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error {
} }
if char == nil { if char == nil {
log.Debug().Msg("Characteristic not available (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 return nil
} }