forked from Elara6331/infinitime
		
	Add contexts and improve error handling
This commit is contained in:
		
							
								
								
									
										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