Add contexts and improve error handling
This commit is contained in:
parent
1533865e4c
commit
9ed74726c4
214
infinitime.go
214
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user