Initial Commit
This commit is contained in:
387
infinitime.go
Normal file
387
infinitime.go
Normal file
@@ -0,0 +1,387 @@
|
||||
package infinitime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const BTName = "InfiniTime"
|
||||
|
||||
const (
|
||||
NewAlertChar = "00002a46-0000-1000-8000-00805f9b34fb"
|
||||
FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb"
|
||||
CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
|
||||
BatteryLvlChar = "00002a19-0000-1000-8000-00805f9b34fb"
|
||||
HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
opts *Options
|
||||
device *device.Device1
|
||||
newAlertChar *gatt.GattCharacteristic1
|
||||
fwVersionChar *gatt.GattCharacteristic1
|
||||
currentTimeChar *gatt.GattCharacteristic1
|
||||
battLevelChar *gatt.GattCharacteristic1
|
||||
heartRateChar *gatt.GattCharacteristic1
|
||||
onReconnect func()
|
||||
Music MusicCtrl
|
||||
DFU DFU
|
||||
}
|
||||
|
||||
var ErrNoDevices = errors.New("no InfiniTime devices found")
|
||||
var ErrNotFound = errors.New("could not find any advertising InfiniTime devices")
|
||||
|
||||
|
||||
type Options struct {
|
||||
AttemptReconnect bool
|
||||
}
|
||||
|
||||
var DefaultOptions = &Options{
|
||||
AttemptReconnect: true,
|
||||
}
|
||||
|
||||
// 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()
|
||||
// If such device does not exist
|
||||
if errors.Is(err, ErrNoDevices) {
|
||||
// Attempt to pair device
|
||||
dev, err = pair()
|
||||
}
|
||||
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() (*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 {
|
||||
// Set outout device to discovered device
|
||||
out.device = dev
|
||||
break
|
||||
}
|
||||
}
|
||||
if out.device == nil {
|
||||
return nil, ErrNoDevices
|
||||
}
|
||||
// Connect to device
|
||||
out.device.Connect()
|
||||
// Resolve characteristics
|
||||
err = out.resolveChars()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Pair attempts to discover and pair an InfiniTime device
|
||||
func pair() (*Device, error) {
|
||||
// Create new device
|
||||
out := &Device{}
|
||||
// Start bluetooth discovery
|
||||
discovery, cancelDiscover, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Cancel discovery at end of function
|
||||
defer cancelDiscover()
|
||||
discoveryLoop:
|
||||
for {
|
||||
select {
|
||||
case event := <-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 {
|
||||
// Set output device
|
||||
out.device = dev
|
||||
// Break out of discoveryLoop
|
||||
break discoveryLoop
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
break discoveryLoop
|
||||
}
|
||||
}
|
||||
|
||||
if out.device == nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// Pair device
|
||||
out.device.Pair()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
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 !i.device.Properties.Connected {
|
||||
return "", nil
|
||||
}
|
||||
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 !i.device.Properties.Connected {
|
||||
return 0, nil
|
||||
}
|
||||
battLevel, err := i.battLevelChar.ReadValue(nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint8(battLevel[0]), nil
|
||||
}
|
||||
|
||||
func (i *Device) HeartRate() (uint8, error) {
|
||||
if !i.device.Properties.Connected {
|
||||
return 0, nil
|
||||
}
|
||||
heartRate, err := i.heartRateChar.ReadValue(nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint8(heartRate[1]), nil
|
||||
}
|
||||
|
||||
func (i *Device) WatchHeartRate() (<-chan uint8, error) {
|
||||
if !i.device.Properties.Connected {
|
||||
return make(<-chan uint8), nil
|
||||
}
|
||||
// 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)
|
||||
go func() {
|
||||
// For every event
|
||||
for event := range ch {
|
||||
// If value changed
|
||||
if event.Name == "Value" {
|
||||
// Send heart rate to channel
|
||||
out <- uint8(event.Value.([]byte)[1])
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SetTime sets the watch's time using the Current Time Service
|
||||
func (i *Device) SetTime(t time.Time) error {
|
||||
if !i.device.Properties.Connected {
|
||||
return nil
|
||||
}
|
||||
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 !i.device.Properties.Connected {
|
||||
return nil
|
||||
}
|
||||
return i.newAlertChar.WriteValue(
|
||||
[]byte(fmt.Sprintf("00\x00%s\x00%s", title, body)),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user