Compare commits
24 Commits
master
..
75942bdb0c
| Author | SHA1 | Date | |
|---|---|---|---|
| 75942bdb0c | |||
| 53aa6f8a0c | |||
| 45baea1048 | |||
| 1dde7f9b07 | |||
| d1a75f1c67 | |||
| 937a3298c8 | |||
| bf99350e48 | |||
| c4c0c46d71 | |||
| c101249d3e | |||
| a1e08ed862 | |||
| dbfe8bb8c4 | |||
| 745b4bd37c | |||
| 0430ddcd30 | |||
| 8648afeebf | |||
| 75121b709c | |||
| 9553844896 | |||
| 47293f04bc | |||
| d228b6cf60 | |||
| 91fbe718e5 | |||
| 7f19dfb354 | |||
| ea488067fb | |||
| 08076e6ba2 | |||
| 3eadabe975 | |||
| 21078996c1 |
@@ -1,42 +1,42 @@
|
||||
# InfiniTime
|
||||
|
||||
> **Warning** This library is no longer maintained. A rewrite has been merged into the ITD repo in [the infinitime subpackage](https://gitea.elara.ws/Elara6331/itd/src/branch/master/infinitime)
|
||||
|
||||
This is a go library for interfacing with InfiniTime firmware
|
||||
over BLE on Linux.
|
||||
|
||||
[](https://pkg.go.dev/go.elara.ws/infinitime)
|
||||
[](https://pkg.go.dev/go.arsenm.dev/infinitime)
|
||||
|
||||
---
|
||||
|
||||
### Importing
|
||||
|
||||
This library's import path is `go.elara.ws/infinitime`.
|
||||
This library's import path is `go.arsenm.dev/infinitime`.
|
||||
|
||||
---
|
||||
|
||||
### Dependencies
|
||||
|
||||
This library requires `dbus`, and `bluez` to function. These allow the library to use bluetooth, control media, control volume, etc.
|
||||
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 --needed
|
||||
sudo pacman -S dbus bluez playerctl --needed
|
||||
```
|
||||
|
||||
#### Debian/Ubuntu
|
||||
|
||||
```shell
|
||||
sudo apt install dbus bluez
|
||||
sudo apt install dbus bluez playerctl
|
||||
```
|
||||
|
||||
#### Fedora
|
||||
|
||||
```shell
|
||||
sudo dnf install dbus bluez
|
||||
sudo dnf install dbus bluez playerctl
|
||||
```
|
||||
|
||||
`pactl` comes with `pulseaudio` or `pipewire-pulse` and should therefore be installed on most systems already.
|
||||
|
||||
---
|
||||
|
||||
### Features
|
||||
@@ -49,10 +49,9 @@ This library currently supports the following features:
|
||||
- Battery level
|
||||
- Music control
|
||||
- OTA firmware upgrades
|
||||
- Navigation
|
||||
|
||||
---
|
||||
|
||||
### 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)
|
||||
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)
|
||||
@@ -1,95 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (blefs *FS) RemoveAll(path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if filepath.Clean(path) == "/" {
|
||||
return ErrNoRemoveRoot
|
||||
}
|
||||
|
||||
fi, err := blefs.Stat(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return blefs.removeAllChildren(path)
|
||||
} else {
|
||||
err = blefs.Remove(path)
|
||||
|
||||
var code int8
|
||||
if err, ok := err.(FSError); ok {
|
||||
code = err.Code
|
||||
}
|
||||
|
||||
if err != nil && code != -2 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (blefs *FS) removeAllChildren(path string) error {
|
||||
list, err := blefs.ReadDir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range list {
|
||||
name := entry.Name()
|
||||
|
||||
if name == "." || name == ".." {
|
||||
continue
|
||||
}
|
||||
entryPath := filepath.Join(path, name)
|
||||
|
||||
if entry.IsDir() {
|
||||
err = blefs.removeAllChildren(entryPath)
|
||||
} else {
|
||||
err = blefs.Remove(entryPath)
|
||||
}
|
||||
|
||||
var code int8
|
||||
if err, ok := err.(FSError); ok {
|
||||
code = err.Code
|
||||
}
|
||||
|
||||
if err != nil && code != -2 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (blefs *FS) MkdirAll(path string) error {
|
||||
if path == "" || path == "/" {
|
||||
return nil
|
||||
}
|
||||
|
||||
splitPath := strings.Split(path, "/")
|
||||
for i := 1; i < len(splitPath); i++ {
|
||||
curPath := strings.Join(splitPath[0:i+1], "/")
|
||||
|
||||
err := blefs.Mkdir(curPath)
|
||||
|
||||
var code int8
|
||||
if err, ok := err.(FSError); ok {
|
||||
code = err.Code
|
||||
}
|
||||
|
||||
if err != nil && code != -17 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Stat does a ReadDir() and finds the current file in the output
|
||||
func (blefs *FS) Stat(path string) (fs.FileInfo, error) {
|
||||
// Get directory in filepath
|
||||
dir := filepath.Dir(path)
|
||||
// Read directory
|
||||
dirEntries, err := blefs.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range dirEntries {
|
||||
// If file name is base name of path
|
||||
if entry.Name() == filepath.Base(path) {
|
||||
// Return file info
|
||||
return entry.Info()
|
||||
}
|
||||
}
|
||||
return nil, ErrFileNotExists
|
||||
}
|
||||
|
||||
// Rename moves or renames a file or directory
|
||||
func (blefs *FS) Rename(old, new string) error {
|
||||
// Create move request
|
||||
err := blefs.request(
|
||||
FSCmdMove,
|
||||
true,
|
||||
uint16(len(old)),
|
||||
uint16(len(new)),
|
||||
old,
|
||||
byte(0x00),
|
||||
new,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var status int8
|
||||
// Upon receiving 0x61 (FSResponseMove)
|
||||
blefs.on(FSResponseMove, func(data []byte) error {
|
||||
// Read status byte
|
||||
return decode(data, &status)
|
||||
})
|
||||
// If status is not ok, return error
|
||||
if status != FSStatusOk {
|
||||
return FSError{status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a file or directory
|
||||
func (blefs *FS) Remove(path string) error {
|
||||
// Create delete request
|
||||
err := blefs.request(
|
||||
FSCmdDelete,
|
||||
true,
|
||||
uint16(len(path)),
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var status int8
|
||||
// Upon receiving 0x31 (FSResponseDelete)
|
||||
blefs.on(FSResponseDelete, func(data []byte) error {
|
||||
// Read status byte
|
||||
return decode(data, &status)
|
||||
})
|
||||
if status != FSStatusOk {
|
||||
// If status is not ok, return error
|
||||
return FSError{status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-156
@@ -1,156 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mkdir creates a directory at the given path
|
||||
func (blefs *FS) Mkdir(path string) error {
|
||||
// Create make directory request
|
||||
err := blefs.request(
|
||||
FSCmdMkdir,
|
||||
true,
|
||||
uint16(len(path)),
|
||||
padding(4),
|
||||
uint64(time.Now().UnixNano()),
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var status int8
|
||||
// Upon receiving 0x41 (FSResponseMkdir), read status byte
|
||||
blefs.on(FSResponseMkdir, func(data []byte) error {
|
||||
return decode(data, &status)
|
||||
})
|
||||
// If status not ok, return error
|
||||
if status != FSStatusOk {
|
||||
return FSError{status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDir returns a list of directory entries from the given path
|
||||
func (blefs *FS) ReadDir(path string) ([]fs.DirEntry, error) {
|
||||
// Create list directory request
|
||||
err := blefs.request(
|
||||
FSCmdListDir,
|
||||
true,
|
||||
uint16(len(path)),
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []fs.DirEntry
|
||||
for {
|
||||
// Create new directory entry
|
||||
listing := DirEntry{}
|
||||
// Upon receiving 0x50 (FSResponseListDir)
|
||||
blefs.on(FSResponseListDir, func(data []byte) error {
|
||||
// Read data into listing
|
||||
err := decode(
|
||||
data,
|
||||
&listing.status,
|
||||
&listing.pathLen,
|
||||
&listing.entryNum,
|
||||
&listing.entries,
|
||||
&listing.flags,
|
||||
&listing.modtime,
|
||||
&listing.size,
|
||||
&listing.path,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// If status is not ok, return error
|
||||
if listing.status != FSStatusOk {
|
||||
return nil, FSError{listing.status}
|
||||
}
|
||||
// Stop once entry number equals total entries
|
||||
if listing.entryNum == listing.entries {
|
||||
break
|
||||
}
|
||||
// Append listing to slice
|
||||
out = append(out, listing)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DirEntry represents an entry from a directory listing
|
||||
type DirEntry struct {
|
||||
status int8
|
||||
pathLen uint16
|
||||
entryNum uint32
|
||||
entries uint32
|
||||
flags uint32
|
||||
modtime uint64
|
||||
size uint32
|
||||
path string
|
||||
}
|
||||
|
||||
// Name returns the name of the file described by the entry
|
||||
func (de DirEntry) Name() string {
|
||||
return de.path
|
||||
}
|
||||
|
||||
// IsDir reports whether the entry describes a directory.
|
||||
func (de DirEntry) IsDir() bool {
|
||||
return de.flags&0b1 == 1
|
||||
}
|
||||
|
||||
// Type returns the type bits for the entry.
|
||||
func (de DirEntry) Type() fs.FileMode {
|
||||
if de.IsDir() {
|
||||
return fs.ModeDir
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Info returns the FileInfo for the file or subdirectory described by the entry.
|
||||
func (de DirEntry) Info() (fs.FileInfo, error) {
|
||||
return FileInfo{
|
||||
name: de.path,
|
||||
size: de.size,
|
||||
modtime: de.modtime,
|
||||
mode: de.Type(),
|
||||
isDir: de.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (de DirEntry) String() string {
|
||||
var isDirChar rune
|
||||
if de.IsDir() {
|
||||
isDirChar = 'd'
|
||||
} else {
|
||||
isDirChar = '-'
|
||||
}
|
||||
|
||||
// Get human-readable value for file size
|
||||
val, unit := bytesHuman(de.size)
|
||||
prec := 0
|
||||
// If value is less than 10, set precision to 1
|
||||
if val < 10 {
|
||||
prec = 1
|
||||
}
|
||||
// Convert float to string
|
||||
valStr := strconv.FormatFloat(val, 'f', prec, 64)
|
||||
|
||||
// Return string formatted like so:
|
||||
// - 10 kB file
|
||||
// or:
|
||||
// d 0 B .
|
||||
return fmt.Sprintf(
|
||||
"%c %3s %-2s %s",
|
||||
isDirChar,
|
||||
valStr,
|
||||
unit,
|
||||
de.path,
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFileNotExists = errors.New("file does not exist")
|
||||
ErrFileReadOnly = errors.New("file is read only")
|
||||
ErrFileWriteOnly = errors.New("file is write only")
|
||||
ErrInvalidOffset = errors.New("invalid file offset")
|
||||
ErrOffsetChanged = errors.New("offset has already been changed")
|
||||
ErrReadOpen = errors.New("only one file can be opened for reading at a time")
|
||||
ErrWriteOpen = errors.New("only one file can be opened for writing at a time")
|
||||
ErrNoRemoveRoot = errors.New("refusing to remove root directory")
|
||||
)
|
||||
|
||||
// FSError represents an error returned by BLE FS
|
||||
type FSError struct {
|
||||
Code int8
|
||||
}
|
||||
|
||||
// Error returns the string associated with the error code
|
||||
func (err FSError) Error() string {
|
||||
switch err.Code {
|
||||
case 0x02:
|
||||
return "filesystem error"
|
||||
case 0x05:
|
||||
return "read-only filesystem"
|
||||
case 0x03:
|
||||
return "no such file"
|
||||
case 0x04:
|
||||
return "protocol error"
|
||||
case -5:
|
||||
return "input/output error"
|
||||
case -84:
|
||||
return "filesystem is corrupted"
|
||||
case -2:
|
||||
return "no such directory entry"
|
||||
case -17:
|
||||
return "entry already exists"
|
||||
case -20:
|
||||
return "entry is not a directory"
|
||||
case -39:
|
||||
return "directory is not empty"
|
||||
case -9:
|
||||
return "bad file number"
|
||||
case -27:
|
||||
return "file is too large"
|
||||
case -22:
|
||||
return "invalid parameter"
|
||||
case -28:
|
||||
return "no space left on device"
|
||||
case -12:
|
||||
return "no more memory available"
|
||||
case -61:
|
||||
return "no attr available"
|
||||
case -36:
|
||||
return "file name is too long"
|
||||
default:
|
||||
return fmt.Sprintf("unknown error (code %d)", err.Code)
|
||||
}
|
||||
}
|
||||
-367
@@ -1,367 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// File represents a file on the BLE filesystem
|
||||
type File struct {
|
||||
fs *FS
|
||||
path string
|
||||
offset uint32
|
||||
offsetCh chan uint32
|
||||
length uint32
|
||||
amtLeft uint32
|
||||
amtTferd uint32
|
||||
isReadOnly bool
|
||||
isWriteOnly bool
|
||||
offsetChanged bool
|
||||
}
|
||||
|
||||
// Open opens a file and returns it as an fs.File to
|
||||
// satisfy the fs.FS interface
|
||||
func (blefs *FS) Open(path string) (*File, error) {
|
||||
if blefs.readOpen {
|
||||
return nil, ErrReadOpen
|
||||
}
|
||||
blefs.readOpen = true
|
||||
// Make a read file request. This opens the file for reading.
|
||||
err := blefs.request(
|
||||
FSCmdReadFile,
|
||||
true,
|
||||
uint16(len(path)),
|
||||
uint32(0),
|
||||
uint32(blefs.maxData()),
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
fs: blefs,
|
||||
path: path,
|
||||
length: 0,
|
||||
offset: 0,
|
||||
offsetCh: make(chan uint32, 5),
|
||||
isReadOnly: true,
|
||||
isWriteOnly: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create makes a new file on the BLE file system and returns it.
|
||||
func (blefs *FS) Create(path string, size uint32) (*File, error) {
|
||||
if blefs.writeOpen {
|
||||
return nil, ErrWriteOpen
|
||||
}
|
||||
blefs.writeOpen = true
|
||||
// Make a write file request. This will create and open a file for writing.
|
||||
err := blefs.request(
|
||||
FSCmdWriteFile,
|
||||
true,
|
||||
uint16(len(path)),
|
||||
uint32(0),
|
||||
uint64(time.Now().UnixNano()),
|
||||
size,
|
||||
path,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &File{
|
||||
fs: blefs,
|
||||
path: path,
|
||||
length: size,
|
||||
amtLeft: size,
|
||||
offset: 0,
|
||||
offsetCh: make(chan uint32, 5),
|
||||
isReadOnly: false,
|
||||
isWriteOnly: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Size returns the total size of the opened file
|
||||
func (file *File) Size() uint32 {
|
||||
return file.length
|
||||
}
|
||||
|
||||
// Progress returns a channel that receives the amount
|
||||
// of bytes sent as they are sent
|
||||
func (file *File) Progress() <-chan uint32 {
|
||||
return file.offsetCh
|
||||
}
|
||||
|
||||
// Read reads data from a file into b
|
||||
func (fl *File) Read(b []byte) (n int, err error) {
|
||||
// If file is write only (opened by FS.Create())
|
||||
if fl.isWriteOnly {
|
||||
return 0, ErrFileWriteOnly
|
||||
}
|
||||
|
||||
// If offset has been changed (Seek() called)
|
||||
if fl.offsetChanged {
|
||||
// Create new read file request with the specified offset to restart reading
|
||||
err := fl.fs.request(
|
||||
FSCmdReadFile,
|
||||
true,
|
||||
uint16(len(fl.path)),
|
||||
fl.offset,
|
||||
uint32(fl.fs.maxData()),
|
||||
fl.path,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Reset offsetChanged
|
||||
fl.offsetChanged = false
|
||||
}
|
||||
|
||||
// Get length of b. This will be the maximum amount that can be read.
|
||||
maxLen := uint32(len(b))
|
||||
if maxLen == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var buf []byte
|
||||
for {
|
||||
// If amount transfered equals max length
|
||||
if fl.amtTferd == maxLen {
|
||||
// Reset amount transfered
|
||||
fl.amtTferd = 0
|
||||
// Copy buffer contents to b
|
||||
copy(b, buf)
|
||||
// Return max length with no error
|
||||
return int(maxLen), nil
|
||||
}
|
||||
// Create new empty fileReadResponse
|
||||
resp := fileReadResponse{}
|
||||
// Upon receiving 0x11 (FSResponseReadFile)
|
||||
err := fl.fs.on(FSResponseReadFile, func(data []byte) error {
|
||||
// Read binary data into struct
|
||||
err := decode(
|
||||
data,
|
||||
&resp.status,
|
||||
&resp.padding,
|
||||
&resp.offset,
|
||||
&resp.length,
|
||||
&resp.chunkLen,
|
||||
&resp.data,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If status is not ok
|
||||
if resp.status != FSStatusOk {
|
||||
return FSError{resp.status}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// If entire file transferred, break
|
||||
if fl.offset == resp.length {
|
||||
break
|
||||
}
|
||||
|
||||
// Append data returned in response to buffer
|
||||
buf = append(buf, resp.data...)
|
||||
// Set file length
|
||||
fl.length = resp.length
|
||||
// Add returned chunk length to offset and amount transferred
|
||||
fl.offset += resp.chunkLen
|
||||
fl.offsetCh <- fl.offset
|
||||
fl.amtTferd += resp.chunkLen
|
||||
|
||||
// Calculate amount of bytes to be sent in next request
|
||||
chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
|
||||
// If after transferring, there will be more data than max length
|
||||
if fl.amtTferd+chunkLen > maxLen {
|
||||
// Set chunk length to amount left to fill max length
|
||||
chunkLen = maxLen - fl.amtTferd
|
||||
}
|
||||
// Make data request. This will return more data from the file.
|
||||
fl.fs.request(
|
||||
FSCmdDataReq,
|
||||
false,
|
||||
byte(FSStatusOk),
|
||||
padding(2),
|
||||
fl.offset,
|
||||
chunkLen,
|
||||
)
|
||||
}
|
||||
close(fl.offsetCh)
|
||||
// Copy buffer contents to b
|
||||
copied := copy(b, buf)
|
||||
// Return amount of bytes copied with EOF error
|
||||
return copied, io.EOF
|
||||
}
|
||||
|
||||
// Write writes data from b into a file on the BLE filesysyem
|
||||
func (fl *File) Write(b []byte) (n int, err error) {
|
||||
maxLen := uint32(cap(b))
|
||||
// If file is read only (opened by FS.Open())
|
||||
if fl.isReadOnly {
|
||||
return 0, ErrFileReadOnly
|
||||
}
|
||||
|
||||
// If offset has been changed (Seek() called)
|
||||
if fl.offsetChanged {
|
||||
// Create new write file request with the specified offset to restart writing
|
||||
err := fl.fs.request(
|
||||
FSCmdWriteFile,
|
||||
true,
|
||||
uint16(len(fl.path)),
|
||||
fl.offset,
|
||||
uint64(time.Now().UnixNano()),
|
||||
fl.length,
|
||||
fl.path,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Reset offsetChanged
|
||||
fl.offsetChanged = false
|
||||
}
|
||||
|
||||
for {
|
||||
// If amount transfered equals max length
|
||||
if fl.amtTferd == maxLen {
|
||||
// Reset amount transfered
|
||||
fl.amtTferd = 0
|
||||
// Return max length with no error
|
||||
return int(maxLen), nil
|
||||
}
|
||||
|
||||
// Create new empty fileWriteResponse
|
||||
resp := fileWriteResponse{}
|
||||
// Upon receiving 0x21 (FSResponseWriteFile)
|
||||
err := fl.fs.on(FSResponseWriteFile, func(data []byte) error {
|
||||
// Read binary data into struct
|
||||
err := decode(
|
||||
data,
|
||||
&resp.status,
|
||||
&resp.padding,
|
||||
&resp.offset,
|
||||
&resp.modtime,
|
||||
&resp.free,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If status is not ok
|
||||
if resp.status != FSStatusOk {
|
||||
return FSError{resp.status}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// If no free space left in current file, break
|
||||
if resp.free == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Calculate amount of bytes to be transferred in next request
|
||||
chunkLen := min(fl.length-fl.offset, uint32(fl.fs.maxData()))
|
||||
// If after transferring, there will be more data than max length
|
||||
if fl.amtTferd+chunkLen > maxLen {
|
||||
// Set chunk length to amount left to fill max length
|
||||
chunkLen = maxLen - fl.amtTferd
|
||||
}
|
||||
// Get data from b
|
||||
chunk := b[fl.amtTferd : fl.amtTferd+chunkLen]
|
||||
// Create transfer request. This will transfer the chunk to the file.
|
||||
fl.fs.request(
|
||||
FSCmdTransfer,
|
||||
false,
|
||||
byte(FSStatusOk),
|
||||
padding(2),
|
||||
fl.offset,
|
||||
chunkLen,
|
||||
chunk,
|
||||
)
|
||||
// Add chunk length to offset and amount transferred
|
||||
fl.offset += chunkLen
|
||||
fl.offsetCh <- fl.offset
|
||||
fl.amtTferd += chunkLen
|
||||
}
|
||||
|
||||
close(fl.offsetCh)
|
||||
return int(fl.amtTferd), nil
|
||||
}
|
||||
|
||||
// WriteString converts the string to []byte and calls Write()
|
||||
func (fl *File) WriteString(s string) (n int, err error) {
|
||||
return fl.Write([]byte(s))
|
||||
}
|
||||
|
||||
// Seek changes the offset of the file being read/written.
|
||||
// This can only be done once in between reads/writes.
|
||||
func (fl *File) Seek(offset int64, whence int) (int64, error) {
|
||||
if fl.offsetChanged {
|
||||
return int64(fl.offset), ErrOffsetChanged
|
||||
}
|
||||
var newOffset int64
|
||||
switch whence {
|
||||
case io.SeekCurrent:
|
||||
newOffset = int64(fl.offset) + offset
|
||||
case io.SeekStart:
|
||||
newOffset = offset
|
||||
case io.SeekEnd:
|
||||
newOffset = int64(fl.length) + offset
|
||||
default:
|
||||
newOffset = int64(fl.offset)
|
||||
}
|
||||
if newOffset < 0 || uint32(newOffset) > fl.length {
|
||||
return int64(fl.offset), ErrInvalidOffset
|
||||
}
|
||||
fl.offset = uint32(newOffset)
|
||||
fl.offsetChanged = true
|
||||
return int64(newOffset), nil
|
||||
}
|
||||
|
||||
// Close must be called before opening another file
|
||||
func (fl *File) Close() error {
|
||||
if fl.isReadOnly {
|
||||
fl.fs.readOpen = false
|
||||
} else if fl.isWriteOnly {
|
||||
fl.fs.writeOpen = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stat does a ReadDir() and finds the current file in the output
|
||||
func (fl *File) Stat() (fs.FileInfo, error) {
|
||||
return fl.fs.Stat(fl.path)
|
||||
}
|
||||
|
||||
// fileReadResponse represents a response for a read request
|
||||
type fileReadResponse struct {
|
||||
status int8
|
||||
padding [2]byte
|
||||
offset uint32
|
||||
length uint32
|
||||
chunkLen uint32
|
||||
data []byte
|
||||
}
|
||||
|
||||
// fileWriteResponse represents a response for a write request
|
||||
type fileWriteResponse struct {
|
||||
status int8
|
||||
padding [2]byte
|
||||
offset uint32
|
||||
modtime uint64
|
||||
free uint32
|
||||
}
|
||||
|
||||
// min returns the smaller uint32 out of two given
|
||||
func min(o, t uint32) uint32 {
|
||||
if t < o {
|
||||
return t
|
||||
}
|
||||
return o
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FileInfo implements fs.FileInfo
|
||||
type FileInfo struct {
|
||||
name string
|
||||
size uint32
|
||||
modtime uint64
|
||||
mode fs.FileMode
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// Name returns the base name of the file
|
||||
func (fi FileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
|
||||
// Size returns the total size of the file
|
||||
func (fi FileInfo) Size() int64 {
|
||||
return int64(fi.size)
|
||||
}
|
||||
|
||||
// Mode returns the mode of the file
|
||||
func (fi FileInfo) Mode() fs.FileMode {
|
||||
return fi.mode
|
||||
}
|
||||
|
||||
// ModTime returns the modification time of the file
|
||||
// As of now, this is unimplemented in InfiniTime, and
|
||||
// will always return 0.
|
||||
func (fi FileInfo) ModTime() time.Time {
|
||||
return time.Unix(0, int64(fi.modtime))
|
||||
}
|
||||
|
||||
// IsDir returns whether the file is a directory
|
||||
func (fi FileInfo) IsDir() bool {
|
||||
return fi.isDir
|
||||
}
|
||||
|
||||
// Sys is unimplemented and returns nil
|
||||
func (fi FileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
-240
@@ -1,240 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/muka/go-bluetooth/bluez"
|
||||
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFSUnexpectedResponse = errors.New("unexpected response returned by filesystem")
|
||||
ErrFSResponseTimeout = errors.New("timed out waiting for response")
|
||||
ErrFSError = errors.New("error reported by filesystem")
|
||||
)
|
||||
|
||||
const (
|
||||
FSStatusOk = 0x01
|
||||
FSStatusError = 0x02
|
||||
)
|
||||
|
||||
// Filesystem command
|
||||
const (
|
||||
FSCmdReadFile = 0x10
|
||||
FSCmdDataReq = 0x12
|
||||
FSCmdWriteFile = 0x20
|
||||
FSCmdTransfer = 0x22
|
||||
FSCmdDelete = 0x30
|
||||
FSCmdMkdir = 0x40
|
||||
FSCmdListDir = 0x50
|
||||
FSCmdMove = 0x60
|
||||
)
|
||||
|
||||
// Filesystem response
|
||||
const (
|
||||
FSResponseReadFile = 0x11
|
||||
FSResponseWriteFile = 0x21
|
||||
FSResponseDelete = 0x31
|
||||
FSResponseMkdir = 0x41
|
||||
FSResponseListDir = 0x51
|
||||
FSResponseMove = 0x61
|
||||
)
|
||||
|
||||
// btOptsCmd cause a write command rather than a wrire request
|
||||
var btOptsCmd = map[string]interface{}{"type": "command"}
|
||||
|
||||
// FS implements the fs.FS interface for the Adafruit BLE FS protocol
|
||||
type FS struct {
|
||||
transferChar *gatt.GattCharacteristic1
|
||||
transferRespCh <-chan *bluez.PropertyChanged
|
||||
readOpen bool
|
||||
writeOpen bool
|
||||
}
|
||||
|
||||
// New creates a new fs given the transfer characteristic
|
||||
func New(transfer *gatt.GattCharacteristic1) (*FS, error) {
|
||||
// Create new FS instance
|
||||
out := &FS{transferChar: transfer}
|
||||
|
||||
// Start notifications on transfer characteristic
|
||||
err := out.transferChar.StartNotify()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Watch properties of transfer characteristic
|
||||
ch, err := out.transferChar.WatchProperties()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create buffered channel for propery change events
|
||||
bufCh := make(chan *bluez.PropertyChanged, 10)
|
||||
go func() {
|
||||
// Relay all messages from original channel to buffered
|
||||
for val := range ch {
|
||||
bufCh <- val
|
||||
}
|
||||
}()
|
||||
// Set transfer response channel to buffered channel
|
||||
out.transferRespCh = bufCh
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (blefs *FS) Close() error {
|
||||
return blefs.transferChar.StopNotify()
|
||||
}
|
||||
|
||||
// request makes a request on the transfer characteristic
|
||||
func (blefs *FS) request(cmd byte, padding bool, data ...interface{}) error {
|
||||
// Encode data as binary
|
||||
dataBin, err := encode(data...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bin := []byte{cmd}
|
||||
if padding {
|
||||
bin = append(bin, 0x00)
|
||||
}
|
||||
// Append encoded data to command with one byte of padding
|
||||
bin = append(bin, dataBin...)
|
||||
// Write value to characteristic
|
||||
err = blefs.transferChar.WriteValue(bin, btOptsCmd)
|
||||
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 (blefs *FS) on(resp byte, onCmdCb func(data []byte) error) error {
|
||||
// Use for loop in case of invalid property
|
||||
for {
|
||||
select {
|
||||
case propChanged := <-blefs.transferRespCh:
|
||||
// If property was invalid
|
||||
if propChanged.Name != "Value" {
|
||||
// Keep waiting
|
||||
continue
|
||||
}
|
||||
// Assert propery value as byte slice
|
||||
data := propChanged.Value.([]byte)
|
||||
// If command has prefix of given command
|
||||
if data[0] == resp {
|
||||
// Return callback with data after command
|
||||
return onCmdCb(data[1:])
|
||||
}
|
||||
return ErrFSUnexpectedResponse
|
||||
case <-time.After(time.Minute):
|
||||
return ErrFSResponseTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// encode encodes go values to binary
|
||||
func encode(data ...interface{}) ([]byte, error) {
|
||||
// Create new buffer
|
||||
buf := &bytes.Buffer{}
|
||||
// For every data element
|
||||
for _, elem := range data {
|
||||
switch val := elem.(type) {
|
||||
case string:
|
||||
// Write string to buffer
|
||||
if _, err := buf.WriteString(val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case []byte:
|
||||
// Write bytes to buffer
|
||||
if _, err := buf.Write(val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
// Encode and write value as little endian binary
|
||||
if err := binary.Write(buf, binary.LittleEndian, val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return bytes from buffer
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// decode reads binary into pointers given in vals
|
||||
func decode(data []byte, vals ...interface{}) error {
|
||||
offset := 0
|
||||
for _, elem := range vals {
|
||||
// If at end of data, stop
|
||||
if offset == len(data) {
|
||||
break
|
||||
}
|
||||
switch val := elem.(type) {
|
||||
case *string:
|
||||
// Set val to string starting from offset
|
||||
*val = string(data[offset:])
|
||||
// Add string length to offset
|
||||
offset += len(data) - offset
|
||||
case *[]byte:
|
||||
// Set val to byte slice starting from offset
|
||||
*val = data[offset:]
|
||||
// Add slice length to offset
|
||||
offset += len(data) - offset
|
||||
default:
|
||||
// Create new reader for data starting from offset
|
||||
reader := bytes.NewReader(data[offset:])
|
||||
// Read binary into value pointer
|
||||
err := binary.Read(reader, binary.LittleEndian, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Add size of value to offset
|
||||
offset += binary.Size(val)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// maxData returns MTU-20. This is the maximum amount of data
|
||||
// to send in a packet. Subtracting 20 ensures that the MTU
|
||||
// is never exceeded.
|
||||
func (blefs *FS) maxData() uint16 {
|
||||
mtu := blefs.transferChar.Properties.MTU
|
||||
// If MTU is zero, the current version of BlueZ likely
|
||||
// doesn't support the MTU property, so assume 256.
|
||||
if mtu == 0 {
|
||||
mtu = 256
|
||||
}
|
||||
return mtu - 20
|
||||
}
|
||||
|
||||
// padding returns a slice of len amount of 0x00.
|
||||
func padding(len int) []byte {
|
||||
return make([]byte, len)
|
||||
}
|
||||
|
||||
// bytesHuman returns a human-readable string for
|
||||
// the amount of bytes inputted.
|
||||
func bytesHuman(b uint32) (float64, string) {
|
||||
const unit = 1000
|
||||
// Set possible units prefixes (PineTime flash is 4MB)
|
||||
units := [2]rune{'k', 'M'}
|
||||
// If amount of bytes is less than smallest unit
|
||||
if b < unit {
|
||||
// Return unchanged with unit "B"
|
||||
return float64(b), "B"
|
||||
}
|
||||
|
||||
div, exp := uint32(unit), 0
|
||||
// Get decimal values and unit prefix index
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
|
||||
// Create string for full unit
|
||||
unitStr := string([]rune{units[exp], 'B'})
|
||||
|
||||
// Return decimal with unit string
|
||||
return float64(b) / float64(div), unitStr
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package blefs
|
||||
|
||||
import "io/fs"
|
||||
|
||||
type goFS struct {
|
||||
*FS
|
||||
}
|
||||
|
||||
func (iofs goFS) Open(path string) (fs.File, error) {
|
||||
return iofs.FS.Open(path)
|
||||
}
|
||||
|
||||
func (blefs *FS) GoFS() fs.FS {
|
||||
return goFS{blefs}
|
||||
}
|
||||
+7
-97
@@ -1,117 +1,27 @@
|
||||
package infinitime
|
||||
|
||||
import (
|
||||
"github.com/godbus/dbus/v5"
|
||||
"os/exec"
|
||||
|
||||
bt "github.com/muka/go-bluetooth/api"
|
||||
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
||||
"github.com/muka/go-bluetooth/bluez/profile/agent"
|
||||
)
|
||||
|
||||
var defaultAdapter *adapter.Adapter1
|
||||
var itdAgent *Agent
|
||||
|
||||
func Init(adapterID string) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ag := &Agent{}
|
||||
err = agent.ExposeAgent(conn, ag, agent.CapKeyboardDisplay, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if adapterID == "" {
|
||||
adapterID = bt.GetDefaultAdapterID()
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Get bluez default adapter
|
||||
da, err := bt.GetAdapter(adapterID)
|
||||
da, err := bt.GetDefaultAdapter()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Power on command (workaround as go-bluetooth does not have a power on function)
|
||||
exec.Command("bluetoothctl", "power", "on").Start()
|
||||
|
||||
defaultAdapter = da
|
||||
itdAgent = ag
|
||||
}
|
||||
|
||||
func Exit() error {
|
||||
if defaultAdapter != nil {
|
||||
defaultAdapter.Close()
|
||||
}
|
||||
agent.RemoveAgent(itdAgent)
|
||||
return bt.Exit()
|
||||
}
|
||||
|
||||
var errAuthFailed = dbus.NewError("org.bluez.Error.AuthenticationFailed", nil)
|
||||
|
||||
// Agent implements the agent.Agent1Client interface.
|
||||
// It only requires RequestPasskey as that is all InfiniTime
|
||||
// will use.
|
||||
type Agent struct {
|
||||
ReqPasskey func() (uint32, error)
|
||||
}
|
||||
|
||||
// Release returns nil
|
||||
func (*Agent) Release() *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestPinCode returns an empty string and nil
|
||||
func (*Agent) RequestPinCode(device dbus.ObjectPath) (pincode string, err *dbus.Error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// DisplayPinCode returns nil
|
||||
func (*Agent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestPasskey runs Agent.ReqPasskey and returns the result
|
||||
func (a *Agent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) {
|
||||
if a.ReqPasskey == nil {
|
||||
return 0, errAuthFailed
|
||||
}
|
||||
log.Debug("Passkey requested, calling onReqPasskey callback").Send()
|
||||
passkey, err := a.ReqPasskey()
|
||||
if err != nil {
|
||||
return 0, errAuthFailed
|
||||
}
|
||||
return passkey, nil
|
||||
}
|
||||
|
||||
// DisplayPasskey returns nil
|
||||
func (*Agent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestConfirmation returns nil
|
||||
func (*Agent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestAuthorization returns nil
|
||||
func (*Agent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeService returns nil
|
||||
func (*Agent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cancel returns nil
|
||||
func (*Agent) Cancel() *dbus.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Path returns "/ws/elara/infinitime/Agent"
|
||||
func (*Agent) Path() dbus.ObjectPath {
|
||||
return "/ws/elara/infinitime/Agent"
|
||||
}
|
||||
|
||||
// Interface returns "org.bluez.Agent1"
|
||||
func (*Agent) Interface() string {
|
||||
return "org.bluez.Agent1"
|
||||
}
|
||||
|
||||
@@ -313,7 +313,6 @@ func (dfu *DFU) Reset() {
|
||||
// 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 {
|
||||
log.Debug("Waiting for DFU command").Bytes("expecting", cmd).Send()
|
||||
// Use for loop in case of invalid property
|
||||
for {
|
||||
select {
|
||||
@@ -325,10 +324,6 @@ func (dfu *DFU) on(cmd []byte, onCmdCb func(data []byte) error) error {
|
||||
}
|
||||
// Assert propery value as byte slice
|
||||
data := propChanged.Value.([]byte)
|
||||
log.Debug("Received DFU command").
|
||||
Bytes("expecting", cmd).
|
||||
Bytes("received", data).
|
||||
Send()
|
||||
// If command has prefix of given command
|
||||
if bytes.HasPrefix(data, cmd) {
|
||||
// Return callback with data after command
|
||||
@@ -413,7 +408,6 @@ func (dfu *DFU) stepSeven() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Sent firmware image segment").Send()
|
||||
// Increment bytes sent by amount read
|
||||
dfu.bytesSent += len(segment)
|
||||
}
|
||||
@@ -421,10 +415,6 @@ func (dfu *DFU) stepSeven() error {
|
||||
err := dfu.on(DFUNotifPktRecvd, func(data []byte) error {
|
||||
// Set bytes received to data returned by InfiniTime
|
||||
dfu.bytesRecvd = int(binary.LittleEndian.Uint32(data))
|
||||
log.Debug("Received packet receipt notification").
|
||||
Int("sent", dfu.bytesSent).
|
||||
Int("rcvd", dfu.bytesRecvd).
|
||||
Send()
|
||||
if dfu.bytesRecvd != dfu.bytesSent {
|
||||
return ErrDFUSizeMismatch
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
# InfiniTime BLE Docs
|
||||
|
||||
While developing [`itd`](https://gitea.arsenm.dev/Arsen6331/itd), I noticed a lack of BLE documentation for InfiniTime. So, I decided to make some. I am not an expert at BLE and only learned how to use it through my development of `itd`. I know enough to document how to use it, but this may be revised later to add details and nuances.
|
||||
|
||||
---
|
||||
|
||||
### Table of Contents
|
||||
|
||||
- [Getting Information](#getting-information)
|
||||
- [Notifications](#notifications)
|
||||
- [Firmware Upgrades](#firmware-upgrades)
|
||||
- [Music Control](#music-control)
|
||||
- [Time](#time)
|
||||
|
||||
---
|
||||
|
||||
### Getting Information
|
||||
|
||||
The InfiniTime firmware exposes some information about itself through BLE. The BLE characteristic UUIDs for this information are as follows:
|
||||
|
||||
- Firmware Version: `00002a26-0000-1000-8000-00805f9b34fb`
|
||||
- Battery Level: `00002a19-0000-1000-8000-00805f9b34fb`
|
||||
- Heart Rate: `00002a37-0000-1000-8000-00805f9b34fb`
|
||||
|
||||
#### Firmware Version
|
||||
|
||||
Reading a value from the firmware version characteristic will yield a UTF-8 encoded string containing the version of InfiniTime being run on the device. Example: `1.6.0`.
|
||||
|
||||
#### Battery Level
|
||||
|
||||
Reading from the battery level characteristic yields a single byte of data. This byte can be converted to an unsigned 8-bit integer which will be the battery percentage. This characteristic allows notify for updates as the value changes.
|
||||
|
||||
#### Heart Rate
|
||||
|
||||
Reading from the heart rate characteristic yields two bytes of data. I am not sure of the function of the first byte. It appears to always be zero. The second byte can be converted to an unsigned 8-bit integer which is the current heart rate. This characteristic also allows notify for updates as the value changes.
|
||||
|
||||
---
|
||||
|
||||
### Notifications
|
||||
|
||||
InfiniTime uses the Alert Notification Service (ANS) for notifications. The relevant UUIDs are as follows:
|
||||
|
||||
- New Alert: `00002a46-0000-1000-8000-00805f9b34fb`
|
||||
- Notification Event: `00020001-78fc-48fe-8e23-433b3a1942d0`
|
||||
|
||||
#### New Alert
|
||||
|
||||
The new alert characteristic allows sending new notifications to InfiniTime. It requires the following format:
|
||||
|
||||
```
|
||||
<category><amount>\x00<\x00-separated data>
|
||||
```
|
||||
|
||||
For example, here is what a normal notification looks like in Golang (language of `itd`):
|
||||
|
||||
```go
|
||||
// \x00 is the category for simple alert, and there is one new notifcation, hence \x01.
|
||||
"\x00\x01\x00Test Title\x00Test Body"
|
||||
```
|
||||
|
||||
A call notification looks like so:
|
||||
|
||||
```go
|
||||
// \x03 is the category for calls, and there is one new call notifcation, hence \x01.
|
||||
"\x03\x01\x00Mary"
|
||||
```
|
||||
|
||||
The `\x00` stands for hexadecimal `00` which means null.
|
||||
|
||||
Here is the list of categories and commands:
|
||||
|
||||
- Simple Alert: `0`
|
||||
- Email: `1`
|
||||
- News: `2`
|
||||
- Call Notification: `3`
|
||||
- Missed Call: `4`
|
||||
- SMS/MMS: `5`
|
||||
- Voicemail: `6`
|
||||
- Schedule: `7`
|
||||
- High Prioritized Alert: `8`
|
||||
- Instant Message: `9`
|
||||
- All Alerts: `0xFF`
|
||||
|
||||
These lists and information were retrieved from the following pages in the Nordic docs:
|
||||
|
||||
- [Alert Notification Service Client](https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v12.2.0%2Fgroup__ble__ans__c.html)
|
||||
- [Alert Notification Application](https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v13.0.0%2Fble_sdk_app_alert_notification.html)
|
||||
|
||||
#### Notification Event
|
||||
|
||||
A call notification in InfiniTime contains three buttons. Decline, Accept, and Mute. The notification event characteristic contains the button tapped by the user on a call notification. This characteristic only allows notify, **not** read.
|
||||
|
||||
Enabling notifications from this characteristic, you get a single byte whenever the user taps a button on the call notification. This byte is an unsigned 8-bit integer that signifies one of the buttons. The numbers are as follows:
|
||||
|
||||
- 0: Declined
|
||||
- 1: Accepted
|
||||
- 2: Muted
|
||||
|
||||
---
|
||||
|
||||
### Firmware Upgrades
|
||||
|
||||
Firmware upgrades in InfiniTime are probably the most complex of the BLE operations. It is a nine step process requiring multiple commands be sent to multiple characteristics. The relevant UUIDs are as follows:
|
||||
|
||||
- Control Point: `00001531-1212-efde-1523-785feabcd123`
|
||||
- Packet: `00001532-1212-efde-1523-785feabcd123`
|
||||
|
||||
A DFU upgrade archive for InfiniTime consists of multiple files. The most important being the .bin and .dat files. The first is the actual firmware, while the second is a packet that initializes DFU. Both are needed for a DFU upgrade.
|
||||
|
||||
The first thing to do is to enable notifications on the control point characteristic. This will be needed for verifying that the proper responses are being sent back from InfiniTime.
|
||||
|
||||
#### Step one
|
||||
|
||||
For the first step, write `0x01`, `0x04` to the control point characteristic. This will signal InfiniTime that a DFU upgrade is to be started.
|
||||
|
||||
#### Step two
|
||||
|
||||
In step two, send the total size in bytes of the firmware file to the packet characteristic. This value should be an unsigned 32-bit integer encoded as little-endian. In front of this integer should be 8 null bytes. This is because there are three items that can be updated and each 4 bytes is for one of those. The last four are for the InfiniTime application, so those are the ones that need to be set.
|
||||
|
||||
#### Step three
|
||||
|
||||
Before running step three, wait for a response from the control point. This response should be `0x10`, `0x01`, `0x01` which indicates a successful DFU start. In step three, send `0x02`, `0x00` to the control point. This will signal InfiniTime to expect the init packet on the packet characteristic.
|
||||
|
||||
#### Step four
|
||||
|
||||
The previous step prepared InfiniTime for this one. In this step, send the contents of the .dat init packet file to the packet characteristic. After this, send `0x02`, `0x01` indicating that the packet has been sent.
|
||||
|
||||
#### Step five
|
||||
|
||||
Before running this step, wait to receive `0x10`, `0x02`, `0x01` which indicates that the packet has been received. During this step, send the packet receipt interval to the control point. The firmware file will be sent in segments of 20 bytes each. The packet receipt interval indicates how many segments should be received before sending a receipt containing the amount of bytes received so that it can be confirmed to be the same as the amount sent. This is very useful for detecting packet loss. `itd` uses `0x08`, `0x0A` which indicates 10 segments.
|
||||
|
||||
#### Step six
|
||||
|
||||
In step six, write `0x03` to the control point, indicating that the firmware will be sent next on the packet characteristic.
|
||||
|
||||
#### Step seven
|
||||
|
||||
This step is the most difficult. Here, the actual firmware is sent to InfiniTime.
|
||||
|
||||
As mentioned before, the firmware file must be split up into segments of 20 bytes each and sent to the packet characteristic one by one. Every 10 segments (or whatever you have set the interval to), check for a response starting with `0x11`. The rest of the response will be the amount of bytes received encoded as a little-endian unsigned 32-bit integer. Confirm that this matches the amount of bytes sent, and then continue sending more segments.
|
||||
|
||||
#### Step eight
|
||||
|
||||
Before running this step, wait to receive `0x10`, `0x03`, `0x01` which indicates a successful receipt of the firmware image. In this step, write `0x04` to the control point to signal InfiniTime to validate the image it has received.
|
||||
|
||||
#### Step nine
|
||||
|
||||
Before running this step, wait to receive `0x10`, `0x04`, `0x01` which indicates that the image has been validated. In this step, send `0x05` to the control point as a command with no response. This signals InfiniTime to activate the new firmware and reboot.
|
||||
|
||||
Once all of these steps are complete, the DFU is complete. Don't forget to validate the firmware in the settings.
|
||||
|
||||
---
|
||||
|
||||
### Music Control
|
||||
|
||||
InfiniTime contains a music controller app which is meant to control the music playback and volume through the companion.
|
||||
|
||||
The following UUIDs are relevant to this:
|
||||
|
||||
- Events: `00000001-78fc-48fe-8e23-433b3a1942d0`
|
||||
- Status: `00000002-78fc-48fe-8e23-433b3a1942d0`
|
||||
- Artist: `00000003-78fc-48fe-8e23-433b3a1942d0`
|
||||
- Track: `00000004-78fc-48fe-8e23-433b3a1942d0`
|
||||
- Album: `00000005-78fc-48fe-8e23-433b3a1942d0`
|
||||
|
||||
#### Events
|
||||
|
||||
The events characteristic is meant to respond to user input in the music controller app.
|
||||
|
||||
Enabling notifications on this characteristic gives you a single byte upon any event. This byte can be converted to an unsigned 8-bit integer which corresponds to each possible event. Here are the events:
|
||||
|
||||
- App Opened: `0xe0`
|
||||
- Play: `0x00`
|
||||
- Pause: `0x01`
|
||||
- Next: `0x03`
|
||||
- Previous: `0x04`
|
||||
- Volume up: `0x05`
|
||||
- Volume down: `0x06`
|
||||
|
||||
#### Status
|
||||
|
||||
The status characteristic allows setting the playing status of music. Send `0x01` to the status characteristic for playing, and `0x01` for paused.
|
||||
|
||||
#### Artist, Track, and Album
|
||||
|
||||
These characteristics all work the same way. Simply send a UTF-8 encoded string to the relevant characteristic in order to set the value in the app.
|
||||
|
||||
#### Time
|
||||
|
||||
InfiniTime allows setting its time via the Current Time Service (CTS)
|
||||
|
||||
The UUID for the current time characteristic is: `00002a2b-0000-1000-8000-00805f9b34fb`
|
||||
|
||||
This characteristic expects a particular format. All of the values mentioned below should be encoded as little-endian:
|
||||
|
||||
- Year (`uint16`)
|
||||
- Month (`uint8`)
|
||||
- Day (`uint8`)
|
||||
- Hour (`uint8`)
|
||||
- Minute (`uint8`)
|
||||
- Second (`uint8`)
|
||||
- Weekday (`uint8`)
|
||||
- Microsecond divided by `1e6*256` (`uint8`)
|
||||
- Binary 0001 (`uint8`)
|
||||
|
||||
Write all of these as a string of bytes to the current time characteristic.
|
||||
@@ -1,11 +1,5 @@
|
||||
module go.elara.ws/infinitime
|
||||
module go.arsenm.dev/infinitime
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/fxamacker/cbor/v2 v2.4.0
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268
|
||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
|
||||
)
|
||||
require github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d
|
||||
|
||||
@@ -3,22 +3,15 @@ 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/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
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/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ=
|
||||
github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM=
|
||||
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/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 h1:kOnq7TfaAO2Vc/MHxPqFIXe00y1qBxJAvhctXdko6vo=
|
||||
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
||||
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=
|
||||
@@ -27,21 +20,11 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
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/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae h1:d+gJUhEWSrOjrrfgeydYWEr8TTnx0DLvcVhghaOsFeE=
|
||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM=
|
||||
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=
|
||||
@@ -55,12 +38,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
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/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
@@ -69,6 +48,5 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+256
-504
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,6 @@ func (mc MusicCtrl) WatchEvents() (<-chan MusicEvent, error) {
|
||||
for event := range ch {
|
||||
// If value changes
|
||||
if event.Name == "Value" {
|
||||
log.Debug("Received music event from watch").Bytes("value", event.Value.([]byte)).Send()
|
||||
// Send music event to channel
|
||||
musicEventCh <- MusicEvent(event.Value.([]byte)[0])
|
||||
}
|
||||
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
package infinitime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
||||
)
|
||||
|
||||
var ErrNavProgress = errors.New("progress needs to be between 0 and 100")
|
||||
|
||||
const (
|
||||
NavFlagsChar = "00010001-78fc-48fe-8e23-433b3a1942d0"
|
||||
NavNarrativeChar = "00010002-78fc-48fe-8e23-433b3a1942d0"
|
||||
NavManDistChar = "00010003-78fc-48fe-8e23-433b3a1942d0"
|
||||
NavProgressChar = "00010004-78fc-48fe-8e23-433b3a1942d0"
|
||||
)
|
||||
|
||||
type NavigationService struct {
|
||||
dev *Device
|
||||
flagsChar *gatt.GattCharacteristic1
|
||||
narrativeChar *gatt.GattCharacteristic1
|
||||
mandistChar *gatt.GattCharacteristic1
|
||||
progressChar *gatt.GattCharacteristic1
|
||||
}
|
||||
|
||||
type NavFlag string
|
||||
|
||||
const (
|
||||
NavFlagArrive NavFlag = "arrive"
|
||||
NavFlagArriveLeft NavFlag = "arrive-left"
|
||||
NavFlagArriveRight NavFlag = "arrive-right"
|
||||
NavFlagArriveStraight NavFlag = "arrive-straight"
|
||||
NavFlagClose NavFlag = "close"
|
||||
NavFlagContinue NavFlag = "continue"
|
||||
NavFlagContinueLeft NavFlag = "continue-left"
|
||||
NavFlagContinueRight NavFlag = "continue-right"
|
||||
NavFlagContinueSlightLeft NavFlag = "continue-slight-left"
|
||||
NavFlagContinueSlightRight NavFlag = "continue-slight-right"
|
||||
NavFlagContinueStraight NavFlag = "continue-straight"
|
||||
NavFlagContinueUturn NavFlag = "continue-uturn"
|
||||
NavFlagDepart NavFlag = "depart"
|
||||
NavFlagDepartLeft NavFlag = "depart-left"
|
||||
NavFlagDepartRight NavFlag = "depart-right"
|
||||
NavFlagDepartStraight NavFlag = "depart-straight"
|
||||
NavFlagEndOfRoadLeft NavFlag = "end-of-road-left"
|
||||
NavFlagEndOfRoadRight NavFlag = "end-of-road-right"
|
||||
NavFlagFerry NavFlag = "ferry"
|
||||
NavFlagFlag NavFlag = "flag"
|
||||
NavFlagFork NavFlag = "fork"
|
||||
NavFlagForkLeft NavFlag = "fork-left"
|
||||
NavFlagForkRight NavFlag = "fork-right"
|
||||
NavFlagForkSlightLeft NavFlag = "fork-slight-left"
|
||||
NavFlagForkSlightRight NavFlag = "fork-slight-right"
|
||||
NavFlagForkStraight NavFlag = "fork-straight"
|
||||
NavFlagInvalid NavFlag = "invalid"
|
||||
NavFlagInvalidLeft NavFlag = "invalid-left"
|
||||
NavFlagInvalidRight NavFlag = "invalid-right"
|
||||
NavFlagInvalidSlightLeft NavFlag = "invalid-slight-left"
|
||||
NavFlagInvalidSlightRight NavFlag = "invalid-slight-right"
|
||||
NavFlagInvalidStraight NavFlag = "invalid-straight"
|
||||
NavFlagInvalidUturn NavFlag = "invalid-uturn"
|
||||
NavFlagMergeLeft NavFlag = "merge-left"
|
||||
NavFlagMergeRight NavFlag = "merge-right"
|
||||
NavFlagMergeSlightLeft NavFlag = "merge-slight-left"
|
||||
NavFlagMergeSlightRight NavFlag = "merge-slight-right"
|
||||
NavFlagMergeStraight NavFlag = "merge-straight"
|
||||
NavFlagNewNameLeft NavFlag = "new-name-left"
|
||||
NavFlagNewNameRight NavFlag = "new-name-right"
|
||||
NavFlagNewNameSharpLeft NavFlag = "new-name-sharp-left"
|
||||
NavFlagNewNameSharpRight NavFlag = "new-name-sharp-right"
|
||||
NavFlagNewNameSlightLeft NavFlag = "new-name-slight-left"
|
||||
NavFlagNewNameSlightRight NavFlag = "new-name-slight-right"
|
||||
NavFlagNewNameStraight NavFlag = "new-name-straight"
|
||||
NavFlagNotificationLeft NavFlag = "notification-left"
|
||||
NavFlagNotificationRight NavFlag = "notification-right"
|
||||
NavFlagNotificationSharpLeft NavFlag = "notification-sharp-left"
|
||||
NavFlagNotificationSharpRight NavFlag = "notification-sharp-right"
|
||||
NavFlagNotificationSlightLeft NavFlag = "notification-slight-left"
|
||||
NavFlagNotificationSlightRight NavFlag = "notification-slight-right"
|
||||
NavFlagNotificationStraight NavFlag = "notification-straight"
|
||||
NavFlagOffRampLeft NavFlag = "off-ramp-left"
|
||||
NavFlagOffRampRight NavFlag = "off-ramp-right"
|
||||
NavFlagOffRampSharpLeft NavFlag = "off-ramp-sharp-left"
|
||||
NavFlagOffRampSharpRight NavFlag = "off-ramp-sharp-right"
|
||||
NavFlagOffRampSlightLeft NavFlag = "off-ramp-slight-left"
|
||||
NavFlagOffRampSlightRight NavFlag = "off-ramp-slight-right"
|
||||
NavFlagOffRampStraight NavFlag = "off-ramp-straight"
|
||||
NavFlagOnRampLeft NavFlag = "on-ramp-left"
|
||||
NavFlagOnRampRight NavFlag = "on-ramp-right"
|
||||
NavFlagOnRampSharpLeft NavFlag = "on-ramp-sharp-left"
|
||||
NavFlagOnRampSharpRight NavFlag = "on-ramp-sharp-right"
|
||||
NavFlagOnRampSlightLeft NavFlag = "on-ramp-slight-left"
|
||||
NavFlagOnRampSlightRight NavFlag = "on-ramp-slight-right"
|
||||
NavFlagOnRampStraight NavFlag = "on-ramp-straight"
|
||||
NavFlagRotary NavFlag = "rotary"
|
||||
NavFlagRotaryLeft NavFlag = "rotary-left"
|
||||
NavFlagRotaryRight NavFlag = "rotary-right"
|
||||
NavFlagRotarySharpLeft NavFlag = "rotary-sharp-left"
|
||||
NavFlagRotarySharpRight NavFlag = "rotary-sharp-right"
|
||||
NavFlagRotarySlightLeft NavFlag = "rotary-slight-left"
|
||||
NavFlagRotarySlightRight NavFlag = "rotary-slight-right"
|
||||
NavFlagRotaryStraight NavFlag = "rotary-straight"
|
||||
NavFlagRoundabout NavFlag = "roundabout"
|
||||
NavFlagRoundaboutLeft NavFlag = "roundabout-left"
|
||||
NavFlagRoundaboutRight NavFlag = "roundabout-right"
|
||||
NavFlagRoundaboutSharpLeft NavFlag = "roundabout-sharp-left"
|
||||
NavFlagRoundaboutSharpRight NavFlag = "roundabout-sharp-right"
|
||||
NavFlagRoundaboutSlightLeft NavFlag = "roundabout-slight-left"
|
||||
NavFlagRoundaboutSlightRight NavFlag = "roundabout-slight-right"
|
||||
NavFlagRoundaboutStraight NavFlag = "roundabout-straight"
|
||||
NavFlagTurnLeft NavFlag = "turn-left"
|
||||
NavFlagTurnRight NavFlag = "turn-right"
|
||||
NavFlagTurnSharpLeft NavFlag = "turn-sharp-left"
|
||||
NavFlagTurnSharpRight NavFlag = "turn-sharp-right"
|
||||
NavFlagTurnSlightLeft NavFlag = "turn-slight-left"
|
||||
NavFlagTurnSlightRight NavFlag = "turn-slight-right"
|
||||
NavFlagTurnStright NavFlag = "turn-stright"
|
||||
NavFlagUpdown NavFlag = "updown"
|
||||
NavFlagUturn NavFlag = "uturn"
|
||||
)
|
||||
|
||||
func (n *NavigationService) SetFlag(flag NavFlag) error {
|
||||
log.Debug("Sending flag").Str("func", "SetFlag").Send()
|
||||
if err := n.dev.checkStatus(n.flagsChar, NavFlagsChar); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.flagsChar.WriteValue([]byte(flag), nil)
|
||||
}
|
||||
|
||||
func (n *NavigationService) SetNarrative(narrative string) error {
|
||||
log.Debug("Sending narrative").Str("func", "SetNarrative").Send()
|
||||
if err := n.dev.checkStatus(n.narrativeChar, NavNarrativeChar); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.narrativeChar.WriteValue([]byte(narrative), nil)
|
||||
}
|
||||
|
||||
func (n *NavigationService) SetManDist(manDist string) error {
|
||||
log.Debug("Sending maneuver distance").Str("func", "SetNarrative").Send()
|
||||
if err := n.dev.checkStatus(n.mandistChar, NavManDistChar); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.mandistChar.WriteValue([]byte(manDist), nil)
|
||||
}
|
||||
|
||||
func (n *NavigationService) SetProgress(progress uint8) error {
|
||||
log.Debug("Sending progress").Str("func", "SetNarrative").Send()
|
||||
if err := n.dev.checkStatus(n.progressChar, NavProgressChar); err != nil {
|
||||
return err
|
||||
}
|
||||
return n.progressChar.WriteValue([]byte{progress}, nil)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
|
||||
func CurrentMetadata(key string) (string, error) {
|
||||
out, err := exec.Command("playerctl", "metadata", key).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data := string(out)
|
||||
if data == "No player could handle this command" || data == "" {
|
||||
data = "Unknown " + strings.Title(key)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func CurrentStatus() (bool, error) {
|
||||
out, err := exec.Command("playerctl", "status").Output()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
data := string(out)
|
||||
return data == "Playing", nil
|
||||
}
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
package infinitime
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.elara.ws/infinitime/blefs"
|
||||
)
|
||||
|
||||
// ResourceOperation represents an operation performed during
|
||||
// resource loading
|
||||
type ResourceOperation uint8
|
||||
|
||||
const (
|
||||
// ResourceOperationUpload represents the upload phase
|
||||
// of resource loading
|
||||
ResourceOperationUpload = iota
|
||||
// ResourceOperationRemoveObsolete represents the obsolete
|
||||
// file removal phase of resource loading
|
||||
ResourceOperationRemoveObsolete
|
||||
)
|
||||
|
||||
// ResourceManifest is the structure of the resource manifest file
|
||||
type ResourceManifest struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
Obsolete []ObsoleteResource `json:"obsolete_files"`
|
||||
}
|
||||
|
||||
// Resource represents a resource entry in the manifest
|
||||
type Resource struct {
|
||||
Name string `json:"filename"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// ObsoleteResource represents an obsolete file entry in the manifest
|
||||
type ObsoleteResource struct {
|
||||
Path string `json:"path"`
|
||||
Since string `json:"since"`
|
||||
}
|
||||
|
||||
// ResourceLoadProgress contains information on the progress of
|
||||
// a resource load
|
||||
type ResourceLoadProgress struct {
|
||||
Operation ResourceOperation
|
||||
Name string
|
||||
Total int64
|
||||
Sent int64
|
||||
Err error
|
||||
}
|
||||
|
||||
// LoadResources accepts a resources zip file and a BLE FS.
|
||||
// It loads the resources from the zip onto the FS.
|
||||
func LoadResources(file *os.File, fs *blefs.FS) (<-chan ResourceLoadProgress, error) {
|
||||
out := make(chan ResourceLoadProgress, 10)
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := zip.NewReader(file, fi.Size())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := r.Open("resources.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
var manifest ResourceManifest
|
||||
err = json.NewDecoder(m).Decode(&manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Close()
|
||||
|
||||
log.Debug("Decoded manifest file").Send()
|
||||
|
||||
go func() {
|
||||
defer close(out)
|
||||
|
||||
for _, file := range manifest.Obsolete {
|
||||
name := filepath.Base(file.Path)
|
||||
|
||||
log.Debug("Removing file").Str("file", file.Path).Send()
|
||||
|
||||
err := fs.RemoveAll(file.Path)
|
||||
if err != nil {
|
||||
out <- ResourceLoadProgress{
|
||||
Name: name,
|
||||
Operation: ResourceOperationRemoveObsolete,
|
||||
Err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Removed file").Str("file", file.Path).Send()
|
||||
|
||||
out <- ResourceLoadProgress{
|
||||
Name: name,
|
||||
Operation: ResourceOperationRemoveObsolete,
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range manifest.Resources {
|
||||
src, err := r.Open(file.Name)
|
||||
if err != nil {
|
||||
out <- ResourceLoadProgress{
|
||||
Name: file.Name,
|
||||
Operation: ResourceOperationUpload,
|
||||
Err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
srcFi, err := src.Stat()
|
||||
if err != nil {
|
||||
out <- ResourceLoadProgress{
|
||||
Name: file.Name,
|
||||
Operation: ResourceOperationUpload,
|
||||
Total: srcFi.Size(),
|
||||
Err: err,
|
||||
}
|
||||
src.Close()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Making directories").Str("file", file.Path).Send()
|
||||
|
||||
err = fs.MkdirAll(filepath.Dir(file.Path))
|
||||
if err != nil {
|
||||
log.Debug("Error making directories").Err(err).Send()
|
||||
out <- ResourceLoadProgress{
|
||||
Name: file.Name,
|
||||
Operation: ResourceOperationUpload,
|
||||
Total: srcFi.Size(),
|
||||
Err: err,
|
||||
}
|
||||
src.Close()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Creating file").
|
||||
Str("file", file.Path).
|
||||
Int64("size", srcFi.Size()).
|
||||
Send()
|
||||
|
||||
dst, err := fs.Create(file.Path, uint32(srcFi.Size()))
|
||||
if err != nil {
|
||||
log.Debug("Error creating file").Err(err).Send()
|
||||
out <- ResourceLoadProgress{
|
||||
Name: file.Name,
|
||||
Operation: ResourceOperationUpload,
|
||||
Total: srcFi.Size(),
|
||||
Err: err,
|
||||
}
|
||||
src.Close()
|
||||
return
|
||||
}
|
||||
|
||||
progCh := dst.Progress()
|
||||
go func() {
|
||||
for sent := range progCh {
|
||||
log.Debug("Progress event sent").
|
||||
Int64("total", srcFi.Size()).
|
||||
Uint32("sent", sent).
|
||||
Send()
|
||||
|
||||
out <- ResourceLoadProgress{
|
||||
Name: file.Name,
|
||||
Operation: ResourceOperationUpload,
|
||||
Total: srcFi.Size(),
|
||||
Sent: int64(sent),
|
||||
}
|
||||
|
||||
if sent == uint32(srcFi.Size()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
n, err := io.Copy(dst, src)
|
||||
if err != nil {
|
||||
log.Debug("Error writing to file").Err(err).Send()
|
||||
out <- ResourceLoadProgress{
|
||||
Name: file.Name,
|
||||
Operation: ResourceOperationUpload,
|
||||
Total: srcFi.Size(),
|
||||
Sent: n,
|
||||
Err: err,
|
||||
}
|
||||
src.Close()
|
||||
dst.Close()
|
||||
return
|
||||
}
|
||||
|
||||
src.Close()
|
||||
dst.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package weather
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Type of weather event
|
||||
type EventType uint8
|
||||
|
||||
// These event types correspond to structs
|
||||
// that can be used to add an event to the
|
||||
// weather timeline
|
||||
const (
|
||||
EventTypeObscuration EventType = iota
|
||||
EventTypePrecipitation
|
||||
EventTypeWind
|
||||
EventTypeTemperature
|
||||
EventTypeAirQuality
|
||||
EventTypeSpecial
|
||||
EventTypePressure
|
||||
EventTypeLocation
|
||||
EventTypeClouds
|
||||
EventTypeHumidity
|
||||
)
|
||||
|
||||
// Special event type
|
||||
type SpecialType uint8
|
||||
|
||||
// See https://git.io/JM7Oe for the meaning of each type
|
||||
const (
|
||||
SpecialTypeSquall SpecialType = iota
|
||||
SpecialTypeTsunami
|
||||
SpecialTypeTornado
|
||||
SpecialTypeFire
|
||||
SpecialTypeThunder
|
||||
)
|
||||
|
||||
// Precipitation type
|
||||
type PrecipitationType uint8
|
||||
|
||||
// See https://git.io/JM7YM for the meaning of each type
|
||||
const (
|
||||
PrecipitationTypeNone PrecipitationType = iota
|
||||
PrecipitationTypeRain
|
||||
PrecipitationTypeDrizzle
|
||||
PrecipitationTypeFreezingRain
|
||||
PrecipitationTypeSleet
|
||||
PrecipitationTypeHail
|
||||
PrecipitationTypeSmallHail
|
||||
PrecipitationTypeSnow
|
||||
PrecipitationTypeSnowGrains
|
||||
PrecipitationTypeIceCrystals
|
||||
PrecipitationTypeAsh
|
||||
)
|
||||
|
||||
// Visibility obscuration type
|
||||
type ObscurationType uint8
|
||||
|
||||
// See https://git.io/JM7Yd for the meaning of each type
|
||||
const (
|
||||
ObscurationTypeNone ObscurationType = iota
|
||||
ObscurationTypeFog
|
||||
ObscurationTypeHaze
|
||||
ObscurationTypeSmoke
|
||||
ObscurationTypeAsh
|
||||
ObscurationTypeDust
|
||||
ObscurationTypeSand
|
||||
ObscurationTypeMist
|
||||
ObscurationTypePrecipitation
|
||||
)
|
||||
|
||||
// TimelineHeader contains the header for a timeline envent
|
||||
type TimelineHeader struct {
|
||||
// UNIX timestamp with timezone offset
|
||||
Timestamp uint64
|
||||
// Seconds until the event expires
|
||||
Expires uint32
|
||||
// Type of weather event
|
||||
EventType EventType
|
||||
}
|
||||
|
||||
// NewHeader creates and populates a new timeline header
|
||||
// and returns it
|
||||
func NewHeader(t time.Time, evtType EventType, expires time.Duration) TimelineHeader {
|
||||
_, offset := t.Zone()
|
||||
t = t.Add(time.Duration(offset) * time.Second)
|
||||
|
||||
return TimelineHeader{
|
||||
Timestamp: uint64(t.Unix()),
|
||||
Expires: uint32(expires.Seconds()),
|
||||
EventType: evtType,
|
||||
}
|
||||
}
|
||||
|
||||
// CloudsEvent corresponds to EventTypeClouds
|
||||
type CloudsEvent struct {
|
||||
TimelineHeader
|
||||
// Cloud coverage percentage
|
||||
Amount uint8
|
||||
}
|
||||
|
||||
// ObscurationEvent corresponds to EventTypeObscuration
|
||||
type ObscurationEvent struct {
|
||||
TimelineHeader
|
||||
// Type of obscuration
|
||||
Type ObscurationType
|
||||
// Visibility in meters. 65535 is unspecified.
|
||||
Amount uint16
|
||||
}
|
||||
|
||||
// PrecipitationEvent corresponds to EventTypePrecipitation
|
||||
type PrecipitationEvent struct {
|
||||
TimelineHeader
|
||||
// Type of precipitation
|
||||
Type PrecipitationType
|
||||
// Amount of rain in millimeters. 255 is unspecified.
|
||||
Amount uint8
|
||||
}
|
||||
|
||||
// WindEvent corresponds to EventTypeWind
|
||||
type WindEvent struct {
|
||||
TimelineHeader
|
||||
// Minimum speed in meters per second
|
||||
SpeedMin uint8
|
||||
// Maximum speed in meters per second
|
||||
SpeedMax uint8
|
||||
// Unitless direction, about 1 unit per 0.71 degrees.
|
||||
DirectionMin uint8
|
||||
// Unitless direction, about 1 unit per 0.71 degrees
|
||||
DirectionMax uint8
|
||||
}
|
||||
|
||||
// TemperatureEvent corresponds to EventTypeTemperature
|
||||
type TemperatureEvent struct {
|
||||
TimelineHeader
|
||||
// Temperature in celcius multiplied by 100.
|
||||
// -32768 is "no data"
|
||||
Temperature int16
|
||||
// Dew point in celcius multiplied by 100.
|
||||
// -32768 is "no data"
|
||||
DewPoint int16
|
||||
}
|
||||
|
||||
// LocationEvent corresponds to EventTypeLocation
|
||||
type LocationEvent struct {
|
||||
TimelineHeader
|
||||
// Location name
|
||||
Location string
|
||||
// Altitude from sea level in meters
|
||||
Altitude int16
|
||||
// EPSG:3857 latitude (Google Maps, Openstreetmaps)
|
||||
Latitude int32
|
||||
// EPSG:3857 longitude (Google Maps, Openstreetmaps)
|
||||
Longitude int32
|
||||
}
|
||||
|
||||
// HumidityEvent corresponds to EventTypeHumidity
|
||||
type HumidityEvent struct {
|
||||
TimelineHeader
|
||||
// Relative humidity percentage
|
||||
Humidity uint8
|
||||
}
|
||||
|
||||
// PressureEvent corresponds to EventTypePressure
|
||||
type PressureEvent struct {
|
||||
TimelineHeader
|
||||
// Air pressure in hectopascals (hPa)
|
||||
Pressure int16
|
||||
}
|
||||
|
||||
// SpecialEvent corresponds to EventTypeSpecial
|
||||
type SpecialEvent struct {
|
||||
TimelineHeader
|
||||
// Type of special event
|
||||
Type SpecialType
|
||||
}
|
||||
|
||||
// AirQualityEvent corresponds to EventTypeAirQuality
|
||||
type AirQualityEvent struct {
|
||||
TimelineHeader
|
||||
// Name of the polluting item
|
||||
//
|
||||
// Do not localize the name. That should be handled by the watch.
|
||||
//
|
||||
// For particulate matter, use "PM0.1"`, "PM5", or "PM10".
|
||||
//
|
||||
// For chemicals, use the molecular formula ("NO2", "CO2", or "O3").
|
||||
//
|
||||
// For pollen, use the genus of the plant.
|
||||
Polluter string
|
||||
// Amount of pollution in SI units
|
||||
//
|
||||
// https://ec.europa.eu/environment/air/quality/standards.htm
|
||||
// http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf
|
||||
Amount uint32
|
||||
}
|
||||
Reference in New Issue
Block a user