This repository has been archived on 2024-04-14. You can view files and clone it, but cannot push or open issues or pull requests.
infinitime/infinitime.go

893 lines
24 KiB
Go
Raw Normal View History

2021-08-20 00:41:09 +00:00
package infinitime
import (
"bytes"
"context"
2021-08-20 00:41:09 +00:00
"encoding/binary"
"errors"
2021-12-12 20:43:43 +00:00
"reflect"
"strings"
2021-08-20 00:41:09 +00:00
"time"
2021-12-12 20:43:43 +00:00
"github.com/fxamacker/cbor/v2"
2021-08-20 00:41:09 +00:00
bt "github.com/muka/go-bluetooth/api"
2022-02-21 10:46:20 +00:00
"github.com/muka/go-bluetooth/bluez"
2021-08-20 00:41:09 +00:00
"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"
2021-08-20 00:41:09 +00:00
)
2022-04-24 02:58:00 +00:00
// 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
2021-08-20 00:41:09 +00:00
const BTName = "InfiniTime"
const (
NewAlertChar = "00002a46-0000-1000-8000-00805f9b34fb"
2021-10-15 07:23:54 +00:00
NotifEventChar = "00020001-78fc-48fe-8e23-433b3a1942d0"
2021-10-22 19:59:51 +00:00
StepCountChar = "00030001-78fc-48fe-8e23-433b3a1942d0"
MotionValChar = "00030002-78fc-48fe-8e23-433b3a1942d0"
2021-08-20 00:41:09 +00:00
FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb"
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
LocalTimeChar = "00002a0f-0000-1000-8000-00805f9b34fb"
2021-08-20 00:41:09 +00:00
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572"
FSVersionChar = "adaf0100-4669-6c65-5472-616e73666572"
2021-12-12 20:43:43 +00:00
WeatherDataChar = "00040001-78fc-48fe-8e23-433b3a1942d0"
2021-08-20 00:41:09 +00:00
)
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",
}
2021-08-20 00:41:09 +00:00
type Device struct {
device *device.Device1
newAlertChar *gatt.GattCharacteristic1
2021-10-15 07:23:54 +00:00
notifEventChar *gatt.GattCharacteristic1
2021-10-22 19:59:51 +00:00
stepCountChar *gatt.GattCharacteristic1
motionValChar *gatt.GattCharacteristic1
2021-08-20 00:41:09 +00:00
fwVersionChar *gatt.GattCharacteristic1
currentTimeChar *gatt.GattCharacteristic1
localTimeChar *gatt.GattCharacteristic1
2021-08-20 00:41:09 +00:00
battLevelChar *gatt.GattCharacteristic1
heartRateChar *gatt.GattCharacteristic1
fsVersionChar *gatt.GattCharacteristic1
fsTransferChar *gatt.GattCharacteristic1
2021-12-12 20:43:43 +00:00
weatherDataChar *gatt.GattCharacteristic1
Added Navigation service (#5) InfiniTime implements a [Navigation Service](https://github.com/InfiniTimeOrg/InfiniTime/blob/develop/doc/NavigationService.md). This pull request will add it to the go library by defining a function ```go func (i *Device) Navigation(flag string, narrative string, dist string, progress uint8) error { ... } ``` From the InfiniTime manual * `flag`: the graphic instruction as provided by [Pure Maps](https://github.com/rinigus/pure-maps/tree/master/qml/icons/navigation). A list of valid instruction icons can be found [here](https://github.com/rinigus/pure-maps/tree/master/qml/icons/navigation) * `narrative`: the instruction in words, eg. "At the roundabout take the first exit". * `dist`: a short string describing the distance to the upcoming instruction such as "50 m". * `progress`: the percent complete in a `uint8` Adding this to the `itd` daemon is straightforward ```patch diff --git a/api/types.go b/api/types.go index 281a85b..14c84de 100644 --- a/api/types.go +++ b/api/types.go @@ -22,6 +22,13 @@ type FwUpgradeData struct { Files []string } +type NavigationData struct { + Flag string + Narrative string + Dist string + Progress uint8 +} + type NotifyData struct { Title string Body string diff --git a/socket.go b/socket.go index 6fcba5c..91b37c0 100644 --- a/socket.go +++ b/socket.go @@ -204,6 +204,10 @@ func (i *ITD) Address(_ *server.Context) string { return i.dev.Address() } +func (i *ITD) Navigation(_ *server.Context, data api.NavigationData) error { + return i.dev.Navigation(data.Flag, data.Narrative, data.Dist, data.Progress) +} + func (i *ITD) Notify(_ *server.Context, data api.NotifyData) error { return i.dev.Notify(data.Title, data.Body) } ``` Co-authored-by: Yannick Ulrich <yannick.ulrich@durham.ac.uk> Reviewed-on: https://gitea.arsenm.dev/Arsen6331/infinitime/pulls/5 Co-authored-by: yannickulrich <yannick.ulrich@protonmail.com> Co-committed-by: yannickulrich <yannick.ulrich@protonmail.com>
2022-11-03 19:09:06 +00:00
weatherdataChar *gatt.GattCharacteristic1
notifEventCh chan uint8
notifEventDone bool
2021-08-20 00:41:09 +00:00
Music MusicCtrl
Navigation NavigationService
2021-08-20 00:41:09 +00:00
DFU DFU
}
var (
2021-12-12 20:43:43 +00:00
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")
2021-12-17 05:30:29 +00:00
ErrPairTimeout = errors.New("reached timeout while pairing")
)
2021-08-20 00:41:09 +00:00
type ErrCharNotAvail struct {
uuid string
}
func (e ErrCharNotAvail) Error() string {
return "characteristic " + e.uuid + " (" + charNames[e.uuid] + ") not available"
}
2021-08-20 00:41:09 +00:00
type Options struct {
AttemptReconnect bool
2021-10-15 07:23:54 +00:00
WhitelistEnabled bool
Whitelist []string
2021-12-17 05:30:29 +00:00
OnReqPasskey func() (uint32, error)
2022-02-21 10:46:20 +00:00
OnReconnect func()
Logger zerolog.Logger
LogLevel zerolog.Level
2021-08-20 00:41:09 +00:00
}
var DefaultOptions = &Options{
AttemptReconnect: true,
2021-10-15 07:23:54 +00:00
WhitelistEnabled: false,
Logger: zerolog.Nop(),
LogLevel: zerolog.Disabled,
2021-08-20 00:41:09 +00:00
}
// 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
2022-02-21 10:46:20 +00:00
// if it disconnects and that is enabled in the options.
func Connect(ctx context.Context, opts *Options) (*Device, error) {
2021-08-20 00:41:09 +00:00
if opts == nil {
opts = DefaultOptions
}
2022-02-21 10:46:20 +00:00
2022-04-24 02:58:00 +00:00
log = opts.Logger.Level(opts.LogLevel)
2022-04-24 02:07:41 +00:00
2022-02-21 10:46:20 +00:00
// Set passkey request callback
2021-12-17 05:30:29 +00:00
setOnPasskeyReq(opts.OnReqPasskey)
2022-02-21 10:46:20 +00:00
// Connect to bluetooth device
btDev, err := connect(ctx, opts, true)
2021-08-20 00:41:09 +00:00
if err != nil {
return nil, err
}
2022-02-21 10:46:20 +00:00
// Create new device
out := &Device{device: btDev}
2022-11-07 04:08:13 +00:00
out.Navigation = NavigationService{dev: out}
2022-02-21 10:46:20 +00:00
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
return out, nil
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
// connect connects to the InfiniTime bluez device
func connect(ctx context.Context, opts *Options, first bool) (dev *device.Device1, err error) {
2022-02-21 10:46:20 +00:00
// Get devices
2021-08-20 00:41:09 +00:00
devs, err := defaultAdapter.GetDevices()
if err != nil {
return nil, err
}
2022-02-21 10:46:20 +00:00
2021-08-20 00:41:09 +00:00
// For every device
2022-02-21 10:46:20 +00:00
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) {
2022-04-24 02:58:00 +00:00
log.Debug().
Str("mac", listDev.Properties.Address).
Msg("InfiniTime device skipped as it is not in whitelist")
2022-02-21 10:46:20 +00:00
continue
}
// Set device
dev = listDev
2022-04-24 02:58:00 +00:00
log.Debug().
Str("mac", dev.Properties.Address).
Msg("InfiniTime device found in list")
2022-02-21 10:46:20 +00:00
break
}
// If device not set
if dev == nil {
2022-04-24 02:58:00 +00:00
log.Debug().Msg("No device found in list, attempting to discover")
2022-02-21 10:46:20 +00:00
// 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
}
2022-02-21 10:46:20 +00:00
// Create new device from event path
discovered, err := device.NewDevice1(event.Path)
if err != nil {
return nil, err
}
2022-02-21 10:46:20 +00:00
// 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
}
2022-02-21 10:46:20 +00:00
// Set device
dev = discovered
2022-04-24 02:58:00 +00:00
log.Debug().
Str("mac", dev.Properties.Address).
Msg("InfiniTime device discovered")
break discoverLoop
case <-ctx.Done():
break discoverLoop
}
2021-08-20 00:41:09 +00:00
}
// Cancel discovery
2022-02-21 10:46:20 +00:00
cancel()
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
// If device is still not set, return error
if dev == nil {
2021-08-20 00:41:09 +00:00
return nil, ErrNoDevices
}
2022-02-21 10:46:20 +00:00
// Create variable to track if reconnect
// was required
reconnRequired := false
// If device is not connected
if !dev.Properties.Connected {
2022-04-24 02:58:00 +00:00
log.Debug().Msg("Device not connected, connecting")
2022-02-21 10:46:20 +00:00
// Connect to device
err = dev.Connect()
if err != nil {
return nil, err
}
// Set reconnect required to true
reconnRequired = true
2021-08-22 03:25:09 +00:00
}
2022-02-21 10:46:20 +00:00
// If device is not paired
if !dev.Properties.Paired {
2022-04-24 02:58:00 +00:00
log.Debug().Msg("Device not paired, pairing")
2022-02-21 10:46:20 +00:00
// Pair device
err = dev.Pair()
if err != nil {
return nil, err
}
}
2022-02-21 10:46:20 +00:00
// If this is the first connection and reconnect
// is enabled, start reconnect goroutine
if first && opts.AttemptReconnect {
go reconnect(ctx, opts, dev)
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
// If this is not the first connection, a reonnect
// was required, and the OnReconnect callback exists,
// run it
if !first && reconnRequired && opts.OnReconnect != nil {
2022-04-24 02:58:00 +00:00
log.Debug().Msg("Reconnected to device, running OnReconnect callback")
2022-02-21 10:46:20 +00:00
opts.OnReconnect()
2021-10-15 07:23:54 +00:00
}
2022-02-21 10:46:20 +00:00
return dev, nil
2021-10-15 07:23:54 +00:00
}
2022-02-21 10:46:20 +00:00
// reconnect reconnects to a device if it disconnects
func reconnect(ctx context.Context, opts *Options, dev *device.Device1) {
2022-02-21 10:46:20 +00:00
// 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 {
2021-08-22 20:12:16 +00:00
continue
}
2022-02-21 10:46:20 +00:00
// Store seconds since last disconnect
secsSince := time.Since(lastDisconnect).Seconds()
// If over 3 seconds have passed, reset disconnect count
if secsSince > 3 {
amtDisconnects = 0
2021-08-22 20:12:16 +00:00
}
2022-02-21 10:46:20 +00:00
// If less than 3 seconds have passed and more than 6
2022-02-21 10:46:20 +00:00
// 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.")
2022-02-21 10:46:20 +00:00
lastDisconnect = time.Unix(0, 0)
amtDisconnects = 0
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
// Set disconnect variables
lastDisconnect = time.Now()
amtDisconnects++
2021-08-20 00:41:09 +00:00
2022-02-21 10:46:20 +00:00
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.")
2022-02-21 10:46:20 +00:00
}
// Connect to device
newDev, err := connect(ctx, opts, false)
2022-02-21 10:46:20 +00:00
if err != nil {
time.Sleep(time.Second)
continue
}
// Replace device with new device
*dev = *newDev
2021-08-20 00:41:09 +00:00
2022-02-21 10:46:20 +00:00
break
}
2021-12-17 05:30:29 +00:00
}
2022-02-21 10:46:20 +00:00
}
2022-02-21 10:46:20 +00:00
// 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
}
2021-08-20 00:41:09 +00:00
2022-02-21 10:46:20 +00:00
// watchProps returns a buffered channel for the device properties
func watchProps(dev *device.Device1) <-chan *bluez.PropertyChanged {
uPropCh, err := dev.WatchProperties()
2021-08-20 00:41:09 +00:00
if err != nil {
2022-02-21 10:46:20 +00:00
panic(err)
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
return bufferChannel(uPropCh)
2021-08-20 00:41:09 +00:00
}
2021-12-17 05:30:29 +00:00
// 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
}
2021-08-20 00:41:09 +00:00
}
2021-12-17 05:30:29 +00:00
}
2021-08-20 00:41:09 +00:00
2022-02-21 10:46:20 +00:00
// 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
2021-12-17 05:30:29 +00:00
}
2021-08-20 00:41:09 +00:00
}
2022-02-21 10:46:20 +00:00
return false
2021-08-20 00:41:09 +00:00
}
// 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 {
2022-04-24 02:58:00 +00:00
charResolved := true
2021-08-20 00:41:09 +00:00
// Set correct characteristics
switch char.Properties.UUID {
Added Navigation service (#5) InfiniTime implements a [Navigation Service](https://github.com/InfiniTimeOrg/InfiniTime/blob/develop/doc/NavigationService.md). This pull request will add it to the go library by defining a function ```go func (i *Device) Navigation(flag string, narrative string, dist string, progress uint8) error { ... } ``` From the InfiniTime manual * `flag`: the graphic instruction as provided by [Pure Maps](https://github.com/rinigus/pure-maps/tree/master/qml/icons/navigation). A list of valid instruction icons can be found [here](https://github.com/rinigus/pure-maps/tree/master/qml/icons/navigation) * `narrative`: the instruction in words, eg. "At the roundabout take the first exit". * `dist`: a short string describing the distance to the upcoming instruction such as "50 m". * `progress`: the percent complete in a `uint8` Adding this to the `itd` daemon is straightforward ```patch diff --git a/api/types.go b/api/types.go index 281a85b..14c84de 100644 --- a/api/types.go +++ b/api/types.go @@ -22,6 +22,13 @@ type FwUpgradeData struct { Files []string } +type NavigationData struct { + Flag string + Narrative string + Dist string + Progress uint8 +} + type NotifyData struct { Title string Body string diff --git a/socket.go b/socket.go index 6fcba5c..91b37c0 100644 --- a/socket.go +++ b/socket.go @@ -204,6 +204,10 @@ func (i *ITD) Address(_ *server.Context) string { return i.dev.Address() } +func (i *ITD) Navigation(_ *server.Context, data api.NavigationData) error { + return i.dev.Navigation(data.Flag, data.Narrative, data.Dist, data.Progress) +} + func (i *ITD) Notify(_ *server.Context, data api.NotifyData) error { return i.dev.Notify(data.Title, data.Body) } ``` Co-authored-by: Yannick Ulrich <yannick.ulrich@durham.ac.uk> Reviewed-on: https://gitea.arsenm.dev/Arsen6331/infinitime/pulls/5 Co-authored-by: yannickulrich <yannick.ulrich@protonmail.com> Co-committed-by: yannickulrich <yannick.ulrich@protonmail.com>
2022-11-03 19:09:06 +00:00
case NavFlagsChar:
i.Navigation.flagsChar = char
case NavNarrativeChar:
i.Navigation.narrativeChar = char
case NavManDistChar:
i.Navigation.mandistChar = char
case NavProgressChar:
i.Navigation.progressChar = char
2021-08-20 00:41:09 +00:00
case NewAlertChar:
i.newAlertChar = char
2021-10-15 07:23:54 +00:00
case NotifEventChar:
i.notifEventChar = char
2021-10-22 19:59:51 +00:00
case StepCountChar:
i.stepCountChar = char
case MotionValChar:
i.motionValChar = char
2021-08-20 00:41:09 +00:00
case FirmwareVerChar:
i.fwVersionChar = char
case CurrentTimeChar:
i.currentTimeChar = char
case LocalTimeChar:
i.localTimeChar = char
2021-08-20 00:41:09 +00:00
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
2021-12-12 20:43:43 +00:00
case WeatherDataChar:
i.weatherDataChar = char
2022-04-24 02:58:00 +00:00
default:
charResolved = false
}
if charResolved {
log.Debug().
Str("uuid", char.Properties.UUID).
Str("name", charNames[char.Properties.UUID]).
2022-04-24 02:58:00 +00:00
Msg("Resolved characteristic")
2021-08-20 00:41:09 +00:00
}
}
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
2021-08-20 00:41:09 +00:00
}
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
2021-08-20 00:41:09 +00:00
}
battLevel, err := i.battLevelChar.ReadValue(nil)
if err != nil {
return 0, err
}
return uint8(battLevel[0]), nil
}
2021-10-22 19:59:51 +00:00
func (i *Device) StepCount() (uint32, error) {
if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil {
return 0, err
2021-10-22 19:59:51 +00:00
}
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
2021-10-22 19:59:51 +00:00
}
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
}
2021-08-20 00:41:09 +00:00
func (i *Device) HeartRate() (uint8, error) {
if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil {
return 0, err
2021-08-20 00:41:09 +00:00
}
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
2021-08-20 00:41:09 +00:00
}
// Start notifications on heart rate characteristic
err := i.heartRateChar.StartNotify()
if err != nil {
return nil, err
2021-08-20 00:41:09 +00:00
}
// Watch characteristics of heart rate characteristic
ch, err := i.heartRateChar.WatchProperties()
if err != nil {
return nil, err
2021-08-20 00:41:09 +00:00
}
out := make(chan uint8, 2)
currentHeartRate, err := i.HeartRate()
if err != nil {
return nil, err
}
out <- currentHeartRate
2021-08-20 00:41:09 +00:00
go func() {
// For every event
for {
select {
case <-ctx.Done():
2022-04-24 02:58:00 +00:00
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) {
2022-04-24 02:58:00 +00:00
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
i.heartRateChar.StartNotify()
}
2021-08-20 00:41:09 +00:00
}
}
}()
return out, nil
2021-08-20 00:41:09 +00:00
}
func (i *Device) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil {
return nil, err
2021-08-25 04:55:03 +00:00
}
// Start notifications on heart rate characteristic
err := i.battLevelChar.StartNotify()
if err != nil {
return nil, err
2021-08-25 04:55:03 +00:00
}
// Watch characteristics of heart rate characteristic
ch, err := i.battLevelChar.WatchProperties()
if err != nil {
return nil, err
2021-08-25 04:55:03 +00:00
}
out := make(chan uint8, 2)
currentBattLevel, err := i.BatteryLevel()
if err != nil {
return nil, err
}
out <- currentBattLevel
2021-08-25 04:55:03 +00:00
go func() {
// For every event
for {
select {
case <-ctx.Done():
2022-04-24 02:58:00 +00:00
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) {
2022-04-24 02:58:00 +00:00
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
i.battLevelChar.StartNotify()
}
2021-08-25 04:55:03 +00:00
}
}
}()
return out, nil
2021-08-25 04:55:03 +00:00
}
func (i *Device) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
// Start notifications on step count characteristic
err := i.stepCountChar.StartNotify()
if err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
// Watch properties of step count characteristic
ch, err := i.stepCountChar.WatchProperties()
if err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
out := make(chan uint32, 2)
currentStepCount, err := i.StepCount()
if err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
out <- currentStepCount
go func() {
// For every event
for {
2021-10-22 19:59:51 +00:00
select {
case <-ctx.Done():
2022-04-24 02:58:00 +00:00
log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
2021-10-22 19:59:51 +00:00
close(out)
i.stepCountChar.StopNotify()
return
case event := <-ch:
2021-10-22 19:59:51 +00:00
// 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) {
2022-04-24 02:58:00 +00:00
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
i.stepCountChar.StartNotify()
2021-10-22 19:59:51 +00:00
}
}
}
}()
return out, nil
2021-10-22 19:59:51 +00:00
}
func (i *Device) WatchMotion(ctx context.Context) (<-chan MotionValues, error) {
if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
// Start notifications on motion characteristic
err := i.motionValChar.StartNotify()
if err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
// Watch properties of motion characteristic
ch, err := i.motionValChar.WatchProperties()
if err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
out := make(chan MotionValues, 2)
motionVals, err := i.Motion()
if err != nil {
return nil, err
2021-10-22 19:59:51 +00:00
}
out <- motionVals
go func() {
// For every event
for {
2021-10-22 19:59:51 +00:00
select {
case <-ctx.Done():
2022-04-24 02:58:00 +00:00
log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
2021-10-22 19:59:51 +00:00
close(out)
i.motionValChar.StopNotify()
return
case event := <-ch:
2021-10-22 19:59:51 +00:00
// 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) {
2022-04-24 02:58:00 +00:00
log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
i.motionValChar.StartNotify()
2021-10-22 19:59:51 +00:00
}
}
}
}()
return out, nil
2021-10-22 19:59:51 +00:00
}
// SetTime sets the watch's
// * time using the Current Time Service's current time characteristic
// * timezone information using the CTS's local time characteristic
2021-08-20 00:41:09 +00:00
func (i *Device) SetTime(t time.Time) error {
if err := i.checkStatus(i.currentTimeChar, CurrentTimeChar); err != nil {
return err
2021-08-20 00:41:09 +00:00
}
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)
2021-08-20 00:41:09 +00:00
}
// 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
2021-08-20 00:41:09 +00:00
}
return i.newAlertChar.WriteValue(
2021-10-15 07:23:54 +00:00
append([]byte{0x00, 0x01, 0x00}, []byte(title+"\x00"+body)...),
2021-08-20 00:41:09 +00:00
nil,
)
}
2021-10-15 07:23:54 +00:00
// 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.
2021-10-15 07:23:54 +00:00
func (i *Device) NotifyCall(from string) (<-chan uint8, error) {
if err := i.checkStatus(i.newAlertChar, NewAlertChar); err != nil {
return nil, err
2021-10-15 07:23:54 +00:00
}
// 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 {
2021-10-15 07:23:54 +00:00
// Start notifications on notification event characteristic
err := i.notifEventChar.StartNotify()
2021-10-15 07:23:54 +00:00
if err != nil {
return err
2021-10-15 07:23:54 +00:00
}
// Watch properties of notification event characteristic
ch, err := i.notifEventChar.WatchProperties()
if err != nil {
return err
2021-10-15 07:23:54 +00:00
}
// Create new output channel for status
i.notifEventCh = make(chan uint8, 1)
2021-10-15 07:23:54 +00:00
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])
2021-10-15 07:23:54 +00:00
}
}
}()
return nil
2021-10-15 07:23:54 +00:00
}
// 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)
}
2021-12-12 20:43:43 +00:00
// 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 {
2021-12-12 20:43:43 +00:00
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
}
2022-04-24 02:58:00 +00:00
log.Debug().Interface("event", event).Msg("Adding weather event")
2021-12-12 20:43:43 +00:00
// Write data to weather data characteristic
return i.weatherDataChar.WriteValue(data, nil)
}
func (i *Device) checkStatus(char *gatt.GattCharacteristic1, uuid string) error {
2022-04-24 02:58:00 +00:00
log.Debug().Msg("Checking characteristic status")
2021-12-17 05:30:29 +00:00
connected, err := i.device.GetConnected()
if err != nil {
return err
}
if !connected {
return ErrNotConnected
}
if char == nil {
2022-04-24 02:58:00 +00:00
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
}