Add contexts and improve error handling

This commit is contained in:
Elara 2022-05-11 13:22:57 -07:00
parent e3a6bd308d
commit 91a47acb50

View File

@ -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
}