Initial Commit
This commit is contained in:
commit
1b10eba464
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Arsen Musayelyan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
49
README.md
Normal file
49
README.md
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# InfiniTime
|
||||||
|
|
||||||
|
This is a go library for interfacing with InfiniTime firmware
|
||||||
|
over BLE on Linux.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
This library requires `dbus`, `bluez`, `playerctl`, and `pactl` to function. The first two are for bluetooth, and the last two for music control.
|
||||||
|
|
||||||
|
#### Arch
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo pacman -S dbus bluez playerctl --needed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debian/Ubuntu
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt install dbus bluez playerctl
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fedora
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo dnf install dbus bluez playerctl
|
||||||
|
```
|
||||||
|
|
||||||
|
`pactl` comes with `pulseaudio` or `pipewire-pulse` and should therefore be installed on most systems already.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
This library currently supports the following features:
|
||||||
|
|
||||||
|
- Notifications
|
||||||
|
- Heart rate monitoring
|
||||||
|
- Setting time
|
||||||
|
- Battery level
|
||||||
|
- Music control
|
||||||
|
- OTA firmware upgrades
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Mentions
|
||||||
|
|
||||||
|
The DFU process used in this library was created with the help of [siglo](https://github.com/alexr4535/siglo)'s source code. Specifically, this file: [ble_dfu.py](https://github.com/alexr4535/siglo/blob/main/src/ble_dfu.py)
|
23
btsetup.go
Normal file
23
btsetup.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package infinitime
|
||||||
|
|
||||||
|
import (
|
||||||
|
bt "github.com/muka/go-bluetooth/api"
|
||||||
|
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultAdapter *adapter.Adapter1
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Get bluez default adapter
|
||||||
|
da, err := bt.GetDefaultAdapter()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultAdapter = da
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func Exit() error {
|
||||||
|
return bt.Exit()
|
||||||
|
}
|
363
dfu.go
Normal file
363
dfu.go
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
package infinitime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/muka/go-bluetooth/bluez"
|
||||||
|
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DFUCtrlPointChar = "00001531-1212-efde-1523-785feabcd123" // UUID of Control Point characteristic
|
||||||
|
DFUPacketChar = "00001532-1212-efde-1523-785feabcd123" // UUID of Packet characteristic
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DFUSegmentSize = 20 // Size of each firmware packet
|
||||||
|
DFUPktRecvInterval = 10 // Amount of packets to send before checking for receipt
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DFUCmdStart = []byte{0x01, 0x04}
|
||||||
|
DFUCmdRecvInitPkt = []byte{0x02, 0x00}
|
||||||
|
DFUCmdInitPktComplete = []byte{0x02, 0x01}
|
||||||
|
DFUCmdPktReceiptInterval = []byte{0x08, 0x0A}
|
||||||
|
DFUCmdRecvFirmware = []byte{0x03}
|
||||||
|
DFUCmdValidate = []byte{0x04}
|
||||||
|
DFUCmdActivateReset = []byte{0x05}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DFUResponseStart = []byte{0x10, 0x01, 0x01}
|
||||||
|
DFUResponseInitParams = []byte{0x10, 0x02, 0x01}
|
||||||
|
DFUResponseRecvFwImgSuccess = []byte{0x10, 0x03, 0x01}
|
||||||
|
DFUResponseValidate = []byte{0x10, 0x04, 0x01}
|
||||||
|
)
|
||||||
|
|
||||||
|
var DFUNotifPktRecvd = []byte{0x11}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDFUInvalidInput = errors.New("input file invalid, must be a .bin file")
|
||||||
|
ErrDFUTimeout = errors.New("timed out waiting for response")
|
||||||
|
ErrDFUNoFilesLoaded = errors.New("no files are loaded")
|
||||||
|
ErrDFUInvalidResponse = errors.New("invalid response returned")
|
||||||
|
ErrDFUSizeMismatch = errors.New("amount of bytes sent does not match amount received")
|
||||||
|
)
|
||||||
|
|
||||||
|
var btOptsCmd = map[string]interface{}{"type": "command"}
|
||||||
|
|
||||||
|
// DFU stores everything required for doing firmware upgrades
|
||||||
|
type DFU struct {
|
||||||
|
initPacket fs.File
|
||||||
|
fwImage fs.File
|
||||||
|
ctrlRespCh <-chan *bluez.PropertyChanged
|
||||||
|
fwSize int64
|
||||||
|
bytesSent int
|
||||||
|
bytesRecvd int
|
||||||
|
fwSendDone bool
|
||||||
|
ctrlPointChar *gatt.GattCharacteristic1
|
||||||
|
packetChar *gatt.GattCharacteristic1
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFiles loads an init packet (.dat) and firmware image (.bin)
|
||||||
|
func (dfu *DFU) LoadFiles(initPath, fwPath string) error {
|
||||||
|
// Open init packet file
|
||||||
|
initPktFl, err := os.Open(initPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dfu.initPacket = initPktFl
|
||||||
|
|
||||||
|
// Open firmware image file
|
||||||
|
fwImgFl, err := os.Open(fwPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dfu.fwImage = fwImgFl
|
||||||
|
|
||||||
|
// Get firmware file size
|
||||||
|
dfu.fwSize, err = getFlSize(dfu.fwImage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type archiveManifest struct {
|
||||||
|
Manifest struct {
|
||||||
|
Application struct {
|
||||||
|
BinFile string `json:"bin_file"`
|
||||||
|
DatFile string `json:"dat_file"`
|
||||||
|
} `json:"application"`
|
||||||
|
} `json:"manifest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadArchive loads an init packet and firmware image from a zip archive
|
||||||
|
// using a maifest.json also stored in the archive.
|
||||||
|
func (dfu *DFU) LoadArchive(archivePath string) error {
|
||||||
|
// Open archive file
|
||||||
|
archiveFl, err := os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get archive size
|
||||||
|
archiveSize, err := getFlSize(archiveFl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zip reader from archive file
|
||||||
|
zipReader, err := zip.NewReader(archiveFl, archiveSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open manifest.json from zip archive
|
||||||
|
manifestFl, err := zipReader.Open("manifest.json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest archiveManifest
|
||||||
|
// Decode manifest file as JSON
|
||||||
|
err = json.NewDecoder(manifestFl).Decode(&manifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open init packet from zip archive
|
||||||
|
initPktFl, err := zipReader.Open(manifest.Manifest.Application.DatFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dfu.initPacket = initPktFl
|
||||||
|
|
||||||
|
// Open firmware image from zip archive
|
||||||
|
fwImgFl, err := zipReader.Open(manifest.Manifest.Application.BinFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dfu.fwImage = fwImgFl
|
||||||
|
|
||||||
|
// Get file size of firmware image
|
||||||
|
dfu.fwSize, err = getFlSize(dfu.fwImage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFlSize uses Stat to get the size of a file
|
||||||
|
func getFlSize(fl fs.File) (int64, error) {
|
||||||
|
// Get file information
|
||||||
|
flInfo, err := fl.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return flInfo.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start DFU process
|
||||||
|
func (dfu *DFU) Start() error {
|
||||||
|
if dfu.fwImage == nil || dfu.initPacket == nil {
|
||||||
|
return ErrDFUNoFilesLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start notifications on control point
|
||||||
|
err := dfu.ctrlPointChar.StartNotify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for property changes on control point
|
||||||
|
dfu.ctrlRespCh, err = dfu.ctrlPointChar.WatchProperties()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run step one
|
||||||
|
err = dfu.stepOne()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run step two
|
||||||
|
err = dfu.stepTwo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 0x100101 received, run step three
|
||||||
|
err = dfu.on(DFUResponseStart, func(_ []byte) error {
|
||||||
|
return dfu.stepThree()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run step three
|
||||||
|
err = dfu.stepFour()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 0x100201 received. run step five
|
||||||
|
err = dfu.on(DFUResponseInitParams, func(_ []byte) error {
|
||||||
|
return dfu.stepFive()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run step six
|
||||||
|
err = dfu.stepSix()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run step seven
|
||||||
|
err = dfu.stepSeven()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 0x100301 received, run step eight
|
||||||
|
err = dfu.on(DFUResponseRecvFwImgSuccess, func(_ []byte) error {
|
||||||
|
return dfu.stepEight()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 0x100401 received, run step nine
|
||||||
|
err = dfu.on(DFUResponseValidate, func(_ []byte) error {
|
||||||
|
return dfu.stepNine()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// on waits for the given command to be received on
|
||||||
|
// the control point characteristic, then runs the callback.
|
||||||
|
func (dfu *DFU) on(cmd []byte, onCmdCb func(data []byte) error) error {
|
||||||
|
select {
|
||||||
|
case propChanged := <-dfu.ctrlRespCh:
|
||||||
|
if propChanged.Name != "Value" {
|
||||||
|
return ErrDFUInvalidResponse
|
||||||
|
}
|
||||||
|
// Assert propery value as byte slice
|
||||||
|
data := propChanged.Value.([]byte)
|
||||||
|
// If command has prefix of given command
|
||||||
|
if bytes.HasPrefix(data, cmd) {
|
||||||
|
// Return callback with data after command
|
||||||
|
return onCmdCb(data[len(cmd):])
|
||||||
|
}
|
||||||
|
return ErrDFUInvalidResponse
|
||||||
|
case <-time.After(50 * time.Second):
|
||||||
|
return ErrDFUTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepOne() error {
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdStart, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepTwo() error {
|
||||||
|
// Create byte slice with 4 bytes allocated
|
||||||
|
data := make([]byte, 4)
|
||||||
|
// Write little endian uint32 to data slice
|
||||||
|
binary.LittleEndian.PutUint32(data, uint32(dfu.fwSize))
|
||||||
|
// Pad data with 8 bytes
|
||||||
|
data = append(make([]byte, 8), data...)
|
||||||
|
// Write data to packet characteristic
|
||||||
|
return dfu.packetChar.WriteValue(data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepThree() error {
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdRecvInitPkt, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepFour() error {
|
||||||
|
// Read init packet
|
||||||
|
data, err := ioutil.ReadAll(dfu.initPacket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Write init packet to packet characteristic
|
||||||
|
err = dfu.packetChar.WriteValue(data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Write init packet complete command to control point
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdInitPktComplete, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepFive() error {
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdPktReceiptInterval, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepSix() error {
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdRecvFirmware, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepSeven() error {
|
||||||
|
// While send is not done
|
||||||
|
for !dfu.fwSendDone {
|
||||||
|
for i := 0; i < DFUPktRecvInterval; i++ {
|
||||||
|
// Create byte slice with segment size
|
||||||
|
segment := make([]byte, DFUSegmentSize)
|
||||||
|
// Write firmware image into slice
|
||||||
|
n, err := dfu.fwImage.Read(segment)
|
||||||
|
// If EOF, send is done
|
||||||
|
if err == io.EOF {
|
||||||
|
dfu.fwSendDone = true
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Write segment to packet characteristic
|
||||||
|
err = dfu.packetChar.WriteValue(segment, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Increment bytes sent by amount read
|
||||||
|
dfu.bytesSent += n
|
||||||
|
}
|
||||||
|
// On 0x11, verify packet receipt size
|
||||||
|
err := dfu.on(DFUNotifPktRecvd, func(data []byte) error {
|
||||||
|
// Set bytes received to data returned by InfiniTime
|
||||||
|
dfu.bytesRecvd = int(binary.LittleEndian.Uint32(data))
|
||||||
|
if dfu.bytesRecvd != dfu.bytesSent {
|
||||||
|
return ErrDFUSizeMismatch
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepEight() error {
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdValidate, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dfu *DFU) stepNine() error {
|
||||||
|
return dfu.ctrlPointChar.WriteValue(DFUCmdActivateReset, btOptsCmd)
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module go.arsenm.dev/infinitime
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d
|
52
go.sum
Normal file
52
go.sum
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d h1:EG/xyWjHT19rkUpwsWSkyiCCmyqNwFovr9m10rhyOxU=
|
||||||
|
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||||
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
|
||||||
|
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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,
|
||||||
|
)
|
||||||
|
}
|
81
music.go
Normal file
81
music.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package infinitime
|
||||||
|
|
||||||
|
import "github.com/muka/go-bluetooth/bluez/profile/gatt"
|
||||||
|
|
||||||
|
type MusicEvent uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
MusicEventChar = "00000001-78fc-48fe-8e23-433b3a1942d0"
|
||||||
|
MusicStatusChar = "00000002-78fc-48fe-8e23-433b3a1942d0"
|
||||||
|
MusicArtistChar = "00000003-78fc-48fe-8e23-433b3a1942d0"
|
||||||
|
MusicTrackChar = "00000004-78fc-48fe-8e23-433b3a1942d0"
|
||||||
|
MusicAlbumChar = "00000005-78fc-48fe-8e23-433b3a1942d0"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MusicEventOpen MusicEvent = 0xe0
|
||||||
|
MusicEventPlay MusicEvent = 0x00
|
||||||
|
MusicEventPause MusicEvent = 0x01
|
||||||
|
MusicEventNext MusicEvent = 0x03
|
||||||
|
MusicEventPrev MusicEvent = 0x04
|
||||||
|
MusicEventVolUp MusicEvent = 0x05
|
||||||
|
MusicEventVolDown MusicEvent = 0x06
|
||||||
|
)
|
||||||
|
|
||||||
|
// MusicCtrl stores everything required to control music
|
||||||
|
type MusicCtrl struct {
|
||||||
|
eventChar *gatt.GattCharacteristic1
|
||||||
|
statusChar *gatt.GattCharacteristic1
|
||||||
|
artistChar *gatt.GattCharacteristic1
|
||||||
|
trackChar *gatt.GattCharacteristic1
|
||||||
|
albumChar *gatt.GattCharacteristic1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStatus sets the playing status
|
||||||
|
func (mc MusicCtrl) SetStatus(playing bool) error {
|
||||||
|
if playing {
|
||||||
|
return mc.statusChar.WriteValue([]byte{0x1}, nil)
|
||||||
|
}
|
||||||
|
return mc.statusChar.WriteValue([]byte{0x0}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetArtist sets the artist on InfniTime
|
||||||
|
func (mc MusicCtrl) SetArtist(artist string) error {
|
||||||
|
return mc.artistChar.WriteValue([]byte(artist), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTrack sets the track name on InfniTime
|
||||||
|
func (mc MusicCtrl) SetTrack(track string) error {
|
||||||
|
return mc.trackChar.WriteValue([]byte(track), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAlbum sets the album on InfniTime
|
||||||
|
func (mc MusicCtrl) SetAlbum(album string) error {
|
||||||
|
return mc.albumChar.WriteValue([]byte(album), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchEvents watches music events from InfiniTime
|
||||||
|
func (mc MusicCtrl) WatchEvents() (<-chan MusicEvent, error) {
|
||||||
|
// Start notifications on music event characteristic
|
||||||
|
err := mc.eventChar.StartNotify()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Watch music event properties
|
||||||
|
ch, err := mc.eventChar.WatchProperties()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
musicEventCh := make(chan MusicEvent, 5)
|
||||||
|
go func() {
|
||||||
|
// For every event
|
||||||
|
for event := range ch {
|
||||||
|
// If value changes
|
||||||
|
if event.Name == "Value" {
|
||||||
|
// Send music event to channel
|
||||||
|
musicEventCh <- MusicEvent(event.Value.([]byte)[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return musicEventCh, nil
|
||||||
|
}
|
16
pkg/player/pactl.go
Normal file
16
pkg/player/pactl.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VolUp uses pactl to increase the volume of the default sink
|
||||||
|
func VolUp(percent uint) error {
|
||||||
|
return exec.Command("pactl", "set-sink-volume", "@DEFAULT_SINK@", fmt.Sprintf("+%d%%", percent)).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// VolDown uses pactl to decrease the volume of the default sink
|
||||||
|
func VolDown(percent uint) error {
|
||||||
|
return exec.Command("pactl", "set-sink-volume", "@DEFAULT_SINK@", fmt.Sprintf("-%d%%", percent)).Run()
|
||||||
|
}
|
91
pkg/player/playerctl.go
Normal file
91
pkg/player/playerctl.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Play uses playerctl to play media
|
||||||
|
func Play() error {
|
||||||
|
return exec.Command("playerctl", "play").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause uses playerctl to pause media
|
||||||
|
func Pause() error {
|
||||||
|
return exec.Command("playerctl", "pause").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next uses playerctl to skip to next media
|
||||||
|
func Next() error {
|
||||||
|
return exec.Command("playerctl", "next").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prev uses playerctl to skip to previous media
|
||||||
|
func Prev() error {
|
||||||
|
return exec.Command("playerctl", "previous").Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata uses playerctl to detect music metadata changes
|
||||||
|
func Metadata(key string, onChange func(string)) error {
|
||||||
|
// Execute playerctl command with key and follow flag
|
||||||
|
cmd := exec.Command("playerctl", "metadata", key, "-F")
|
||||||
|
// Get stdout pipe
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
// Read line from command stdout
|
||||||
|
line, _, err := bufio.NewReader(stdout).ReadLine()
|
||||||
|
if err == io.EOF {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Convert line to string
|
||||||
|
data := string(line)
|
||||||
|
// If key unknown, return suitable default
|
||||||
|
if data == "No player could handle this command" || data == "" {
|
||||||
|
data = "Unknown " + strings.Title(key)
|
||||||
|
}
|
||||||
|
// Run the onChange callback
|
||||||
|
onChange(data)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Start command asynchronously
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Status(onChange func(bool)) error {
|
||||||
|
// Execute playerctl status with follow flag
|
||||||
|
cmd := exec.Command("playerctl", "status", "-F")
|
||||||
|
// Get stdout pipe
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
// Read line from command stdout
|
||||||
|
line, _, err := bufio.NewReader(stdout).ReadLine()
|
||||||
|
if err == io.EOF {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Convert line to string
|
||||||
|
data := string(line)
|
||||||
|
// Run the onChange callback
|
||||||
|
onChange(data == "Playing")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Start command asynchronously
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue