cybuzuma
d20e8af00e
Co-authored-by: cybuzuma <cybuzuma@vnxs.de> Co-committed-by: cybuzuma <cybuzuma@vnxs.de>
893 lines
24 KiB
Go
893 lines
24 KiB
Go
package infinitime
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
bt "github.com/muka/go-bluetooth/api"
|
|
"github.com/muka/go-bluetooth/bluez"
|
|
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
|
"github.com/muka/go-bluetooth/bluez/profile/device"
|
|
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
|
"github.com/rs/zerolog"
|
|
"go.arsenm.dev/infinitime/blefs"
|
|
)
|
|
|
|
// This global is used to store the logger.
|
|
// log.Logger is not used as it would interfere
|
|
// with the package importing the library
|
|
var log zerolog.Logger
|
|
|
|
const BTName = "InfiniTime"
|
|
|
|
const (
|
|
NewAlertChar = "00002a46-0000-1000-8000-00805f9b34fb"
|
|
NotifEventChar = "00020001-78fc-48fe-8e23-433b3a1942d0"
|
|
StepCountChar = "00030001-78fc-48fe-8e23-433b3a1942d0"
|
|
MotionValChar = "00030002-78fc-48fe-8e23-433b3a1942d0"
|
|
FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb"
|
|
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
|
|
LocalTimeChar = "00002a0f-0000-1000-8000-00805f9b34fb"
|
|
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
|
|
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
|
|
FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572"
|
|
FSVersionChar = "adaf0100-4669-6c65-5472-616e73666572"
|
|
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",
|
|
LocalTimeChar: "Local Time",
|
|
BatteryLvlChar: "Battery Level",
|
|
HeartRateChar: "Heart Rate",
|
|
FSTransferChar: "Filesystem Transfer",
|
|
FSVersionChar: "Filesystem Version",
|
|
WeatherDataChar: "Weather Data",
|
|
NavFlagsChar: "Navigation Icon",
|
|
NavNarrativeChar: "Navigation Instruction",
|
|
NavManDistChar: "Navigation Distance to next event",
|
|
NavProgressChar: "Navigation Progress",
|
|
}
|
|
|
|
type Device struct {
|
|
device *device.Device1
|
|
newAlertChar *gatt.GattCharacteristic1
|
|
notifEventChar *gatt.GattCharacteristic1
|
|
stepCountChar *gatt.GattCharacteristic1
|
|
motionValChar *gatt.GattCharacteristic1
|
|
fwVersionChar *gatt.GattCharacteristic1
|
|
currentTimeChar *gatt.GattCharacteristic1
|
|
localTimeChar *gatt.GattCharacteristic1
|
|
battLevelChar *gatt.GattCharacteristic1
|
|
heartRateChar *gatt.GattCharacteristic1
|
|
fsVersionChar *gatt.GattCharacteristic1
|
|
fsTransferChar *gatt.GattCharacteristic1
|
|
weatherDataChar *gatt.GattCharacteristic1
|
|
weatherdataChar *gatt.GattCharacteristic1
|
|
notifEventCh chan uint8
|
|
notifEventDone bool
|
|
Music MusicCtrl
|
|
Navigation NavigationService
|
|
DFU DFU
|
|
}
|
|
|
|
var (
|
|
ErrNoDevices = errors.New("no InfiniTime devices found")
|
|
ErrNotFound = errors.New("could not find any advertising InfiniTime devices")
|
|
ErrNotConnected = errors.New("not connected")
|
|
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
|
|
Whitelist []string
|
|
OnReqPasskey func() (uint32, error)
|
|
OnReconnect func()
|
|
Logger zerolog.Logger
|
|
LogLevel zerolog.Level
|
|
}
|
|
|
|
var DefaultOptions = &Options{
|
|
AttemptReconnect: true,
|
|
WhitelistEnabled: false,
|
|
Logger: zerolog.Nop(),
|
|
LogLevel: zerolog.Disabled,
|
|
}
|
|
|
|
// Connect will attempt to connect to a
|
|
// paired InfiniTime device. If none are paired,
|
|
// it will attempt to discover and pair one.
|
|
//
|
|
// It will also attempt to reconnect to the device
|
|
// if it disconnects and that is enabled in the options.
|
|
func Connect(ctx context.Context, opts *Options) (*Device, error) {
|
|
if opts == nil {
|
|
opts = DefaultOptions
|
|
}
|
|
|
|
log = opts.Logger.Level(opts.LogLevel)
|
|
|
|
// Set passkey request callback
|
|
setOnPasskeyReq(opts.OnReqPasskey)
|
|
|
|
// Connect to bluetooth device
|
|
btDev, err := connect(ctx, opts, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create new device
|
|
out := &Device{device: btDev}
|
|
out.Navigation = NavigationService{dev: out}
|
|
|
|
// Resolve characteristics
|
|
err = out.resolveChars()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// connect connects to the InfiniTime bluez device
|
|
func connect(ctx context.Context, opts *Options, first bool) (dev *device.Device1, err error) {
|
|
// Get devices
|
|
devs, err := defaultAdapter.GetDevices()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For every device
|
|
for _, listDev := range devs {
|
|
// If device name does not match, skip
|
|
if listDev.Properties.Name != BTName {
|
|
continue
|
|
}
|
|
// If whitelist enabled and doesn't contain
|
|
// device, skip
|
|
if opts.WhitelistEnabled &&
|
|
!contains(opts.Whitelist, listDev.Properties.Address) {
|
|
log.Debug().
|
|
Str("mac", listDev.Properties.Address).
|
|
Msg("InfiniTime device skipped as it is not in whitelist")
|
|
continue
|
|
}
|
|
|
|
// Set device
|
|
dev = listDev
|
|
|
|
log.Debug().
|
|
Str("mac", dev.Properties.Address).
|
|
Msg("InfiniTime device found in list")
|
|
|
|
break
|
|
}
|
|
|
|
// If device not set
|
|
if dev == nil {
|
|
log.Debug().Msg("No device found in list, attempting to discover")
|
|
// Discover devices on adapter
|
|
discoverCh, cancel, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
|
|
log.Debug().
|
|
Str("mac", dev.Properties.Address).
|
|
Msg("InfiniTime device discovered")
|
|
break discoverLoop
|
|
case <-ctx.Done():
|
|
break discoverLoop
|
|
|
|
}
|
|
}
|
|
|
|
// Cancel discovery
|
|
cancel()
|
|
}
|
|
|
|
// If device is still not set, return error
|
|
if dev == nil {
|
|
return nil, ErrNoDevices
|
|
}
|
|
|
|
// Create variable to track if reconnect
|
|
// was required
|
|
reconnRequired := false
|
|
// If device is not connected
|
|
if !dev.Properties.Connected {
|
|
log.Debug().Msg("Device not connected, connecting")
|
|
// Connect to device
|
|
err = dev.Connect()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Set reconnect required to true
|
|
reconnRequired = true
|
|
}
|
|
|
|
// If device is not paired
|
|
if !dev.Properties.Paired {
|
|
log.Debug().Msg("Device not paired, pairing")
|
|
// Pair device
|
|
err = dev.Pair()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If this is the first connection and reconnect
|
|
// is enabled, start reconnect goroutine
|
|
if first && opts.AttemptReconnect {
|
|
go reconnect(ctx, opts, dev)
|
|
}
|
|
|
|
// If this is not the first connection, a reonnect
|
|
// was required, and the OnReconnect callback exists,
|
|
// run it
|
|
if !first && reconnRequired && opts.OnReconnect != nil {
|
|
log.Debug().Msg("Reconnected to device, running OnReconnect callback")
|
|
opts.OnReconnect()
|
|
}
|
|
|
|
return dev, nil
|
|
}
|
|
|
|
// reconnect reconnects to a device if it disconnects
|
|
func reconnect(ctx context.Context, opts *Options, dev *device.Device1) {
|
|
// Watch device properties
|
|
propCh := watchProps(dev)
|
|
|
|
// Create variables to store time of last disconnect
|
|
// and amount of diconnects
|
|
lastDisconnect := time.Unix(0, 0)
|
|
amtDisconnects := 0
|
|
|
|
for event := range propCh {
|
|
// If event name is not Connected and value is not false, skip
|
|
if event.Name != "Connected" && event.Value != false {
|
|
continue
|
|
}
|
|
|
|
// Store seconds since last disconnect
|
|
secsSince := time.Since(lastDisconnect).Seconds()
|
|
// If over 3 seconds have passed, reset disconnect count
|
|
if secsSince > 3 {
|
|
amtDisconnects = 0
|
|
}
|
|
|
|
// If less than 3 seconds have passed and more than 6
|
|
// disconnects have occurred, remove the device and reset
|
|
if secsSince <= 3 && amtDisconnects >= 6 {
|
|
opts.Logger.Warn().Msg("At least 6 disconnects have occurred in the last three seconds. If this continues, try removing the InfiniTime device from bluetooth.")
|
|
lastDisconnect = time.Unix(0, 0)
|
|
amtDisconnects = 0
|
|
}
|
|
|
|
// Set disconnect variables
|
|
lastDisconnect = time.Now()
|
|
amtDisconnects++
|
|
|
|
for i := 0; i < 6; i++ {
|
|
// If three tries failed, remove device
|
|
if i == 3 {
|
|
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(ctx, opts, false)
|
|
if err != nil {
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
// Replace device with new device
|
|
*dev = *newDev
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// bufferChannel writes all events on propCh to a new, buffered channel
|
|
func bufferChannel(propCh chan *bluez.PropertyChanged) <-chan *bluez.PropertyChanged {
|
|
out := make(chan *bluez.PropertyChanged, 10)
|
|
go func() {
|
|
for event := range propCh {
|
|
out <- event
|
|
}
|
|
}()
|
|
return out
|
|
}
|
|
|
|
// watchProps returns a buffered channel for the device properties
|
|
func watchProps(dev *device.Device1) <-chan *bluez.PropertyChanged {
|
|
uPropCh, err := dev.WatchProperties()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return bufferChannel(uPropCh)
|
|
}
|
|
|
|
// setOnPasskeyReq sets the callback for a passkey request.
|
|
// It ensures the function will never be nil.
|
|
func setOnPasskeyReq(onReqPasskey func() (uint32, error)) {
|
|
itdAgent.ReqPasskey = onReqPasskey
|
|
if itdAgent.ReqPasskey == nil {
|
|
itdAgent.ReqPasskey = func() (uint32, error) {
|
|
return 0, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// contains checks if s is contained within ss
|
|
func contains(ss []string, s string) bool {
|
|
for _, str := range ss {
|
|
if strings.EqualFold(str, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// resolveChars attempts to set all required
|
|
// characteristics in an InfiniTime struct
|
|
func (i *Device) resolveChars() error {
|
|
// Get device characteristics
|
|
chars, err := i.device.GetCharacteristics()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// While no characteristics found
|
|
for len(chars) == 0 {
|
|
// Sleep one second
|
|
time.Sleep(time.Second)
|
|
// Attempt to retry getting characteristics
|
|
chars, err = i.device.GetCharacteristics()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// For every discovered characteristics
|
|
for _, char := range chars {
|
|
charResolved := true
|
|
// Set correct characteristics
|
|
switch char.Properties.UUID {
|
|
case NavFlagsChar:
|
|
i.Navigation.flagsChar = char
|
|
case NavNarrativeChar:
|
|
i.Navigation.narrativeChar = char
|
|
case NavManDistChar:
|
|
i.Navigation.mandistChar = char
|
|
case NavProgressChar:
|
|
i.Navigation.progressChar = char
|
|
case NewAlertChar:
|
|
i.newAlertChar = char
|
|
case NotifEventChar:
|
|
i.notifEventChar = char
|
|
case StepCountChar:
|
|
i.stepCountChar = char
|
|
case MotionValChar:
|
|
i.motionValChar = char
|
|
case FirmwareVerChar:
|
|
i.fwVersionChar = char
|
|
case CurrentTimeChar:
|
|
i.currentTimeChar = char
|
|
case LocalTimeChar:
|
|
i.localTimeChar = char
|
|
case BatteryLvlChar:
|
|
i.battLevelChar = char
|
|
case HeartRateChar:
|
|
i.heartRateChar = char
|
|
case MusicEventChar:
|
|
i.Music.eventChar = char
|
|
case MusicStatusChar:
|
|
i.Music.statusChar = char
|
|
case MusicArtistChar:
|
|
i.Music.artistChar = char
|
|
case MusicTrackChar:
|
|
i.Music.trackChar = char
|
|
case MusicAlbumChar:
|
|
i.Music.albumChar = char
|
|
case DFUCtrlPointChar:
|
|
i.DFU.ctrlPointChar = char
|
|
case DFUPacketChar:
|
|
i.DFU.packetChar = char
|
|
case FSTransferChar:
|
|
i.fsTransferChar = char
|
|
case FSVersionChar:
|
|
i.fsVersionChar = char
|
|
case WeatherDataChar:
|
|
i.weatherDataChar = char
|
|
default:
|
|
charResolved = false
|
|
}
|
|
if charResolved {
|
|
log.Debug().
|
|
Str("uuid", char.Properties.UUID).
|
|
Str("name", charNames[char.Properties.UUID]).
|
|
Msg("Resolved characteristic")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Address returns the InfiniTime's bluetooth address
|
|
func (i *Device) Address() string {
|
|
return i.device.Properties.Address
|
|
}
|
|
|
|
// Version returns InfiniTime's reported firmware version string
|
|
func (i *Device) Version() (string, error) {
|
|
if err := i.checkStatus(i.fwVersionChar, FirmwareVerChar); err != nil {
|
|
return "", err
|
|
}
|
|
ver, err := i.fwVersionChar.ReadValue(nil)
|
|
return string(ver), err
|
|
}
|
|
|
|
// BatteryLevel gets the watch's battery level via the Battery Service
|
|
func (i *Device) BatteryLevel() (uint8, error) {
|
|
if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil {
|
|
return 0, err
|
|
}
|
|
battLevel, err := i.battLevelChar.ReadValue(nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint8(battLevel[0]), nil
|
|
}
|
|
|
|
func (i *Device) StepCount() (uint32, error) {
|
|
if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil {
|
|
return 0, err
|
|
}
|
|
stepCountData, err := i.stepCountChar.ReadValue(nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return binary.LittleEndian.Uint32(stepCountData), nil
|
|
}
|
|
|
|
type MotionValues struct {
|
|
X int16
|
|
Y int16
|
|
Z int16
|
|
}
|
|
|
|
func (i *Device) Motion() (MotionValues, error) {
|
|
out := MotionValues{}
|
|
if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil {
|
|
return out, err
|
|
}
|
|
motionVals, err := i.motionValChar.ReadValue(nil)
|
|
if err != nil {
|
|
return out, nil
|
|
}
|
|
motionValReader := bytes.NewReader(motionVals)
|
|
err = binary.Read(motionValReader, binary.LittleEndian, &out)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (i *Device) HeartRate() (uint8, error) {
|
|
if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil {
|
|
return 0, err
|
|
}
|
|
heartRate, err := i.heartRateChar.ReadValue(nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return uint8(heartRate[1]), nil
|
|
}
|
|
|
|
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, err
|
|
}
|
|
// Watch characteristics of heart rate characteristic
|
|
ch, err := i.heartRateChar.WatchProperties()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(chan uint8, 2)
|
|
currentHeartRate, err := i.HeartRate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out <- currentHeartRate
|
|
go func() {
|
|
// For every event
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
|
|
close(out)
|
|
i.heartRateChar.StopNotify()
|
|
return
|
|
case event := <-ch:
|
|
// If value changed
|
|
if event.Name == "Value" {
|
|
// Send heart rate to channel
|
|
out <- uint8(event.Value.([]byte)[1])
|
|
} else if event.Name == "Notifying" && !event.Value.(bool) {
|
|
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
|
|
i.heartRateChar.StartNotify()
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return out, nil
|
|
}
|
|
|
|
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, err
|
|
}
|
|
// Watch characteristics of heart rate characteristic
|
|
ch, err := i.battLevelChar.WatchProperties()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(chan uint8, 2)
|
|
currentBattLevel, err := i.BatteryLevel()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out <- currentBattLevel
|
|
go func() {
|
|
// For every event
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
|
|
close(out)
|
|
i.battLevelChar.StopNotify()
|
|
return
|
|
case event := <-ch:
|
|
// If value changed
|
|
if event.Name == "Value" {
|
|
// Send heart rate to channel
|
|
out <- uint8(event.Value.([]byte)[0])
|
|
} else if event.Name == "Notifying" && !event.Value.(bool) {
|
|
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
|
|
i.battLevelChar.StartNotify()
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return out, nil
|
|
}
|
|
|
|
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, err
|
|
}
|
|
// Watch properties of step count characteristic
|
|
ch, err := i.stepCountChar.WatchProperties()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(chan uint32, 2)
|
|
currentStepCount, err := i.StepCount()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out <- currentStepCount
|
|
go func() {
|
|
// For every event
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
|
|
close(out)
|
|
i.stepCountChar.StopNotify()
|
|
return
|
|
case event := <-ch:
|
|
// If value changed
|
|
if event.Name == "Value" {
|
|
// Send step count to channel
|
|
out <- binary.LittleEndian.Uint32(event.Value.([]byte))
|
|
} else if event.Name == "Notifying" && !event.Value.(bool) {
|
|
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
|
|
i.stepCountChar.StartNotify()
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return out, nil
|
|
}
|
|
|
|
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, err
|
|
}
|
|
// Watch properties of motion characteristic
|
|
ch, err := i.motionValChar.WatchProperties()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(chan MotionValues, 2)
|
|
motionVals, err := i.Motion()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out <- motionVals
|
|
go func() {
|
|
// For every event
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
|
|
close(out)
|
|
i.motionValChar.StopNotify()
|
|
return
|
|
case event := <-ch:
|
|
// If value changed
|
|
if event.Name == "Value" {
|
|
// Read binary into MotionValues struct
|
|
binary.Read(bytes.NewReader(event.Value.([]byte)), binary.LittleEndian, &motionVals)
|
|
// Send step count to channel
|
|
out <- motionVals
|
|
} else if event.Name == "Notifying" && !event.Value.(bool) {
|
|
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
|
|
i.motionValChar.StartNotify()
|
|
}
|
|
}
|
|
|
|
}
|
|
}()
|
|
return out, nil
|
|
}
|
|
|
|
// SetTime sets the watch's
|
|
// * time using the Current Time Service's current time characteristic
|
|
// * timezone information using the CTS's local time characteristic
|
|
func (i *Device) SetTime(t time.Time) error {
|
|
if err := i.checkStatus(i.currentTimeChar, CurrentTimeChar); err != nil {
|
|
return err
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
binary.Write(buf, binary.LittleEndian, uint16(t.Year()))
|
|
binary.Write(buf, binary.LittleEndian, uint8(t.Month()))
|
|
binary.Write(buf, binary.LittleEndian, uint8(t.Day()))
|
|
binary.Write(buf, binary.LittleEndian, uint8(t.Hour()))
|
|
binary.Write(buf, binary.LittleEndian, uint8(t.Minute()))
|
|
binary.Write(buf, binary.LittleEndian, uint8(t.Second()))
|
|
binary.Write(buf, binary.LittleEndian, uint8(t.Weekday()))
|
|
binary.Write(buf, binary.LittleEndian, uint8((t.Nanosecond()/1000)/1e6*256))
|
|
binary.Write(buf, binary.LittleEndian, uint8(0b0001))
|
|
if err := i.currentTimeChar.WriteValue(buf.Bytes(), nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := i.checkStatus(i.localTimeChar, LocalTimeChar); err != nil {
|
|
// If the characteristic is unavailable,
|
|
// fail silently, as many people may be on
|
|
// older InfiniTime versions. A warning
|
|
// may be added later.
|
|
if _, ok := err.(ErrCharNotAvail); ok {
|
|
return nil
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
_, offset := t.Zone()
|
|
dst := 0
|
|
|
|
// Local time expects two values: the timezone offset and the dst offset, both
|
|
// expressed in quarters of an hour.
|
|
// Timezone offset is to be constant over DST, with dst offset holding the offset != 0
|
|
// when DST is in effect.
|
|
// As there is no standard way in go to get the actual dst offset, we assume it to be 1h
|
|
// when DST is in effect
|
|
if t.IsDST() {
|
|
dst = 3600
|
|
offset -= 3600
|
|
}
|
|
bufTz := &bytes.Buffer{}
|
|
binary.Write(bufTz, binary.LittleEndian, uint8(offset / 3600 * 4))
|
|
binary.Write(bufTz, binary.LittleEndian, uint8(dst / 3600 * 4))
|
|
return i.localTimeChar.WriteValue(bufTz.Bytes(), nil)
|
|
}
|
|
|
|
// 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, NewAlertChar); err != nil {
|
|
return err
|
|
}
|
|
return i.newAlertChar.WriteValue(
|
|
append([]byte{0x00, 0x01, 0x00}, []byte(title+"\x00"+body)...),
|
|
nil,
|
|
)
|
|
}
|
|
|
|
// These constants represent the possible call statuses selected by the user
|
|
const (
|
|
CallStatusDeclined uint8 = iota
|
|
CallStatusAccepted
|
|
CallStatusMuted
|
|
)
|
|
|
|
// 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, NewAlertChar); err != nil {
|
|
return nil, err
|
|
}
|
|
// Write call notification to new alert characteristic
|
|
err := i.newAlertChar.WriteValue(
|
|
append([]byte{0x03, 0x01, 0x00}, []byte(from)...),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !i.notifEventDone {
|
|
err = i.initNotifEvent()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.notifEventDone = true
|
|
}
|
|
|
|
return i.notifEventCh, nil
|
|
}
|
|
|
|
// initNotifEvent initializes the notification event channel
|
|
func (i *Device) initNotifEvent() error {
|
|
// Start notifications on notification event characteristic
|
|
err := i.notifEventChar.StartNotify()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Watch properties of notification event characteristic
|
|
ch, err := i.notifEventChar.WatchProperties()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Create new output channel for status
|
|
i.notifEventCh = make(chan uint8, 1)
|
|
go func() {
|
|
// For every event
|
|
for event := range ch {
|
|
// If value changed
|
|
if event.Name == "Value" {
|
|
// Send status to channel
|
|
i.notifEventCh <- uint8(event.Value.([]byte)[0])
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// FS creates and returns a new filesystem from the device
|
|
func (i *Device) FS() (*blefs.FS, error) {
|
|
if err := i.checkStatus(i.fsTransferChar, FSTransferChar); err != nil {
|
|
return nil, err
|
|
}
|
|
return blefs.New(i.fsTransferChar)
|
|
}
|
|
|
|
// AddWeatherEvent adds one of the event structs from
|
|
// 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, WeatherDataChar); err != nil {
|
|
return err
|
|
}
|
|
// Get type of input
|
|
inputType := reflect.TypeOf(event)
|
|
// Check if input contains TimelineHeader
|
|
_, hdrExists := inputType.FieldByName("TimelineHeader")
|
|
// If header does not exist or input is not struct
|
|
if !hdrExists || inputType.Kind() != reflect.Struct {
|
|
return ErrNoTimelineHeader
|
|
}
|
|
|
|
// Encode event as CBOR
|
|
data, err := cbor.Marshal(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug().Interface("event", event).Msg("Adding weather event")
|
|
// Write data to weather data characteristic
|
|
return i.weatherDataChar.WriteValue(data, nil)
|
|
}
|
|
|
|
func (i *Device) checkStatus(char *gatt.GattCharacteristic1, uuid string) error {
|
|
log.Debug().Msg("Checking characteristic status")
|
|
connected, err := i.device.GetConnected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !connected {
|
|
return ErrNotConnected
|
|
}
|
|
if char == nil {
|
|
log.Debug().Msg("Characteristic not available (nil)")
|
|
return ErrCharNotAvail{uuid}
|
|
}
|
|
log.Debug().
|
|
Str("uuid", char.Properties.UUID).
|
|
Str("name", charNames[char.Properties.UUID]).
|
|
Msg("Characteristic available")
|
|
return nil
|
|
}
|