infinitime/infinitime.go

730 lines
18 KiB
Go

package infinitime
import (
"bytes"
"encoding/binary"
"errors"
"reflect"
"strings"
"time"
"github.com/fxamacker/cbor/v2"
bt "github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez/profile/adapter"
"github.com/muka/go-bluetooth/bluez/profile/device"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
"go.arsenm.dev/infinitime/blefs"
)
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"
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"
)
type Device struct {
opts *Options
device *device.Device1
newAlertChar *gatt.GattCharacteristic1
notifEventChar *gatt.GattCharacteristic1
stepCountChar *gatt.GattCharacteristic1
motionValChar *gatt.GattCharacteristic1
fwVersionChar *gatt.GattCharacteristic1
currentTimeChar *gatt.GattCharacteristic1
battLevelChar *gatt.GattCharacteristic1
heartRateChar *gatt.GattCharacteristic1
fsVersionChar *gatt.GattCharacteristic1
fsTransferChar *gatt.GattCharacteristic1
weatherDataChar *gatt.GattCharacteristic1
notifEventCh chan uint8
notifEventDone bool
onReconnect func()
Music MusicCtrl
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")
ErrCharNotAvail = errors.New("required characteristic is not available")
ErrNoTimelineHeader = errors.New("events must contain the timeline header")
)
type Options struct {
AttemptReconnect bool
WhitelistEnabled bool
Whitelist []string
}
var DefaultOptions = &Options{
AttemptReconnect: true,
WhitelistEnabled: false,
}
// 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.
func Connect(opts *Options) (*Device, error) {
if opts == nil {
opts = DefaultOptions
}
// Attempt to connect to paired device by name
dev, err := connectByName(opts)
// If such device does not exist
if errors.Is(err, ErrNoDevices) {
// Attempt to pair device
dev, err = pair(opts)
}
if err != nil {
return nil, err
}
dev.opts = opts
dev.onReconnect = func() {}
// Watch device properties
devEvtCh, err := dev.device.WatchProperties()
if err != nil {
return nil, err
}
// If AttemptReconnect enabled
if dev.opts.AttemptReconnect {
go func() {
disconnEvtNum := 0
// For every event
for evt := range devEvtCh {
// If device disconnected
if evt.Name == "Connected" && evt.Value == false {
// Increment disconnect event number
disconnEvtNum++
// If more than one disconnect event
if disconnEvtNum > 1 {
// Decrement disconnect event number
disconnEvtNum--
// Skip loop
continue
}
// Set connected to false
dev.device.Properties.Connected = false
// While not connected
for !dev.device.Properties.Connected {
// Attempt to connect via bluetooth address
reConnDev, err := ConnectByAddress(dev.device.Properties.Address)
if err != nil {
// Decrement disconnect event number
disconnEvtNum--
// Skip rest of loop
continue
}
// Store onReconn callback
onReconn := dev.onReconnect
// Set device to new device
*dev = *reConnDev
// Run on reconnect callback
onReconn()
// Assign callback to new device
dev.onReconnect = onReconn
}
// Decrement disconnect event number
disconnEvtNum--
}
}
}()
}
return dev, nil
}
// OnReconnect sets the callback that runs on reconnect
func (i *Device) OnReconnect(f func()) {
i.onReconnect = f
}
// Connect connects to a paired InfiniTime device
func connectByName(opts *Options) (*Device, error) {
// Create new device
out := &Device{}
// Get devices from default adapter
devs, err := defaultAdapter.GetDevices()
if err != nil {
return nil, err
}
// For every device
for _, dev := range devs {
// If device name is InfiniTime
if dev.Properties.Name == BTName {
if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) {
continue
}
// Set outout device to discovered device
out.device = dev
break
}
}
if out.device == nil {
return nil, ErrNoDevices
}
// Connect to device
err = out.device.Connect()
if err != nil {
return nil, err
}
out.device.Properties.Connected = true
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
}
return out, nil
}
func contains(ss []string, s string) bool {
for _, str := range ss {
if strings.EqualFold(str, s) {
return true
}
}
return false
}
// Pair attempts to discover and pair an InfiniTime device
func pair(opts *Options) (*Device, error) {
// Create new device
out := &Device{}
// Start bluetooth discovery
// Ignore the cancel function as it blocks forever
discovery, _, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"})
if err != nil {
return nil, err
}
// For every discovery event
for event := range discovery {
// If device removed, skip event
if event.Type == adapter.DeviceRemoved {
continue
}
// Create new device with discovered path
dev, err := device.NewDevice1(event.Path)
if err != nil {
return nil, err
}
// If device name is InfiniTime
if dev.Properties.Name == BTName {
if opts.WhitelistEnabled && !contains(opts.Whitelist, dev.Properties.Address) {
continue
}
// Set output device
out.device = dev
break
}
}
if out.device == nil {
return nil, ErrNotFound
}
// Pair device
out.device.Pair()
out.device.Properties.Connected = true
// Set connected to true
out.device.Properties.Connected = true
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
}
return out, nil
}
// ConnectByAddress tries to connect to an InifiniTime at
// the specified InfiniTime address
func ConnectByAddress(addr string) (*Device, error) {
var err error
// Create new device
out := &Device{}
// Get device from bluetooth address
out.device, err = defaultAdapter.GetDeviceByAddress(addr)
if err != nil {
return nil, err
}
// Connect to device
err = out.device.Connect()
if err != nil {
return nil, err
}
out.device.Properties.Connected = true
// Resolve characteristics
err = out.resolveChars()
if err != nil {
return nil, err
}
return out, nil
}
// 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 {
// Set correct characteristics
switch char.Properties.UUID {
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 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
}
}
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); 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); 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); 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); 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); 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() (<-chan uint8, func(), error) {
if err := i.checkStatus(i.heartRateChar); err != nil {
return nil, nil, err
}
// Start notifications on heart rate characteristic
err := i.heartRateChar.StartNotify()
if err != nil {
return nil, nil, err
}
// Watch characteristics of heart rate characteristic
ch, err := i.heartRateChar.WatchProperties()
if err != nil {
return nil, nil, err
}
out := make(chan uint8, 2)
currentHeartRate, err := i.HeartRate()
if err != nil {
return nil, nil, err
}
out <- currentHeartRate
cancel, done := cancelFunc()
go func() {
// For every event
for event := range ch {
select {
case <-done:
close(out)
close(done)
i.heartRateChar.StopNotify()
return
default:
// If value changed
if event.Name == "Value" {
// Send heart rate to channel
out <- uint8(event.Value.([]byte)[1])
}
}
}
}()
return out, cancel, nil
}
func (i *Device) WatchBatteryLevel() (<-chan uint8, func(), error) {
if err := i.checkStatus(i.battLevelChar); err != nil {
return nil, nil, err
}
// Start notifications on heart rate characteristic
err := i.battLevelChar.StartNotify()
if err != nil {
return nil, nil, err
}
// Watch characteristics of heart rate characteristic
ch, err := i.battLevelChar.WatchProperties()
if err != nil {
return nil, nil, err
}
out := make(chan uint8, 2)
currentBattLevel, err := i.BatteryLevel()
if err != nil {
return nil, nil, err
}
out <- currentBattLevel
cancel, done := cancelFunc()
go func() {
// For every event
for event := range ch {
select {
case <-done:
close(out)
close(done)
i.battLevelChar.StopNotify()
return
default:
// If value changed
if event.Name == "Value" {
// Send heart rate to channel
out <- uint8(event.Value.([]byte)[0])
}
}
}
}()
return out, cancel, nil
}
func (i *Device) WatchStepCount() (<-chan uint32, func(), error) {
if err := i.checkStatus(i.stepCountChar); err != nil {
return nil, nil, err
}
// Start notifications on step count characteristic
err := i.stepCountChar.StartNotify()
if err != nil {
return nil, nil, err
}
// Watch properties of step count characteristic
ch, err := i.stepCountChar.WatchProperties()
if err != nil {
return nil, nil, err
}
out := make(chan uint32, 2)
currentStepCount, err := i.StepCount()
if err != nil {
return nil, nil, err
}
out <- currentStepCount
cancel, done := cancelFunc()
go func() {
// For every event
for event := range ch {
select {
case <-done:
close(out)
close(done)
i.stepCountChar.StopNotify()
return
default:
// If value changed
if event.Name == "Value" {
// Send step count to channel
out <- binary.LittleEndian.Uint32(event.Value.([]byte))
}
}
}
}()
return out, cancel, nil
}
func (i *Device) WatchMotion() (<-chan MotionValues, func(), error) {
if err := i.checkStatus(i.motionValChar); err != nil {
return nil, nil, err
}
// Start notifications on motion characteristic
err := i.motionValChar.StartNotify()
if err != nil {
return nil, nil, err
}
// Watch properties of motion characteristic
ch, err := i.motionValChar.WatchProperties()
if err != nil {
return nil, nil, err
}
out := make(chan MotionValues, 2)
motionVals, err := i.Motion()
if err != nil {
return nil, nil, err
}
out <- motionVals
cancel, done := cancelFunc()
go func() {
// For every event
for event := range ch {
select {
case <-done:
close(out)
close(done)
i.motionValChar.StopNotify()
return
default:
// 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
}
}
}
}()
return out, cancel, nil
}
func cancelFunc() (func(), chan struct{}) {
done := make(chan struct{}, 1)
return func() {
done <- struct{}{}
}, done
}
// 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 {
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))
return i.currentTimeChar.WriteValue(buf.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); 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); 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); 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); 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
}
// Write data to weather data characteristic
return i.weatherDataChar.WriteValue(data, nil)
}
func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error {
if !i.device.Properties.Connected {
return ErrNotConnected
}
if char == nil {
return ErrCharNotAvail
}
return nil
}