Compare commits

...

No commits in common. "master" and "master" have entirely different histories.

54 changed files with 2898 additions and 558 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
liberapay: Elara6331

View File

@ -14,6 +14,7 @@ builds:
- amd64
- arm
- arm64
- riscv64
goarm:
- 7
- id: itctl
@ -43,6 +44,7 @@ archives:
- README.md
- itd.toml
- itd.service
allow_different_binary_count: true
nfpms:
- id: itd
file_name_template: >-
@ -53,8 +55,8 @@ nfpms:
{{- else }}{{.Arch}}
{{- end }}
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
homepage: 'https://gitea.arsenm.dev/Arsen6331/itd'
maintainer: 'Arsen Musyaelyan <arsen@arsenm.dev>'
homepage: 'https://gitea.elara.ws/Elara6331/itd'
maintainer: 'Elara Ivy <elara@elara.ws>'
license: GPLv3
formats:
- apk
@ -74,10 +76,10 @@ nfpms:
mode: 0755
aurs:
- name: itd-bin
homepage: 'https://gitea.arsenm.dev/Arsen6331/itd'
homepage: 'https://gitea.elara.ws/Elara6331/itd'
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
maintainers:
- 'Arsen Musyaelyan <arsen@arsenm.dev>'
- 'Elara Ivy <elara@elara.ws>'
license: GPLv3
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://aur@aur.archlinux.org/itd-bin.git'
@ -105,11 +107,11 @@ aurs:
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/itd/LICENSE"
release:
gitea:
owner: Arsen6331
owner: Elara6331
name: itd
gitea_urls:
api: 'https://gitea.arsenm.dev/api/v1/'
download: 'https://gitea.arsenm.dev'
api: 'https://gitea.elara.ws/api/v1/'
download: 'https://gitea.elara.ws'
skip_tls_verify: false
checksum:
name_template: 'checksums.txt'

View File

@ -1,11 +1,17 @@
# ITD
## InfiniTime Daemon
`itd` is a daemon that uses my infinitime [library](https://go.arsenm.dev/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io).
`itd` is a daemon that uses my infinitime [library](https://go.elara.ws/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io).
[![status-badge](https://ci.arsenm.dev/api/badges/Arsen6331/itd/status.svg)](https://ci.arsenm.dev/Arsen6331/itd)
[![status-badge](https://ci.elara.ws/api/badges/Elara6331/itd/status.svg)](https://ci.elara.ws/Elara6331/itd)
[![itd-git AUR package](https://img.shields.io/aur/version/itd-git?label=itd-git&logo=archlinux)](https://aur.archlinux.org/packages/itd-git/)
[![itd-bin AUR package](https://img.shields.io/aur/version/itd-bin?label=itd-bin&logo=archlinux)](https://aur.archlinux.org/packages/itd-bin/)
[![LURE badge for itd-git](https://lure.sh/pkg/default/itd-git/badge.svg)](https://lure.sh/pkg/default/itd-git)
[![LURE badge for itd-bin](https://lure.sh/pkg/default/itd-bin/badge.svg)](https://lure.sh/pkg/default/itd-bin)
This repository is part of the Software Heritage Archive:
[![SWH](https://archive.softwareheritage.org/badge/swh:1:dir:1374aa47b5c0a0d636d6f9c69f77af5e5bae99b2/)](https://archive.softwareheritage.org/swh:1:dir:1374aa47b5c0a0d636d6f9c69f77af5e5bae99b2;origin=https://gitea.elara.ws/Elara6331/itd;visit=swh:1:snp:d2935acbc966dfe1b15c771927bb08b5fc2ec89f;anchor=swh:1:rev:395cded9758dccc020fcd5b666f83a62308c9ab7)
---
@ -36,19 +42,19 @@ Use the `itd-bin` or `itd-git` AUR packages.
#### Debian/Ubuntu
- Go to the [latest release](https://gitea.arsenm.dev/Arsen6331/itd/releases/latest) and download the `.deb` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
- Go to the [latest release](https://gitea.elara.ws/Elara6331/itd/releases/latest) and download the `.deb` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
- Run `sudo apt install <package>`, replacing `<package>` with the path to the downloaded file. Note: relative paths must begin with `./`.
- Example: `sudo apt install ~/Downloads/itd-0.0.7-linux-aarch64.deb`
#### Fedora
- Go to the [latest release](https://gitea.arsenm.dev/Arsen6331/itd/releases/latest) and download the `.rpm` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
- Go to the [latest release](https://gitea.elara.ws/Elara6331/itd/releases/latest) and download the `.rpm` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
- Run `sudo dnf install <package>`, replacing `<package>` with the path to the downloaded file.
- Example: `sudo dnf install ~/Downloads/itd-0.0.7-linux-aarch64.rpm`
#### Alpine (and postmarketOS)
- Go to the [latest release](https://gitea.arsenm.dev/Arsen6331/itd/releases/latest) and download the `.apk` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
- Go to the [latest release](https://gitea.elara.ws/Elara6331/itd/releases/latest) and download the `.apk` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
- Run `sudo apk add --allow-untrusted <package>`, replacing `<package>` with the path to the downloaded file.
- Example: `sudo apk add --allow-untrusted ~/Downloads/itd-0.0.7-linux-aarch64.apk`
@ -91,11 +97,11 @@ In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [Fyn
#### Easy Installation
The easiest way to install `itgui` is to use my other project, [LURE](https://gitea.arsenm.dev/Arsen6331/lure). LURE will only work if your package manager is `apt`, `dnf`, `yum`, `zypper`, `pacman`, or `apk`.
The easiest way to install `itgui` is to use my other project, [LURE](https://gitea.elara.ws/Elara6331/lure). LURE will only work if your package manager is `apt`, `dnf`, `yum`, `zypper`, `pacman`, or `apk`.
Instructions:
1. Install LURE. This can be done with the following command: `curl https://www.arsenm.dev/lure.sh | bash`.
1. Install LURE. This can be done with the following command: `curl https://www.elara.ws/lure.sh | bash`.
2. Check to make sure LURE is properly installed by running `lure ref`.
3. Run `lure in itgui`. This process may take a while as it will compile `itgui` from source and package it for your distro.
4. Once the process is complete, you should be able to open and use `itgui` like any other app.
@ -138,17 +144,6 @@ Due to the use of OpenGL, cross-compilation of `itgui` isn't as simple as that o
---
### Installation
To install, install the go compiler and make. Usually, go is provided by a package either named `go` or `golang`, and make is usually provided by `make`. The go compiler must be version 1.17 or newer for various new `reflect` features.
To install, run
```shell
make && sudo make install
```
---
### Socket
This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directly control the daemon and, by extension, the connected watch.

View File

@ -4,8 +4,8 @@ import (
"io"
"net"
"go.arsenm.dev/drpc/muxconn"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/drpc/muxconn"
"go.elara.ws/itd/internal/rpc"
"storj.io/drpc"
)

View File

@ -3,7 +3,7 @@ package api
import (
"context"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
type DFUProgress struct {

View File

@ -5,7 +5,7 @@ import (
"errors"
"io"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
type FSClient struct {
@ -42,7 +42,10 @@ func (c *FSClient) Mkdir(ctx context.Context, paths ...string) error {
func (c *FSClient) ReadDir(ctx context.Context, dir string) ([]FileInfo, error) {
res, err := c.client.ReadDir(ctx, &rpc.PathRequest{Path: dir})
return convertEntries(res.Entries), err
if err != nil {
return nil, err
}
return convertEntries(res.Entries), nil
}
func convertEntries(e []*rpc.FileInfo) []FileInfo {

View File

@ -3,7 +3,7 @@ package api
import (
"context"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) HeartRate(ctx context.Context) (uint8, error) {

View File

@ -3,7 +3,7 @@ package api
import (
"context"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) Notify(ctx context.Context, title, body string) error {

View File

@ -3,15 +3,15 @@ package api
import (
"context"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/internal/rpc"
)
type ResourceOperation uint8
type ResourceOperation infinitime.ResourceOperation
const (
ResourceOperationRemoveObsolete = infinitime.ResourceOperationRemoveObsolete
ResourceOperationUpload = infinitime.ResourceOperationUpload
ResourceRemove = infinitime.ResourceRemove
ResourceUpload = infinitime.ResourceUpload
)
type ResourceLoadProgress struct {

View File

@ -4,7 +4,7 @@ import (
"context"
"time"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) SetTime(ctx context.Context, t time.Time) error {

View File

@ -3,7 +3,7 @@ package api
import (
"context"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) WeatherUpdate(ctx context.Context) error {

View File

@ -3,7 +3,7 @@ package api
import (
"context"
"go.arsenm.dev/itd/internal/rpc"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {

View File

@ -2,12 +2,11 @@ package main
import (
"context"
"sync"
"github.com/godbus/dbus/v5"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/utils"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/internal/utils"
"go.elara.ws/logger/log"
)
func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
@ -50,7 +49,6 @@ func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) e
// Notify channel upon received message
monitorConn.Eavesdrop(callCh)
var respHandlerOnce sync.Once
var callObj dbus.BusObject
wg.Add(1)
@ -71,34 +69,40 @@ func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) e
continue
}
// Send call notification to InfiniTime
resCh, err := dev.NotifyCall(phoneNum)
// Get direction of call object using method call connection
direction, err := getDirection(conn, callObj)
if err != nil {
log.Error("Error getting call direction").Err(err).Send()
continue
}
go respHandlerOnce.Do(func() {
// Wait for PineTime response
for res := range resCh {
switch res {
case infinitime.CallStatusAccepted:
// Attempt to accept call
err = acceptCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error accepting call").Err(err).Send()
}
case infinitime.CallStatusDeclined:
// Attempt to decline call
err = declineCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error declining call").Err(err).Send()
}
case infinitime.CallStatusMuted:
// Warn about unimplemented muting
log.Warn("Muting calls is not implemented").Send()
if direction != MMCallDirectionIncoming {
continue
}
// Send call notification to InfiniTime
err = dev.NotifyCall(phoneNum, func(cs infinitime.CallStatus) {
switch cs {
case infinitime.CallStatusAccepted:
// Attempt to accept call
err = acceptCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error accepting call").Err(err).Send()
}
case infinitime.CallStatusDeclined:
// Attempt to decline call
err = declineCall(ctx, conn, callObj)
if err != nil {
log.Warn("Error declining call").Err(err).Send()
}
case infinitime.CallStatusMuted:
// Warn about unimplemented muting
log.Warn("Muting calls is not implemented").Send()
}
})
if err != nil {
continue
}
case <-ctx.Done():
return
}
@ -131,6 +135,25 @@ func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
return out, nil
}
type MMCallDirection int
const (
MMCallDirectionUnknown MMCallDirection = iota
MMCallDirectionIncoming
MMCallDirectionOutgoing
)
// getDirection gets the direction of a call object using a DBus connection
func getDirection(conn *dbus.Conn, callObj dbus.BusObject) (MMCallDirection, error) {
var out MMCallDirection
// Get number property on DBus object and store return value in out
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Direction", &out)
if err != nil {
return 0, err
}
return out, nil
}
// getPhoneNum accepts a call using a DBus connection
func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
// Call Accept() method on DBus object

View File

@ -7,8 +7,8 @@ import (
"github.com/cheggaaa/pb/v3"
"github.com/urfave/cli/v2"
"go.arsenm.dev/itd/api"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/api"
"go.elara.ws/logger/log"
)
func fwUpgrade(c *cli.Context) error {

View File

@ -8,9 +8,9 @@ import (
"time"
"github.com/urfave/cli/v2"
"go.arsenm.dev/itd/api"
"go.arsenm.dev/logger"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/api"
"go.elara.ws/logger"
"go.elara.ws/logger/log"
)
var client *api.Client

View File

@ -6,7 +6,7 @@ import (
"github.com/cheggaaa/pb/v3"
"github.com/urfave/cli/v2"
"go.arsenm.dev/infinitime"
"go.elara.ws/itd/infinitime"
)
func resourcesLoad(c *cli.Context) error {
@ -39,7 +39,7 @@ func resLoad(ctx context.Context, args []string) error {
return evt.Err
}
if evt.Operation == infinitime.ResourceOperationRemoveObsolete {
if evt.Operation == infinitime.ResourceRemove {
bar.SetTemplateString(rmTmpl)
bar.Set("filename", evt.Name)
} else {

View File

@ -9,7 +9,7 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
)
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {

View File

@ -11,8 +11,8 @@ import (
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
"go.elara.ws/itd/infinitime"
)
func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan struct{}) fyne.CanvasObject {
@ -77,9 +77,9 @@ func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan s
for evt := range progCh {
switch evt.Operation {
case infinitime.ResourceOperationRemoveObsolete:
case infinitime.ResourceRemove:
progressDlg.SetText("Removing " + evt.Name)
case infinitime.ResourceOperationUpload:
case infinitime.ResourceUpload:
progressDlg.SetText("Uploading " + evt.Name)
progressDlg.SetTotal(float64(evt.Total))
progressDlg.SetValue(float64(evt.Sent))

View File

@ -11,7 +11,7 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/x/fyne/widget/charts"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
_ "modernc.org/sqlite"
)

View File

@ -6,7 +6,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
)
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {

View File

@ -6,7 +6,7 @@ import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
)
func main() {

View File

@ -7,7 +7,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
)
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {

View File

@ -7,7 +7,7 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
)
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {

View File

@ -8,7 +8,7 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
"go.elara.ws/itd/api"
)
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {

View File

@ -9,8 +9,8 @@ import (
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"go.arsenm.dev/logger"
"go.arsenm.dev/logger/log"
"go.elara.ws/logger"
"go.elara.ws/logger/log"
)
var cfgDir string

View File

@ -6,9 +6,9 @@ import (
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/fusefs"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/internal/fusefs"
"go.elara.ws/logger/log"
)
func startFUSE(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {

21
go.mod
View File

@ -1,9 +1,11 @@
module go.arsenm.dev/itd
module go.elara.ws/itd
go 1.18
replace fyne.io/x/fyne => github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb
replace tinygo.org/x/bluetooth => github.com/elara6331/bluetooth v0.9.1-0.20240413234149-a0e71474a768
require (
fyne.io/fyne/v2 v2.3.0
fyne.io/x/fyne v0.0.0-20220107050838-c4a1de51d4ce
@ -15,13 +17,13 @@ require (
github.com/mattn/go-isatty v0.0.17
github.com/mozillazg/go-pinyin v0.19.0
github.com/urfave/cli/v2 v2.23.7
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae
golang.org/x/text v0.5.0
google.golang.org/protobuf v1.28.1
modernc.org/sqlite v1.20.1
storj.io/drpc v0.0.32
tinygo.org/x/bluetooth v0.9.0
)
require (
@ -32,15 +34,14 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fredbi/uri v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/fyne-io/gl-js v0.0.0-20220802150000-8e339395f381 // indirect
github.com/fyne-io/glfw-js v0.0.0-20220517201726-bebc2019cd33 // indirect
github.com/fyne-io/image v0.0.0-20221020213044-f609c6a24345 // indirect
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-text/typesetting v0.0.0-20221219135543-5d0d724ee181 // indirect
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
github.com/google/uuid v1.3.0 // indirect
@ -54,18 +55,18 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/tinygo-org/cbgo v0.0.4 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yuin/goldmark v1.5.3 // indirect
@ -74,7 +75,7 @@ require (
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/tools v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/js/dom v0.0.0-20221001195520-26252dedbe70 // indirect

41
go.sum
View File

@ -107,6 +107,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
github.com/elara6331/bluetooth v0.9.1-0.20240413234149-a0e71474a768 h1:iWP52WinMhd+pQB+2GedWvUxkd4pMqFvV0S6MjMFQSc=
github.com/elara6331/bluetooth v0.9.1-0.20240413234149-a0e71474a768/go.mod h1:V9XwH/xQ2SmCIW+T0pmpL7VzijY53JRVsJcDM0YN6PI=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -119,7 +121,6 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
github.com/fredbi/uri v0.1.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
@ -129,8 +130,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
github.com/fyne-io/gl-js v0.0.0-20220802150000-8e339395f381 h1:SFtj9yo9C7F4CxyJeSJi9AjT6x9c88gnY1tjlXWh9QU=
github.com/fyne-io/gl-js v0.0.0-20220802150000-8e339395f381/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
@ -161,15 +160,14 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-text/typesetting v0.0.0-20221212183139-1eb938670a1f/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI=
github.com/go-text/typesetting v0.0.0-20221219135543-5d0d724ee181 h1:J6XG/Xx7uCCpskM71R6YAgPHd/E8FzhyPhL6Ll94uMY=
github.com/go-text/typesetting v0.0.0-20221219135543-5d0d724ee181/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -240,7 +238,6 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -398,8 +395,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mozillazg/go-pinyin v0.19.0 h1:p+J8/kjJ558KPvVGYLvqBhxf8jbZA2exSLCs2uUVN8c=
github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
@ -411,7 +406,6 @@ github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnu
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
@ -456,6 +450,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4 h1:zurEWtOr/OYiTb5bcD7eeHLOfj6vCR30uldlwse1cSM=
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
@ -463,10 +459,11 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@ -501,17 +498,16 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
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/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@ -529,12 +525,10 @@ github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5ta
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794 h1:8KQpRoQmCTgDvyHFStaIiz5NUNNqMHqVlZoGvIk+OwQ=
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794/go.mod h1:K5cFls42m5q1RIphTVojRdXLaoCknq/kBqQt8Ow3XuA=
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469 h1:LsJHg+8rQSYnTE1sSCjBCACxUUVMZIOQani8J6wF2/E=
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469/go.mod h1:scUyDmLmCHn6CanGbau8yjTjzyhUbLJcsjmDCCKMIII=
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df h1:8mBHvEe7BJmpOeKSMA5YLqrGo9dCpePocTeR0C1+/2w=
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df/go.mod h1:RV2qydKDdoyaRkhAq8JEGvojR8eJ6bjq5WnSIlH7gYw=
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d h1:ANb8YPtcxPipwKgmnW688e5PGpNaLh+22nO2LBpIPOU=
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d/go.mod h1:NDprjiVqKXQKVGzX7jp2g/jctsUbvOxz1nN15QOBEGk=
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=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
@ -726,7 +720,6 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -748,7 +741,6 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -756,8 +748,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -820,7 +812,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=

142
infinitime/chars.go Normal file
View File

@ -0,0 +1,142 @@
package infinitime
import "tinygo.org/x/bluetooth"
type btChar struct {
Name string
ID bluetooth.UUID
ServiceID bluetooth.UUID
}
var (
musicServiceUUID = mustParse("00000000-78fc-48fe-8e23-433b3a1942d0")
navigationServiceUUID = mustParse("00010000-78fc-48fe-8e23-433b3a1942d0")
motionServiceUUID = mustParse("00030000-78fc-48fe-8e23-433b3a1942d0")
weatherServiceUUID = mustParse("00050000-78fc-48fe-8e23-433b3a1942d0")
)
var (
newAlertChar = btChar{
"New Alert",
bluetooth.CharacteristicUUIDNewAlert,
bluetooth.ServiceUUIDAlertNotification,
}
notifEventChar = btChar{
"Notification Event",
mustParse("00020001-78fc-48fe-8e23-433b3a1942d0"),
bluetooth.ServiceUUIDAlertNotification,
}
stepCountChar = btChar{
"Step Count",
mustParse("00030001-78fc-48fe-8e23-433b3a1942d0"),
motionServiceUUID,
}
rawMotionChar = btChar{
"Raw Motion",
mustParse("00030002-78fc-48fe-8e23-433b3a1942d0"),
motionServiceUUID,
}
firmwareVerChar = btChar{
"Firmware Version",
bluetooth.CharacteristicUUIDFirmwareRevisionString,
bluetooth.ServiceUUIDDeviceInformation,
}
currentTimeChar = btChar{
"Current Time",
bluetooth.CharacteristicUUIDCurrentTime,
bluetooth.ServiceUUIDCurrentTime,
}
localTimeChar = btChar{
"Local Time",
bluetooth.CharacteristicUUIDLocalTimeInformation,
bluetooth.ServiceUUIDCurrentTime,
}
batteryLevelChar = btChar{
"Battery Level",
bluetooth.CharacteristicUUIDBatteryLevel,
bluetooth.ServiceUUIDBattery,
}
heartRateChar = btChar{
"Heart Rate",
bluetooth.CharacteristicUUIDHeartRateMeasurement,
bluetooth.ServiceUUIDHeartRate,
}
fsVersionChar = btChar{
"Filesystem Version",
mustParse("adaf0200-4669-6c65-5472-616e73666572"),
bluetooth.ServiceUUIDFileTransferByAdafruit,
}
fsTransferChar = btChar{
"Filesystem Transfer",
mustParse("adaf0200-4669-6c65-5472-616e73666572"),
bluetooth.ServiceUUIDFileTransferByAdafruit,
}
dfuCtrlPointChar = btChar{
"DFU Control Point",
bluetooth.CharacteristicUUIDLegacyDFUControlPoint,
bluetooth.ServiceUUIDLegacyDFU,
}
dfuPacketChar = btChar{
"DFU Packet",
bluetooth.CharacteristicUUIDLegacyDFUPacket,
bluetooth.ServiceUUIDLegacyDFU,
}
navigationFlagsChar = btChar{
"Navigation Flags",
mustParse("00010001-78fc-48fe-8e23-433b3a1942d0"),
navigationServiceUUID,
}
navigationNarrativeChar = btChar{
"Navigation Narrative",
mustParse("00010002-78fc-48fe-8e23-433b3a1942d0"),
navigationServiceUUID,
}
navigationManDist = btChar{
"Navigation Man Dist",
mustParse("00010003-78fc-48fe-8e23-433b3a1942d0"),
navigationServiceUUID,
}
navigationProgress = btChar{
"Navigation Progress",
mustParse("00010004-78fc-48fe-8e23-433b3a1942d0"),
navigationServiceUUID,
}
weatherDataChar = btChar{
"Weather Data",
mustParse("00050001-78fc-48fe-8e23-433b3a1942d0"),
weatherServiceUUID,
}
musicEventChar = btChar{
"Music Event",
mustParse("00000001-78fc-48fe-8e23-433b3a1942d0"),
musicServiceUUID,
}
musicStatusChar = btChar{
"Music Status",
mustParse("00000002-78fc-48fe-8e23-433b3a1942d0"),
musicServiceUUID,
}
musicArtistChar = btChar{
"Music Artist",
mustParse("00000003-78fc-48fe-8e23-433b3a1942d0"),
musicServiceUUID,
}
musicTrackChar = btChar{
"Music Track",
mustParse("00000004-78fc-48fe-8e23-433b3a1942d0"),
musicServiceUUID,
}
musicAlbumChar = btChar{
"Music Album",
mustParse("00000005-78fc-48fe-8e23-433b3a1942d0"),
musicServiceUUID,
}
)
func mustParse(s string) bluetooth.UUID {
uuid, err := bluetooth.ParseUUID(s)
if err != nil {
panic(err)
}
return uuid
}

222
infinitime/dfu.go Normal file
View File

@ -0,0 +1,222 @@
package infinitime
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"tinygo.org/x/bluetooth"
)
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}
dfuCmdRecvFirmware = []byte{0x03}
dfuCmdValidate = []byte{0x04}
dfuCmdActivateReset = []byte{0x05}
dfuResponseStart = []byte{0x10, 0x01, 0x01}
dfuResponseInitParams = []byte{0x10, 0x02, 0x01}
dfuResponseRecvFwImgSuccess = []byte{0x10, 0x03, 0x01}
dfuResponseValidate = []byte{0x10, 0x04, 0x01}
)
// DFUOptions contains options for [UpgradeFirmware]
type DFUOptions struct {
InitPacket fs.File
FirmwareImage fs.File
ProgressFunc func(sent, received, total uint32)
SegmentSize int
ReceiveInterval uint8
}
// UpgradeFirmware upgrades the firmware running on the PineTime.
func (d *Device) UpgradeFirmware(opts DFUOptions) error {
if opts.SegmentSize <= 0 {
opts.SegmentSize = dfuSegmentSize
}
if opts.ReceiveInterval <= 0 {
opts.ReceiveInterval = dfuPktRecvInterval
}
ctrlPoint, err := d.getChar(dfuCtrlPointChar)
if err != nil {
return err
}
packet, err := d.getChar(dfuPacketChar)
if err != nil {
return err
}
d.deviceMtx.Lock()
defer d.deviceMtx.Unlock()
d.updating.Store(true)
defer d.updating.Store(false)
_, err = ctrlPoint.WriteWithoutResponse(dfuCmdStart)
if err != nil {
return err
}
fi, err := opts.FirmwareImage.Stat()
if err != nil {
return err
}
size := uint32(fi.Size())
sizePacket := make([]byte, 8, 12)
sizePacket = binary.LittleEndian.AppendUint32(sizePacket, size)
_, err = packet.WriteWithoutResponse(sizePacket)
if err != nil {
return err
}
_, err = awaitDFUResponse(ctrlPoint, dfuResponseStart)
if err != nil {
return err
}
err = writeDFUInitPacket(ctrlPoint, packet, opts.InitPacket)
if err != nil {
return err
}
err = setRecvInterval(ctrlPoint, opts.ReceiveInterval)
if err != nil {
return err
}
err = sendFirmware(ctrlPoint, packet, opts, size)
if err != nil {
return err
}
return finalize(ctrlPoint)
}
func finalize(ctrlPoint *bluetooth.DeviceCharacteristic) error {
_, err := ctrlPoint.WriteWithoutResponse(dfuCmdValidate)
if err != nil {
return err
}
_, err = awaitDFUResponse(ctrlPoint, dfuResponseValidate)
if err != nil {
return err
}
_, _ = ctrlPoint.WriteWithoutResponse(dfuCmdActivateReset)
return nil
}
func sendFirmware(ctrlPoint, packet *bluetooth.DeviceCharacteristic, opts DFUOptions, totalSize uint32) error {
_, err := ctrlPoint.WriteWithoutResponse(dfuCmdRecvFirmware)
if err != nil {
return err
}
var (
chunksSinceReceipt uint8
bytesSent uint32
)
chunk := make([]byte, opts.SegmentSize)
for {
n, err := opts.FirmwareImage.Read(chunk)
if err != nil && !errors.Is(err, io.EOF) {
return err
} else if n == 0 {
break
}
bytesSent += uint32(n)
_, err = packet.WriteWithoutResponse(chunk[:n])
if err != nil {
return err
}
if errors.Is(err, io.EOF) {
break
}
chunksSinceReceipt += 1
if chunksSinceReceipt == opts.ReceiveInterval {
sizeData, err := awaitDFUResponse(ctrlPoint, []byte{0x11})
if err != nil {
return err
}
size := binary.LittleEndian.Uint32(sizeData)
if size != bytesSent {
return fmt.Errorf("size mismatch: expected %d, got %d", bytesSent, size)
}
if opts.ProgressFunc != nil {
opts.ProgressFunc(bytesSent, size, totalSize)
}
chunksSinceReceipt = 0
}
}
return nil
}
func writeDFUInitPacket(ctrlPoint, packet *bluetooth.DeviceCharacteristic, initPkt fs.File) error {
_, err := ctrlPoint.WriteWithoutResponse(dfuCmdRecvInitPkt)
if err != nil {
return err
}
initData, err := io.ReadAll(initPkt)
if err != nil {
return err
}
_, err = packet.WriteWithoutResponse(initData)
if err != nil {
return err
}
_, err = ctrlPoint.WriteWithoutResponse(dfuCmdInitPktComplete)
if err != nil {
return err
}
_, err = awaitDFUResponse(ctrlPoint, dfuResponseInitParams)
return err
}
func setRecvInterval(ctrlPoint *bluetooth.DeviceCharacteristic, interval uint8) error {
_, err := ctrlPoint.WriteWithoutResponse(append(dfuCmdPktReceiptInterval, interval))
return err
}
func awaitDFUResponse(ctrlPoint *bluetooth.DeviceCharacteristic, expect []byte) ([]byte, error) {
respCh := make(chan []byte, 1)
err := ctrlPoint.EnableNotifications(func(buf []byte) {
respCh <- buf
})
if err != nil {
return nil, err
}
data := <-respCh
ctrlPoint.EnableNotifications(nil)
if !bytes.HasPrefix(data, expect) {
return nil, fmt.Errorf("unexpected dfu response %x (expected %x)", data, expect)
}
return bytes.TrimPrefix(data, expect), nil
}

617
infinitime/fs.go Normal file
View File

@ -0,0 +1,617 @@
package infinitime
import (
"errors"
"io"
"io/fs"
"math"
"path"
"strings"
"sync"
"sync/atomic"
"go.elara.ws/itd/internal/fsproto"
"tinygo.org/x/bluetooth"
)
// FS represents a remote BLE filesystem
type FS struct {
mtx sync.Mutex
dev *Device
}
// Stat gets information about a file at the given path.
//
// WARNING: Since there's no stat command in the BLE FS protocol,
// this function does a ReadDir and then finds the requested file
// in the results, which makes it pretty slow.
func (ifs *FS) Stat(p string) (fs.FileInfo, error) {
dir := path.Dir(p)
entries, err := ifs.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.Name() == path.Base(p) {
return entry.Info()
}
}
return nil, fsproto.ErrFileNotExists
}
// Remove removes a file or empty directory at the given path.
//
// For a function that removes directories recursively, see [FS.RemoveAll]
func (ifs *FS) Remove(path string) error {
ifs.mtx.Lock()
defer ifs.mtx.Unlock()
char, err := ifs.dev.getChar(fsTransferChar)
if err != nil {
return err
}
return ifs.requestThenAwaitResponse(
char,
fsproto.DeleteFileOpcode,
fsproto.DeleteFileRequest{
PathLen: uint16(len(path)),
Path: path,
},
func(buf []byte) (bool, error) {
var mdr fsproto.DeleteFileResponse
return true, fsproto.ReadResponse(buf, fsproto.DeleteFileResp, &mdr)
},
)
}
// Rename moves a file or directory from an old path to a new path.
func (ifs *FS) Rename(old, new string) error {
ifs.mtx.Lock()
defer ifs.mtx.Unlock()
char, err := ifs.dev.getChar(fsTransferChar)
if err != nil {
return err
}
return ifs.requestThenAwaitResponse(
char,
fsproto.MoveFileOpcode,
fsproto.MoveFileRequest{
OldPathLen: uint16(len(old)),
OldPath: old,
NewPathLen: uint16(len(new)),
NewPath: new,
},
func(buf []byte) (bool, error) {
var mfr fsproto.MoveFileResponse
return true, fsproto.ReadResponse(buf, fsproto.MoveFileResp, &mfr)
},
)
}
// Mkdir creates a new directory at the specified path.
//
// For a function that creates necessary parents as well, see [FS.MkdirAll]
func (ifs *FS) Mkdir(path string) error {
ifs.mtx.Lock()
defer ifs.mtx.Unlock()
char, err := ifs.dev.getChar(fsTransferChar)
if err != nil {
return err
}
return ifs.requestThenAwaitResponse(
char,
fsproto.MakeDirectoryOpcode,
fsproto.MkdirRequest{
PathLen: uint16(len(path)),
Path: path,
},
func(buf []byte) (bool, error) {
var mdr fsproto.MkdirResponse
return true, fsproto.ReadResponse(buf, fsproto.MakeDirectoryResp, &mdr)
},
)
}
// ReadDir reads the directory at the specified path and returns a list of directory entries.
func (ifs *FS) ReadDir(path string) ([]fs.DirEntry, error) {
ifs.mtx.Lock()
defer ifs.mtx.Unlock()
char, err := ifs.dev.getChar(fsTransferChar)
if err != nil {
return nil, err
}
var out []fs.DirEntry
return out, ifs.requestThenAwaitResponse(
char,
fsproto.ListDirectoryOpcode,
fsproto.ListDirRequest{
PathLen: uint16(len(path)),
Path: path,
},
func(buf []byte) (bool, error) {
var ldr fsproto.ListDirResponse
err := fsproto.ReadResponse(buf, fsproto.ListDirectoryResp, &ldr)
if err != nil {
return true, err
}
if ldr.EntryNum == ldr.TotalEntries {
return true, nil
}
out = append(out, DirEntry{
flags: ldr.Flags,
modtime: ldr.ModTime,
size: ldr.FileSize,
path: string(ldr.Path),
})
return false, nil
},
)
}
// RemoveAll removes the file at the specified path and any children it contains,
// similar to the rm -r command.
func (ifs *FS) RemoveAll(p string) error {
if p == "" {
return nil
}
if path.Clean(p) == "/" {
return fsproto.ErrNoRemoveRoot
}
fi, err := ifs.Stat(p)
if err != nil {
return nil
}
if fi.IsDir() {
return ifs.removeWithChildren(p)
} else {
err = ifs.Remove(p)
var code int8
if err, ok := err.(fsproto.Error); ok {
code = err.Code
}
if err != nil && code != -2 {
return err
}
}
return nil
}
// removeWithChildren removes the directory at the given path and its children recursively.
func (ifs *FS) removeWithChildren(p string) error {
list, err := ifs.ReadDir(p)
if err != nil {
return err
}
for _, entry := range list {
name := entry.Name()
if name == "." || name == ".." {
continue
}
entryPath := path.Join(p, name)
if entry.IsDir() {
err = ifs.removeWithChildren(entryPath)
} else {
err = ifs.Remove(entryPath)
}
var code int8
if err, ok := err.(fsproto.Error); ok {
code = err.Code
}
if err != nil && code != -2 {
return err
}
}
return ifs.Remove(p)
}
// MkdirAll creates a directory and any necessary parents in the file system,
// similar to the mkdir -p command.
func (ifs *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 := ifs.Mkdir(curPath)
var code int8
if err, ok := err.(fsproto.Error); ok {
code = err.Code
}
if err != nil && code != -17 {
return err
}
}
return nil
}
var _ fs.File = (*File)(nil)
// File represents a remote file on a BLE filesystem.
//
// If ProgressFunc is set, it will be called whenever a read or write happens
// with the amount of bytes transferred and the total size of the file.
type File struct {
fs *FS
path string
offset uint32
size uint32
readOnly bool
closed bool
ProgressFunc func(transferred, total uint32)
}
// Open opens an existing file at the specified path.
// It returns a handle for the file and an error, if any.
func (ifs *FS) Open(path string) (*File, error) {
return &File{
fs: ifs,
path: path,
offset: 0,
readOnly: true,
}, nil
}
// Create creates a new file with the specified path and size.
// It returns a handle for the created file and an error, if any.
func (ifs *FS) Create(path string, size uint32) (*File, error) {
return &File{
fs: ifs,
path: path,
offset: 0,
size: size,
}, nil
}
// Write writes data from the byte slice b to the file.
// It returns the number of bytes written and an error, if any.
func (fl *File) Write(b []byte) (int, error) {
if fl.closed {
return 0, fsproto.ErrFileClosed
}
if fl.readOnly {
return 0, fsproto.ErrFileReadOnly
}
fl.fs.mtx.Lock()
defer fl.fs.mtx.Unlock()
char, err := fl.fs.dev.getChar(fsTransferChar)
if err != nil {
return 0, err
}
defer char.EnableNotifications(nil)
var chunkLen uint32
dataLen := uint32(len(b))
transferred := uint32(0)
mtu := uint32(fl.fs.mtu(char))
// continueCh is used to prevent race conditions. When the
// request loop starts, it reads from continueCh, blocking it
// until it's "released" by the notification function after
// the response is processed.
continueCh := make(chan struct{}, 2)
var notifErr error
err = char.EnableNotifications(func(buf []byte) {
var wfr fsproto.WriteFileResponse
err = fsproto.ReadResponse(buf, fsproto.WriteFileResp, &wfr)
if err != nil {
notifErr = err
char.EnableNotifications(nil)
close(continueCh)
return
}
transferred += chunkLen
fl.offset += chunkLen
if wfr.FreeSpace == 0 || transferred == dataLen {
char.EnableNotifications(nil)
close(continueCh)
return
}
if fl.ProgressFunc != nil {
fl.ProgressFunc(transferred, fl.size)
}
// Release the request loop
continueCh <- struct{}{}
})
err = fsproto.WriteRequest(char, fsproto.WriteFileHeaderOpcode, fsproto.WriteFileHeaderRequest{
PathLen: uint16(len(fl.path)),
Offset: fl.offset,
FileSize: fl.size,
Path: fl.path,
})
if err != nil {
return int(transferred), err
}
for range continueCh {
if notifErr != nil {
return int(transferred), notifErr
}
amountLeft := dataLen - transferred
chunkLen = mtu
if amountLeft < mtu {
chunkLen = amountLeft
}
err = fsproto.WriteRequest(char, fsproto.WriteFileOpcode, fsproto.WriteFileRequest{
Status: 0x01,
Offset: fl.offset,
ChunkLen: chunkLen,
Data: b[transferred : transferred+chunkLen],
})
if err != nil {
return int(transferred), err
}
}
return int(transferred), notifErr
}
// Read reads data from the file into the byte slice b.
// It returns the number of bytes read and an error, if any.
func (fl *File) Read(b []byte) (int, error) {
if fl.closed {
return 0, fsproto.ErrFileClosed
}
fl.fs.mtx.Lock()
defer fl.fs.mtx.Unlock()
char, err := fl.fs.dev.getChar(fsTransferChar)
if err != nil {
return 0, err
}
defer char.EnableNotifications(nil)
transferred := uint32(0)
maxLen := uint32(len(b))
mtu := uint32(fl.fs.mtu(char))
var (
notifErr error
done bool
)
// continueCh is used to prevent race conditions. When the
// request loop starts, it reads from continueCh, blocking it
// until it's "released" by the notification function after
// the response is processed.
continueCh := make(chan struct{}, 2)
err = char.EnableNotifications(func(buf []byte) {
var rfr fsproto.ReadFileResponse
err = fsproto.ReadResponse(buf, fsproto.ReadFileResp, &rfr)
if err != nil {
notifErr = err
char.EnableNotifications(nil)
close(continueCh)
return
}
fl.size = rfr.FileSize
if rfr.Offset == rfr.FileSize || rfr.ChunkLen == 0 {
notifErr = io.EOF
done = true
char.EnableNotifications(nil)
close(continueCh)
return
}
n := copy(b[transferred:], rfr.Data[:rfr.ChunkLen])
fl.offset += uint32(n)
transferred += uint32(n)
if fl.ProgressFunc != nil {
fl.ProgressFunc(transferred, rfr.FileSize)
}
// Release the request loop
continueCh <- struct{}{}
})
if err != nil {
return 0, err
}
defer char.EnableNotifications(nil)
amountLeft := maxLen - transferred
chunkLen := mtu
if amountLeft < mtu {
chunkLen = amountLeft
}
err = fsproto.WriteRequest(char, fsproto.ReadFileHeaderOpcode, fsproto.ReadFileHeaderRequest{
PathLen: uint16(len(fl.path)),
Offset: fl.offset,
ReadLen: chunkLen,
Path: fl.path,
})
if err != nil {
return 0, err
}
if notifErr != nil {
return int(transferred), notifErr
}
for !done {
// Wait for the notification function to release the loop
<-continueCh
if notifErr != nil {
return int(transferred), notifErr
}
amountLeft = maxLen - transferred
chunkLen = mtu
if amountLeft < mtu {
chunkLen = amountLeft
}
err = fsproto.WriteRequest(char, fsproto.ReadFileOpcode, fsproto.ReadFileRequest{
Status: 0x01,
Offset: fl.offset,
ReadLen: chunkLen,
})
if err != nil {
return int(transferred), err
}
}
return int(transferred), notifErr
}
// Stat returns information about the file,
func (fl *File) Stat() (fs.FileInfo, error) {
return fl.fs.Stat(fl.path)
}
// Seek sets the offset for the next Read or Write on the file to the specified offset.
// The whence parameter specifies the seek reference point:
//
// io.SeekStart: offset is relative to the start of the file.
// io.SeekCurrent: offset is relative to the current offset.
// io.SeekEnd: offset is relative to the end of the file.
//
// Seek returns the new offset and an error, if any.
func (fl *File) Seek(offset int64, whence int) (int64, error) {
if fl.closed {
return 0, fsproto.ErrFileClosed
}
if offset > math.MaxUint32 {
return 0, fsproto.ErrInvalidOffset
}
u32Offset := uint32(offset)
fl.fs.mtx.Lock()
defer fl.fs.mtx.Unlock()
if fl.size == 0 {
return 0, errors.New("file size unknown")
}
var newOffset uint32
switch whence {
case io.SeekStart:
newOffset = u32Offset
case io.SeekCurrent:
newOffset = fl.offset + u32Offset
case io.SeekEnd:
newOffset = fl.size + u32Offset
}
if newOffset > fl.size || newOffset < 0 {
return 0, fsproto.ErrInvalidOffset
}
fl.offset = newOffset
return int64(fl.offset), nil
}
// Close closes the file for future operations
func (fl *File) Close() error {
fl.fs.mtx.Lock()
defer fl.fs.mtx.Unlock()
fl.closed = true
return nil
}
// requestThenAwaitResponse executes a BLE FS request and then waits for one or more responses,
// until fn returns true or an error is encountered.
func (ifs *FS) requestThenAwaitResponse(char *bluetooth.DeviceCharacteristic, opcode fsproto.FSReqOpcode, req any, fn func(buf []byte) (bool, error)) error {
var stopped atomic.Bool
errCh := make(chan error, 1)
char.EnableNotifications(func(buf []byte) {
stop, err := fn(buf)
if err != nil && !stopped.Load() {
errCh <- err
char.EnableNotifications(nil)
return
} else if !stopped.Load() {
errCh <- nil
}
if stop && !stopped.Load() {
stopped.Store(true)
close(errCh)
char.EnableNotifications(nil)
}
})
defer char.EnableNotifications(nil)
err := fsproto.WriteRequest(char, opcode, req)
if err != nil {
return err
}
for err := range errCh {
if err != nil {
return err
}
}
return nil
}
func (ifs *FS) mtu(char *bluetooth.DeviceCharacteristic) uint16 {
mtuVal, _ := char.GetMTU()
if mtuVal == 0 {
mtuVal = 256
}
return mtuVal - 20
}
var _ fs.FS = (*GoFS)(nil)
var _ fs.StatFS = (*GoFS)(nil)
var _ fs.ReadDirFS = (*GoFS)(nil)
// GoFS implements [io/fs.FS], [io/fs.StatFS], and [io/fs.ReadDirFS]
// for the InfiniTime filesystem
type GoFS struct {
*FS
}
// Open opens an existing file at the specified path.
// It returns a handle for the file and an error, if any.
func (gfs GoFS) Open(path string) (fs.File, error) {
return gfs.FS.Open(path)
}

142
infinitime/fstypes.go Normal file
View File

@ -0,0 +1,142 @@
package infinitime
import (
"fmt"
"io/fs"
"strconv"
"time"
)
// DirEntry represents an entry from a directory listing
type DirEntry struct {
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,
)
}
func bytesHuman(b uint32) (float64, string) {
const unit = 1000
// Set possible unit 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
}
// 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() any {
return nil
}

173
infinitime/infinitime.go Normal file
View File

@ -0,0 +1,173 @@
package infinitime
import (
"fmt"
"sync"
"sync/atomic"
"time"
"tinygo.org/x/bluetooth"
)
type Options struct {
Allowlist []string
Blocklist []string
ScanInterval time.Duration
OnDisconnect func(dev *Device)
OnReconnect func(dev *Device)
OnConnect func(dev *Device)
}
func reconnect(opts Options, adapter *bluetooth.Adapter, device *Device, mac string) {
if device == nil {
return
}
done := false
for {
adapter.Scan(func(a *bluetooth.Adapter, sr bluetooth.ScanResult) {
if sr.Address.String() != mac {
return
}
dev, err := a.Connect(sr.Address, bluetooth.ConnectionParams{})
if err != nil {
return
}
adapter.StopScan()
device.deviceMtx.Lock()
device.device = dev
device.deviceMtx.Unlock()
device.notifierMtx.Lock()
for char, notifier := range device.notifierMap {
c, err := device.getChar(char)
if err != nil {
continue
}
err = c.EnableNotifications(nil)
if err != nil {
continue
}
err = c.EnableNotifications(notifier.notify)
if err != nil {
continue
}
}
device.notifierMtx.Unlock()
done = true
})
if done {
return
}
time.Sleep(opts.ScanInterval)
}
}
func Connect(opts Options) (device *Device, err error) {
adapter := bluetooth.DefaultAdapter
if opts.ScanInterval == 0 {
opts.ScanInterval = 2 * time.Minute
}
var mac string
adapter.SetConnectHandler(func(dev bluetooth.Device, connected bool) {
if mac == "" || dev.Address.String() != mac {
return
}
if connected {
if opts.OnReconnect != nil {
opts.OnReconnect(device)
}
} else {
if opts.OnDisconnect != nil {
opts.OnDisconnect(device)
}
go reconnect(opts, adapter, device, mac)
}
})
err = adapter.Enable()
if err != nil {
return nil, err
}
var scanErr error
err = adapter.Scan(func(a *bluetooth.Adapter, sr bluetooth.ScanResult) {
if sr.LocalName() != "InfiniTime" {
return
}
dev, err := a.Connect(sr.Address, bluetooth.ConnectionParams{})
if err != nil {
scanErr = err
adapter.StopScan()
return
}
mac = dev.Address.String()
device = &Device{adapter: a, device: dev, notifierMap: map[btChar]notifier{}}
if opts.OnConnect != nil {
opts.OnConnect(device)
}
adapter.StopScan()
})
if err != nil {
return nil, err
}
if scanErr != nil {
return nil, scanErr
}
return device, nil
}
// Device represents an InfiniTime device
type Device struct {
adapter *bluetooth.Adapter
deviceMtx sync.Mutex
device bluetooth.Device
updating atomic.Bool
notifierMtx sync.Mutex
notifierMap map[btChar]notifier
}
// FS returns a handle for InifniTime's filesystem'
func (d *Device) FS() *FS {
return &FS{
dev: d,
}
}
func (d *Device) getChar(c btChar) (*bluetooth.DeviceCharacteristic, error) {
if d.updating.Load() {
return nil, fmt.Errorf("device is currently updating")
}
d.deviceMtx.Lock()
defer d.deviceMtx.Unlock()
services, err := d.device.DiscoverServices([]bluetooth.UUID{c.ServiceID})
if err != nil {
return nil, fmt.Errorf("characteristic %s (%s) not found", c.ID, c.Name)
}
chars, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{c.ID})
if err != nil {
return nil, fmt.Errorf("characteristic %s (%s) not found", c.ID, c.Name)
}
return chars[0], err
}

101
infinitime/info.go Normal file
View File

@ -0,0 +1,101 @@
package infinitime
import (
"context"
"encoding/binary"
)
// Address returns the MAC address of the connected device.
func (d *Device) Address() string {
return d.device.Address.String()
}
// Version returns the version of InifniTime that the connected device is running.
func (d *Device) Version() (string, error) {
c, err := d.getChar(firmwareVerChar)
if err != nil {
return "", err
}
ver := make([]byte, 16)
n, err := c.Read(ver)
return string(ver[:n]), err
}
// BatteryLevel returns the current battery level of the connected PineTime.
func (d *Device) BatteryLevel() (lvl uint8, err error) {
c, err := d.getChar(batteryLevelChar)
if err != nil {
return 0, err
}
err = binary.Read(c, binary.LittleEndian, &lvl)
return lvl, err
}
// WatchBatteryLevel calls fn whenever the battery level changes.
func (d *Device) WatchBatteryLevel(ctx context.Context, fn func(level uint8, err error)) error {
return watchChar(ctx, d, batteryLevelChar, fn)
}
// StepCount returns the current step count recorded on the watch.
func (d *Device) StepCount() (sc uint32, err error) {
c, err := d.getChar(stepCountChar)
if err != nil {
return 0, err
}
err = binary.Read(c, binary.LittleEndian, &sc)
return sc, err
}
// WatchStepCount calls fn whenever the step count changes.
func (d *Device) WatchStepCount(ctx context.Context, fn func(count uint32, err error)) error {
return watchChar(ctx, d, stepCountChar, fn)
}
// HeartRate returns the current heart rate recorded on the watch.
func (d *Device) HeartRate() (uint8, error) {
c, err := d.getChar(heartRateChar)
if err != nil {
return 0, err
}
data := make([]byte, 2)
_, err = c.Read(data)
if err != nil {
return 0, err
}
return data[1], nil
}
// WatchHeartRate calls fn whenever the heart rate changes.
func (d *Device) WatchHeartRate(ctx context.Context, fn func(rate uint8, err error)) error {
return watchChar(ctx, d, heartRateChar, func(rate [2]uint8, err error) {
fn(rate[1], err)
})
}
// MotionValues represents gyroscope coordinates.
type MotionValues struct {
X int16
Y int16
Z int16
}
// Motion returns the current gyroscope coordinates of the PineTime.
func (d *Device) Motion() (mv MotionValues, err error) {
c, err := d.getChar(rawMotionChar)
if err != nil {
return MotionValues{}, err
}
err = binary.Read(c, binary.LittleEndian, &mv)
return mv, err
}
// WatchMotion calls fn whenever the gyroscope coordinates change.
func (d *Device) WatchMotion(ctx context.Context, fn func(level MotionValues, err error)) error {
return watchChar(ctx, d, rawMotionChar, fn)
}

68
infinitime/music.go Normal file
View File

@ -0,0 +1,68 @@
package infinitime
import "context"
type MusicEvent uint8
const (
MusicEventOpen MusicEvent = 0xe0
MusicEventPlay MusicEvent = 0x00
MusicEventPause MusicEvent = 0x01
MusicEventNext MusicEvent = 0x03
MusicEventPrev MusicEvent = 0x04
MusicEventVolUp MusicEvent = 0x05
MusicEventVolDown MusicEvent = 0x06
)
// SetMusicStatus sets whether the music is playing or paused.
func (d *Device) SetMusicStatus(playing bool) error {
char, err := d.getChar(musicStatusChar)
if err != nil {
return err
}
if playing {
_, err = char.WriteWithoutResponse([]byte{0x1})
} else {
_, err = char.WriteWithoutResponse([]byte{0x0})
}
return err
}
// SetMusicArtist sets the music artist.
func (d *Device) SetMusicArtist(artist string) error {
char, err := d.getChar(musicArtistChar)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte(artist))
return err
}
// SetMusicTrack sets the music track name.
func (d *Device) SetMusicTrack(track string) error {
char, err := d.getChar(musicTrackChar)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte(track))
return err
}
// SetMusicAlbum sets the music album name.
func (d *Device) SetMusicAlbum(album string) error {
char, err := d.getChar(musicAlbumChar)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte(album))
return err
}
// WatchMusicEvents calls fn whenever the InfiniTime music app broadcasts an event.
func (d *Device) WatchMusicEvents(ctx context.Context, fn func(event MusicEvent, err error)) error {
return watchChar(ctx, d, musicEventChar, fn)
}

137
infinitime/navigation.go Normal file
View File

@ -0,0 +1,137 @@
package infinitime
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"
NavFlagTurnStraight NavFlag = "turn-straight"
NavFlagUpDown NavFlag = "updown"
NavFlagUTurn NavFlag = "uturn"
)
// SetNavFlag sets the navigation flag icon.
func (d *Device) SetNavFlag(flag NavFlag) error {
char, err := d.getChar(navigationFlagsChar)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte(flag))
return err
}
// SetNavNarrative sets the navigation narrative string.
func (d *Device) SetNavNarrative(narrative string) error {
char, err := d.getChar(navigationNarrativeChar)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte(narrative))
return err
}
// SetNavManeuverDistance sets the navigation maneuver distance.
func (d *Device) SetNavManeuverDistance(manDist string) error {
char, err := d.getChar(navigationManDist)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte(manDist))
return err
}
// SetNavProgress sets the navigation progress.
func (d *Device) SetNavProgress(progress uint8) error {
char, err := d.getChar(navigationProgress)
if err != nil {
return err
}
_, err = char.WriteWithoutResponse([]byte{progress})
return err
}

42
infinitime/notifs.go Normal file
View File

@ -0,0 +1,42 @@
package infinitime
var (
regularNotifHeader = []byte{0x00, 0x01, 0x00}
callNotifHeader = []byte{0x03, 0x01, 0x00}
)
// Notify sends a notification to the PineTime using the Alert Notification Service
func (d *Device) Notify(title, body string) error {
c, err := d.getChar(newAlertChar)
if err != nil {
return err
}
content := title + "\x00" + body
_, err = c.WriteWithoutResponse(append(regularNotifHeader, content...))
return err
}
type CallStatus uint8
const (
CallStatusDeclined CallStatus = iota
CallStatusAccepted
CallStatusMuted
)
// NotifyCall sends a call to the PineTime using the Alert Notification Service,
// then executes fn once the user presses a button on the watch.
func (d *Device) NotifyCall(from string, fn func(CallStatus)) error {
c, err := d.getChar(newAlertChar)
if err != nil {
return err
}
_, err = c.WriteWithoutResponse(append(callNotifHeader, from...))
if err != nil {
return err
}
return watchCharOnce(d, notifEventChar, fn)
}

135
infinitime/resources.go Normal file
View File

@ -0,0 +1,135 @@
package infinitime
import (
"archive/zip"
"encoding/json"
"errors"
"io"
"path/filepath"
)
type ResourceOperation int
const (
// ResourceUpload represents the upload phase
// of resource loading
ResourceUpload = iota
// ResourceRemove represents the obsolete
// file removal phase of resource loading
ResourceRemove
)
// 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 uint32
Transferred uint32
}
// LoadResources accepts the path of an InfiniTime resource archive and loads its contents to the watch's filesystem.
func LoadResources(archivePath string, fs *FS, progress func(ResourceLoadProgress)) error {
r, err := zip.OpenReader(archivePath)
if err != nil {
return err
}
defer r.Close()
manifestFl, err := r.Open("resources.json")
if err != nil {
return err
}
var manifest resourceManifest
err = json.NewDecoder(manifestFl).Decode(&manifest)
if err != nil {
return err
}
err = manifestFl.Close()
if err != nil {
return err
}
for _, file := range manifest.Obsolete {
err := fs.RemoveAll(file.Path)
if err != nil {
return err
}
progress(ResourceLoadProgress{
Operation: ResourceRemove,
Name: filepath.Base(file.Path),
})
}
for _, file := range manifest.Resources {
src, err := r.Open(file.Name)
if err != nil {
return err
}
fi, err := src.Stat()
if err != nil {
return err
}
err = fs.MkdirAll(filepath.Dir(file.Path))
if err != nil {
return err
}
dst, err := fs.Create(file.Path, uint32(fi.Size()))
if err != nil {
return err
}
dst.ProgressFunc = func(transferred, total uint32) {
progress(ResourceLoadProgress{
Name: file.Name,
Transferred: transferred,
Total: total,
})
}
_, err = io.Copy(dst, src)
if err != nil {
return errors.Join(
err,
src.Close(),
dst.Close(),
)
}
err = src.Close()
if err != nil {
return err
}
err = dst.Close()
if err != nil {
return err
}
}
return nil
}

58
infinitime/time.go Normal file
View File

@ -0,0 +1,58 @@
package infinitime
import (
"bytes"
"encoding/binary"
"time"
)
// SetTime sets the current time, and then sets the timezone data,
// if the local time characteristic is available.
func (d *Device) SetTime(t time.Time) error {
c, err := d.getChar(currentTimeChar)
if err != nil {
return err
}
buf := &bytes.Buffer{}
binary.Write(buf, binary.LittleEndian, uint16(t.Year()))
binary.Write(buf, binary.LittleEndian, uint8(t.Month()))
binary.Write(buf, binary.LittleEndian, uint8(t.Day()))
binary.Write(buf, binary.LittleEndian, uint8(t.Hour()))
binary.Write(buf, binary.LittleEndian, uint8(t.Minute()))
binary.Write(buf, binary.LittleEndian, uint8(t.Second()))
binary.Write(buf, binary.LittleEndian, uint8(t.Weekday()))
binary.Write(buf, binary.LittleEndian, uint8((t.Nanosecond()/1000)/1e6*256))
binary.Write(buf, binary.LittleEndian, uint8(0b0001))
_, err = c.WriteWithoutResponse(buf.Bytes())
if err != nil {
return err
}
ltc, err := d.getChar(localTimeChar)
if err != nil {
return nil
}
_, offset := t.Zone()
dst := 0
// Local time expects two values: the timezone offset and the dst offset, both
// expressed in quarters of an hour.
// Timezone offset is to be constant over DST, with dst offset holding the offset != 0
// when DST is in effect.
// As there is no standard way in go to get the actual dst offset, we assume it to be 1h
// when DST is in effect
if t.IsDST() {
dst = 3600
offset -= 3600
}
buf.Reset()
binary.Write(buf, binary.LittleEndian, uint8(offset/3600*4))
binary.Write(buf, binary.LittleEndian, uint8(dst/3600*4))
_, err = ltc.WriteWithoutResponse(buf.Bytes())
return err
}

108
infinitime/watch.go Normal file
View File

@ -0,0 +1,108 @@
package infinitime
import (
"bytes"
"context"
"encoding/binary"
"sync"
"tinygo.org/x/bluetooth"
)
type notifier interface {
notify([]byte)
}
type watcher[T any] struct {
mu sync.Mutex
nextFuncID int
callbacks map[int]func(T, error)
char *bluetooth.DeviceCharacteristic
}
func (w *watcher[T]) addCallback(fn func(T, error)) int {
w.mu.Lock()
defer w.mu.Unlock()
funcID := w.nextFuncID
w.callbacks[funcID] = fn
w.nextFuncID++
return funcID
}
func (w *watcher[T]) notify(b []byte) {
var val T
err := binary.Read(bytes.NewReader(b), binary.LittleEndian, &val)
w.mu.Lock()
for _, fn := range w.callbacks {
go fn(val, err)
}
w.mu.Unlock()
}
func (w *watcher[T]) cancelFn(d *Device, ch btChar, id int) func() {
return func() {
w.mu.Lock()
delete(w.callbacks, id)
w.mu.Unlock()
if len(w.callbacks) == 0 {
d.notifierMtx.Lock()
delete(d.notifierMap, ch)
d.notifierMtx.Unlock()
w.char.EnableNotifications(nil)
}
}
}
func watchChar[T any](ctx context.Context, d *Device, ch btChar, fn func(T, error)) error {
d.notifierMtx.Lock()
defer d.notifierMtx.Unlock()
if n, ok := d.notifierMap[ch]; ok {
w := n.(*watcher[T])
funcID := w.addCallback(fn)
context.AfterFunc(ctx, w.cancelFn(d, ch, funcID))
go func() {
<-ctx.Done()
w.cancelFn(d, ch, funcID)()
}()
return nil
} else {
c, err := d.getChar(ch)
if err != nil {
return err
}
w := &watcher[T]{callbacks: map[int]func(T, error){}}
err = c.EnableNotifications(w.notify)
if err != nil {
return err
}
w.char = c
funcID := w.addCallback(fn)
d.notifierMap[ch] = w
context.AfterFunc(ctx, w.cancelFn(d, ch, funcID))
return nil
}
}
func watchCharOnce[T any](d *Device, ch btChar, fn func(T)) error {
ctx, cancel := context.WithCancel(context.Background())
var watchErr error
err := watchChar(ctx, d, ch, func(val T, err error) {
defer cancel()
if err != nil {
watchErr = err
return
}
fn(val)
})
if err != nil {
return err
}
<-ctx.Done()
return watchErr
}

124
infinitime/weather.go Normal file
View File

@ -0,0 +1,124 @@
package infinitime
import (
"bytes"
"encoding/binary"
"errors"
"time"
)
const (
weatherVersion = 0
currentWeatherType = 0
forecastWeatherType = 1
)
type WeatherIcon uint8
const (
WeatherIconClear WeatherIcon = iota
WeatherIconFewClouds
WeatherIconClouds
WeatherIconHeavyClouds
WeatherIconCloudsWithRain
WeatherIconRain
WeatherIconThunderstorm
WeatherIconSnow
WeatherIconMist
)
// CurrentWeather represents the current weather
type CurrentWeather struct {
Time time.Time
CurrentTemp float32
MinTemp float32
MaxTemp float32
Location string
Icon WeatherIcon
}
// Bytes returns the [CurrentWeather] struct encoded using the InfiniTime
// weather wire protocol.
func (cw CurrentWeather) Bytes() []byte {
buf := &bytes.Buffer{}
buf.WriteByte(currentWeatherType)
buf.WriteByte(weatherVersion)
_, offset := cw.Time.Zone()
binary.Write(buf, binary.LittleEndian, cw.Time.Unix()+int64(offset))
binary.Write(buf, binary.LittleEndian, int16(cw.CurrentTemp*100))
binary.Write(buf, binary.LittleEndian, int16(cw.MinTemp*100))
binary.Write(buf, binary.LittleEndian, int16(cw.MaxTemp*100))
location := make([]byte, 32)
copy(location, cw.Location)
buf.Write(location)
buf.WriteByte(byte(cw.Icon))
return buf.Bytes()
}
// Forecast represents a weather forecast
type Forecast struct {
Time time.Time
Days []ForecastDay
}
// ForecastDay represents a forecast for a single day
type ForecastDay struct {
MinTemp int16
MaxTemp int16
Icon WeatherIcon
}
// Bytes returns the [Forecast] struct encoded using the InfiniTime
// weather wire protocol.
func (f Forecast) Bytes() []byte {
buf := &bytes.Buffer{}
buf.WriteByte(forecastWeatherType)
buf.WriteByte(weatherVersion)
_, offset := f.Time.Zone()
binary.Write(buf, binary.LittleEndian, f.Time.Unix()+int64(offset))
buf.WriteByte(uint8(len(f.Days)))
for _, day := range f.Days {
binary.Write(buf, binary.LittleEndian, day.MinTemp*100)
binary.Write(buf, binary.LittleEndian, day.MaxTemp*100)
buf.WriteByte(byte(day.Icon))
}
return buf.Bytes()
}
// SetCurrentWeather updates the current weather data on the PineTime
func (d *Device) SetCurrentWeather(cw CurrentWeather) error {
c, err := d.getChar(weatherDataChar)
if err != nil {
return err
}
_, err = c.WriteWithoutResponse(cw.Bytes())
return err
}
// SetForecast sets future forecast data on the PineTime
func (d *Device) SetForecast(f Forecast) error {
c, err := d.getChar(weatherDataChar)
if err != nil {
return err
}
if len(f.Days) > 5 {
return errors.New("amount of forecast days exceeds maximum of 5")
}
_, err = c.WriteWithoutResponse(f.Bytes())
return err
}

View File

@ -0,0 +1,62 @@
package fsproto
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("offset out of range")
ErrNoRemoveRoot = errors.New("refusing to remove root directory")
ErrFileClosed = errors.New("cannot perform operation on a closed file")
)
// Error represents an error returned by BLE FS
type Error struct {
Code int8
}
// Error returns the string associated with the error code
func (err Error) 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)
}
}

212
internal/fsproto/fsproto.go Normal file
View File

@ -0,0 +1,212 @@
package fsproto
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"reflect"
"tinygo.org/x/bluetooth"
)
type FSReqOpcode uint8
const (
ReadFileHeaderOpcode FSReqOpcode = 0x10
ReadFileOpcode FSReqOpcode = 0x12
WriteFileHeaderOpcode FSReqOpcode = 0x20
WriteFileOpcode FSReqOpcode = 0x22
DeleteFileOpcode FSReqOpcode = 0x30
MakeDirectoryOpcode FSReqOpcode = 0x40
ListDirectoryOpcode FSReqOpcode = 0x50
MoveFileOpcode FSReqOpcode = 0x60
)
type FSRespOpcode uint8
const (
ReadFileResp FSRespOpcode = 0x11
WriteFileResp FSRespOpcode = 0x21
DeleteFileResp FSRespOpcode = 0x31
MakeDirectoryResp FSRespOpcode = 0x41
ListDirectoryResp FSRespOpcode = 0x51
MoveFileResp FSRespOpcode = 0x61
)
type ReadFileHeaderRequest struct {
Padding byte
PathLen uint16
Offset uint32
ReadLen uint32
Path string
}
type ReadFileRequest struct {
Status uint8
Padding [2]byte
Offset uint32
ReadLen uint32
}
type ReadFileResponse struct {
Status int8
Padding [2]byte
Offset uint32
FileSize uint32
ChunkLen uint32
Data []byte
}
type WriteFileHeaderRequest struct {
Padding byte
PathLen uint16
Offset uint32
ModTime uint64
FileSize uint32
Path string
}
type WriteFileRequest struct {
Status uint8
Padding [2]byte
Offset uint32
ChunkLen uint32
Data []byte
}
type WriteFileResponse struct {
Status int8
Padding [2]byte
Offset uint32
ModTime uint64
FreeSpace uint32
}
type DeleteFileRequest struct {
Padding byte
PathLen uint16
Path string
}
type DeleteFileResponse struct {
Status int8
}
type MkdirRequest struct {
Padding byte
PathLen uint16
Padding2 [4]byte
Timestamp uint64
Path string
}
type MkdirResponse struct {
Status int8
Padding [6]byte
ModTime uint64
}
type ListDirRequest struct {
Padding byte
PathLen uint16
Path string
}
type ListDirResponse struct {
Status int8
PathLen uint16
EntryNum uint32
TotalEntries uint32
Flags uint32
ModTime uint64
FileSize uint32
Path []byte
}
type MoveFileRequest struct {
Padding byte
OldPathLen uint16
NewPathLen uint16
OldPath string
Padding2 byte
NewPath string
}
type MoveFileResponse struct {
Status int8
}
func WriteRequest(char *bluetooth.DeviceCharacteristic, opcode FSReqOpcode, req any) error {
buf := &bytes.Buffer{}
buf.WriteByte(byte(opcode))
rv := reflect.ValueOf(req)
for rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
for i := 0; i < rv.NumField(); i++ {
switch field := rv.Field(i); field.Kind() {
case reflect.String:
io.WriteString(buf, field.String())
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.Uint8 {
buf.Write(field.Bytes())
}
default:
binary.Write(buf, binary.LittleEndian, field.Interface())
}
}
_, err := char.WriteWithoutResponse(buf.Bytes())
return err
}
func ReadResponse(b []byte, expect FSRespOpcode, out interface{}) error {
if len(b) == 0 {
return errors.New("empty response packet")
}
if opcode := FSRespOpcode(b[0]); opcode != expect {
return fmt.Errorf("unexpected response opcode: expected %x, got %x", expect, opcode)
}
r := bytes.NewReader(b[1:])
ot := reflect.TypeOf(out)
if ot.Kind() != reflect.Ptr || ot.Elem().Kind() != reflect.Struct {
return errors.New("out parameter must be a pointer to a struct")
}
ov := reflect.ValueOf(out).Elem()
for i := 0; i < ot.Elem().NumField(); i++ {
field := ot.Elem().Field(i)
fieldValue := ov.Field(i)
// If the last field is a byte slice, just read the remaining data into it and return.
if i == ot.Elem().NumField()-1 {
if field.Type.Kind() == reflect.Slice && field.Type.Elem().Kind() == reflect.Uint8 {
data, err := io.ReadAll(r)
if err != nil {
return err
}
fieldValue.SetBytes(data)
return nil
}
}
if err := binary.Read(r, binary.LittleEndian, fieldValue.Addr().Interface()); err != nil {
return err
}
}
if statusField := ov.FieldByName("Status"); !statusField.IsZero() {
code := statusField.Interface().(int8)
if code != 0x01 {
return Error{code}
}
}
return nil
}

View File

@ -9,9 +9,8 @@ import (
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/infinitime/blefs"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/logger/log"
)
type ITProperty struct {
@ -47,14 +46,14 @@ const (
)
var (
myfs *blefs.FS = nil
myfs *infinitime.FS = nil
inodemap map[string]uint64 = nil
)
func BuildRootNode(dev *infinitime.Device) (*ITNode, error) {
var err error
inodemap = make(map[string]uint64)
myfs, err = dev.FS()
myfs = dev.FS()
if err != nil {
log.Error("FUSE Failed to get filesystem").Err(err).Send()
return nil, err
@ -343,13 +342,10 @@ func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno)
}
return 0
}
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Debug("FUSE Flush progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send()
}
}()
fp.ProgressFunc = func(transferred, total uint32) {
log.Debug("FUSE Read progress").Uint32("bytes", transferred).Uint32("total", total).Send()
}
r := bytes.NewReader(fh.content)
nread, err := io.Copy(fp, r)
@ -430,12 +426,9 @@ func (f *ITNode) Open(ctx context.Context, openFlags uint32) (fh fs.FileHandle,
b := &bytes.Buffer{}
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send()
}
}()
fp.ProgressFunc = func(transferred, total uint32) {
log.Debug("FUSE Read progress").Uint32("bytes", transferred).Uint32("total", total).Send()
}
_, err = io.Copy(b, fp)
if err != nil {

View File

@ -3,7 +3,7 @@ package fusefs
import (
"syscall"
"go.arsenm.dev/infinitime/blefs"
"go.elara.ws/itd/internal/fsproto"
)
func syscallErr(err error) syscall.Errno {
@ -12,10 +12,10 @@ func syscallErr(err error) syscall.Errno {
}
switch err := err.(type) {
case blefs.FSError:
case fsproto.Error:
switch err.Code {
case 0x02: // filesystem error
return syscall.EIO // TODO
return syscall.EIO
case 0x05: // read-only filesystem
return syscall.EROFS
case 0x03: // no such file
@ -25,7 +25,7 @@ func syscallErr(err error) syscall.Errno {
case -5: // input/output error
return syscall.EIO
case -84: // filesystem is corrupted
return syscall.ENOTRECOVERABLE // TODO
return syscall.ENOTRECOVERABLE
case -2: // no such directory entry
return syscall.ENOENT
case -17: // entry already exists
@ -45,28 +45,26 @@ func syscallErr(err error) syscall.Errno {
case -12: // no more memory available
return syscall.ENOMEM
case -61: // no attr available
return syscall.ENODATA // TODO
return syscall.ENODATA
case -36: // file name is too long
return syscall.ENAMETOOLONG
}
default:
switch err {
case blefs.ErrFileNotExists: // file does not exist
case fsproto.ErrFileNotExists: // file does not exist
return syscall.ENOENT
case blefs.ErrFileReadOnly: // file is read only
case fsproto.ErrFileReadOnly: // file is read only
return syscall.EACCES
case blefs.ErrFileWriteOnly: // file is write only
case fsproto.ErrFileWriteOnly: // file is write only
return syscall.EACCES
case blefs.ErrInvalidOffset: // invalid file offset
case fsproto.ErrInvalidOffset: // invalid file offset
return syscall.EINVAL
case blefs.ErrOffsetChanged: // offset has already been changed
return syscall.ESPIPE
case blefs.ErrReadOpen: // only one file can be opened for reading at a time
return syscall.ENFILE
case blefs.ErrWriteOpen: // only one file can be opened for writing at a time
return syscall.ENFILE
case blefs.ErrNoRemoveRoot: // refusing to remove root directory
case fsproto.ErrNoRemoveRoot: // refusing to remove root directory
return syscall.EPERM
case fsproto.ErrFileClosed: // cannot perform operation on closed file
return syscall.EBADF
default:
return syscall.EINVAL
}
}

71
main.go
View File

@ -27,16 +27,15 @@ import (
"os/signal"
"strconv"
"sync"
"syscall"
"time"
"github.com/gen2brain/dlgs"
"github.com/knadh/koanf"
"github.com/mattn/go-isatty"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/logger"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/logger"
"go.elara.ws/logger/log"
)
var k = koanf.New(".")
@ -60,55 +59,43 @@ func main() {
if err != nil {
level = logger.LogLevelInfo
}
// Initialize infinitime library
infinitime.Init(k.String("bluetooth.adapter"))
// Cleanly exit after function
defer infinitime.Exit()
log.Logger.SetLevel(level)
// Create infinitime options struct
opts := &infinitime.Options{
AttemptReconnect: k.Bool("conn.reconnect"),
WhitelistEnabled: k.Bool("conn.whitelist.enabled"),
Whitelist: k.Strings("conn.whitelist.devices"),
OnReqPasskey: onReqPasskey,
Logger: log.Logger,
LogLevel: level,
opts := infinitime.Options{
OnReconnect: func(dev *infinitime.Device) {
if k.Bool("on.reconnect.setTime") {
// Set time to current time
err = dev.SetTime(time.Now())
if err != nil {
return
}
}
// If config specifies to notify on reconnect
if k.Bool("on.reconnect.notify") {
// Send notification to InfiniTime
err = dev.Notify("itd", "Successfully reconnected")
if err != nil {
return
}
}
// FS must be updated on reconnect
updateFS = true
// Resend weather on reconnect
sendWeatherCh <- struct{}{}
},
}
ctx := context.Background()
// Connect to InfiniTime with default options
dev, err := infinitime.Connect(ctx, opts)
dev, err := infinitime.Connect(opts)
if err != nil {
log.Fatal("Error connecting to InfiniTime").Err(err).Send()
}
// When InfiniTime reconnects
opts.OnReconnect = func() {
if k.Bool("on.reconnect.setTime") {
// Set time to current time
err = dev.SetTime(time.Now())
if err != nil {
return
}
}
// If config specifies to notify on reconnect
if k.Bool("on.reconnect.notify") {
// Send notification to InfiniTime
err = dev.Notify("itd", "Successfully reconnected")
if err != nil {
return
}
}
// FS must be updated on reconnect
updateFS = true
// Resend weather on reconnect
sendWeatherCh <- struct{}{}
}
// Get firmware version
ver, err := dev.Version()
if err != nil {

32
maps.go
View File

@ -5,13 +5,13 @@ import (
"strings"
"github.com/godbus/dbus/v5"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/utils"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/internal/utils"
"go.elara.ws/logger/log"
)
const (
interfaceName = "lomiri-app-launch--application-click--pure-maps.jonnius_pure-maps_3.2.0--"
interfaceName = "io.github.rinigus.PureMaps.navigator"
iconProperty = interfaceName + ".icon"
narrativeProperty = interfaceName + ".narrative"
manDistProperty = interfaceName + ".manDist"
@ -38,7 +38,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
// Define rules to listen for
rules := []string{
"type='signal',interface='lomiri-app-launch--application-click--pure-maps.jonnius_pure-maps_3.2.0--'",
"type='signal',interface='io.github.rinigus.PureMaps.navigator'",
}
var flag uint = 0
// Becode monitor for notifications
@ -52,7 +52,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
var navigator dbus.BusObject
if exists {
navigator = conn.Object("lomiri-app-launch--application-click--pure-maps.jonnius_pure-maps_3.2.0--", "/opt/click.ubuntu.com/.click/users/@all/pure-maps.jonnius/navigator")
navigator = conn.Object("io.github.rinigus.PureMaps", "/io/github/rinigus/PureMaps/navigator")
err = setAll(navigator, dev)
if err != nil {
log.Error("Error setting all navigation fields").Err(err).Send()
@ -88,7 +88,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
// The object must be retrieved in this loop in case PureMaps was not
// open at the time ITD was started.
navigator = conn.Object("lomiri-app-launch--application-click--pure-maps.jonnius_pure-maps_3.2.0--", "/opt/click.ubuntu.com/.click/users/@all/pure-maps.jonnius/navigator")
navigator = conn.Object("io.github.rinigus.PureMaps", "/io/github/rinigus/PureMaps/navigator")
member = strings.TrimSuffix(member, "Changed")
switch member {
@ -100,7 +100,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
continue
}
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
err = dev.SetNavFlag(infinitime.NavFlag(icon))
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
@ -113,7 +113,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
continue
}
err = dev.Navigation.SetNarrative(narrative)
err = dev.SetNavNarrative(narrative)
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
@ -126,7 +126,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
continue
}
err = dev.Navigation.SetManDist(manDist)
err = dev.SetNavManeuverDistance(manDist)
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
@ -139,7 +139,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
continue
}
err = dev.Navigation.SetProgress(uint8(progress))
err = dev.SetNavProgress(uint8(progress))
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
@ -165,7 +165,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
return err
}
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
err = dev.SetNavFlag(infinitime.NavFlag(icon))
if err != nil {
return err
}
@ -176,7 +176,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
return err
}
err = dev.Navigation.SetNarrative(narrative)
err = dev.SetNavNarrative(narrative)
if err != nil {
return err
}
@ -187,7 +187,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
return err
}
err = dev.Navigation.SetManDist(manDist)
err = dev.SetNavManeuverDistance(manDist)
if err != nil {
return err
}
@ -198,7 +198,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
return err
}
return dev.Navigation.SetProgress(uint8(progress))
return dev.SetNavProgress(uint8(progress))
}
// pureMapsExists checks to make sure the PureMaps service exists on the bus
@ -210,5 +210,5 @@ func pureMapsExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
if err != nil {
return false, err
}
return strSlcContains(names, "lomiri-app-launch--application-click--pure-maps.jonnius_pure-maps_3.2.0--"), nil
return strSlcContains(names, "io.github.rinigus.PureMaps"), nil
}

View File

@ -4,11 +4,10 @@ import (
"context"
"database/sql"
"path/filepath"
"time"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/logger/log"
_ "modernc.org/sqlite"
)
@ -48,82 +47,95 @@ func initMetrics(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
return err
}
// If heart rate metrics enabled in config
// Watch heart rate
if k.Bool("metrics.heartRate.enabled") {
// Watch heart rate
heartRateCh, err := dev.WatchHeartRate(ctx)
err := dev.WatchHeartRate(ctx, func(heartRate uint8, err error) {
if err != nil {
// Handle error
return
}
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO heartRate VALUES (?, ?);", unixTime, heartRate)
})
if err != nil {
return err
}
go func() {
// For every heart rate sample
for heartRate := range heartRateCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO heartRate VALUES (?, ?);", unixTime, heartRate)
}
}()
}
// If step count metrics enabled in config
if k.Bool("metrics.stepCount.enabled") {
// Watch step count
stepCountCh, err := dev.WatchStepCount(ctx)
err := dev.WatchStepCount(ctx, func(count uint32, err error) {
if err != nil {
return
}
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, count)
})
if err != nil {
return err
}
go func() {
// For every step count sample
for stepCount := range stepCountCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, stepCount)
}
}()
}
// If battery level metrics enabled in config
// Watch step count
if k.Bool("metrics.stepCount.enabled") {
err := dev.WatchStepCount(ctx, func(count uint32, err error) {
if err != nil {
// Handle error
return
}
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, count)
})
if err != nil {
return err
}
}
// Watch battery level
if k.Bool("metrics.battLevel.enabled") {
// Watch battery level
battLevelCh, err := dev.WatchBatteryLevel(ctx)
err := dev.WatchBatteryLevel(ctx, func(battLevel uint8, err error) {
if err != nil {
// Handle error
return
}
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO battLevel VALUES (?, ?);", unixTime, battLevel)
})
if err != nil {
return err
}
go func() {
// For every battery level sample
for battLevel := range battLevelCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO battLevel VALUES (?, ?);", unixTime, battLevel)
}
}()
}
// If motion metrics enabled in config
// Watch motion values
if k.Bool("metrics.motion.enabled") {
// Watch motion values
motionCh, err := dev.WatchMotion(ctx)
err := dev.WatchMotion(ctx, func(motionVals infinitime.MotionValues, err error) {
if err != nil {
// Handle error
return
}
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample values and time into database
db.Exec(
"INSERT INTO motion VALUES (?, ?, ?, ?);",
unixTime,
motionVals.X,
motionVals.Y,
motionVals.Z,
)
})
if err != nil {
return err
}
go func() {
// For every motion sample
for motionVals := range motionCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample values and time into database
db.Exec(
"INSERT INTO motion VALUES (?, ?, ?, ?);",
unixTime,
motionVals.X,
motionVals.Y,
motionVals.Z,
)
}
}()
}
wg.Add(1)

View File

@ -6,7 +6,7 @@ import (
"sync"
"github.com/godbus/dbus/v5"
"go.arsenm.dev/itd/internal/utils"
"go.elara.ws/itd/internal/utils"
)
var (

View File

@ -20,12 +20,11 @@ package main
import (
"context"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/mpris"
"go.arsenm.dev/itd/translit"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/mpris"
"go.elara.ws/itd/translit"
"go.elara.ws/logger/log"
)
func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
@ -39,50 +38,42 @@ func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) er
if !firmwareUpdating {
switch ct {
case mpris.ChangeTypeStatus:
dev.Music.SetStatus(val == "Playing")
dev.SetMusicStatus(val == "Playing")
case mpris.ChangeTypeTitle:
dev.Music.SetTrack(newVal)
dev.SetMusicTrack(newVal)
case mpris.ChangeTypeAlbum:
dev.Music.SetAlbum(newVal)
dev.SetMusicAlbum(newVal)
case mpris.ChangeTypeArtist:
dev.Music.SetArtist(newVal)
dev.SetMusicArtist(newVal)
}
}
})
// Watch for music events
musicEvtCh, err := dev.Music.WatchEvents()
err := dev.WatchMusicEvents(ctx, func(event infinitime.MusicEvent, err error) {
if err != nil {
log.Error("Music event error").Err(err).Send()
}
// Perform appropriate action based on event
switch event {
case infinitime.MusicEventPlay:
mpris.Play()
case infinitime.MusicEventPause:
mpris.Pause()
case infinitime.MusicEventNext:
mpris.Next()
case infinitime.MusicEventPrev:
mpris.Prev()
case infinitime.MusicEventVolUp:
mpris.VolUp(uint(k.Int("music.vol.interval")))
case infinitime.MusicEventVolDown:
mpris.VolDown(uint(k.Int("music.vol.interval")))
}
})
if err != nil {
return err
}
wg.Add(1)
go func() {
defer wg.Done("musicCtrl")
// For every music event received
for {
select {
case musicEvt := <-musicEvtCh:
// Perform appropriate action based on event
switch musicEvt {
case infinitime.MusicEventPlay:
mpris.Play()
case infinitime.MusicEventPause:
mpris.Pause()
case infinitime.MusicEventNext:
mpris.Next()
case infinitime.MusicEventPrev:
mpris.Prev()
case infinitime.MusicEventVolUp:
mpris.VolUp(uint(k.Int("music.vol.interval")))
case infinitime.MusicEventVolDown:
mpris.VolDown(uint(k.Int("music.vol.interval")))
}
case <-ctx.Done():
return
}
}
}()
// Log completed initialization
log.Info("Initialized InfiniTime music controls").Send()

View File

@ -23,10 +23,10 @@ import (
"fmt"
"github.com/godbus/dbus/v5"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/utils"
"go.arsenm.dev/itd/translit"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/internal/utils"
"go.elara.ws/itd/translit"
"go.elara.ws/logger/log"
)
func initNotifRelay(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {

343
socket.go
View File

@ -19,20 +19,19 @@
package main
import (
"archive/zip"
"context"
"errors"
"io"
"net"
"os"
"path/filepath"
"time"
"go.arsenm.dev/drpc/muxserver"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/infinitime/blefs"
"go.arsenm.dev/itd/internal/rpc"
"go.arsenm.dev/logger/log"
"go.elara.ws/drpc/muxserver"
"go.elara.ws/itd/infinitime"
"go.elara.ws/itd/internal/rpc"
"go.elara.ws/logger/log"
"storj.io/drpc/drpcmux"
)
@ -61,11 +60,7 @@ func startSocket(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
return err
}
fs, err := dev.FS()
if err != nil {
log.Warn("Error getting BLE filesystem").Err(err).Send()
}
fs := dev.FS()
mux := drpcmux.New()
err = rpc.DRPCRegisterITD(mux, &ITD{dev})
@ -99,19 +94,29 @@ func (i *ITD) HeartRate(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, erro
}
func (i *ITD) WatchHeartRate(_ *rpc.Empty, s rpc.DRPCITD_WatchHeartRateStream) error {
heartRateCh, err := i.dev.WatchHeartRate(s.Context())
errCh := make(chan error)
err := i.dev.WatchHeartRate(s.Context(), func(rate uint8, err error) {
if err != nil {
errCh <- err
return
}
err = s.Send(&rpc.IntResponse{Value: uint32(rate)})
if err != nil {
errCh <- err
}
})
if err != nil {
return err
}
for heartRate := range heartRateCh {
err = s.Send(&rpc.IntResponse{Value: uint32(heartRate)})
if err != nil {
return err
}
select {
case <-errCh:
return err
case <-s.Context().Done():
return nil
}
return nil
}
func (i *ITD) BatteryLevel(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
@ -120,19 +125,29 @@ func (i *ITD) BatteryLevel(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, e
}
func (i *ITD) WatchBatteryLevel(_ *rpc.Empty, s rpc.DRPCITD_WatchBatteryLevelStream) error {
battLevelCh, err := i.dev.WatchBatteryLevel(s.Context())
errCh := make(chan error)
err := i.dev.WatchBatteryLevel(s.Context(), func(level uint8, err error) {
if err != nil {
errCh <- err
return
}
err = s.Send(&rpc.IntResponse{Value: uint32(level)})
if err != nil {
errCh <- err
}
})
if err != nil {
return err
}
for battLevel := range battLevelCh {
err = s.Send(&rpc.IntResponse{Value: uint32(battLevel)})
if err != nil {
return err
}
select {
case <-errCh:
return err
case <-s.Context().Done():
return nil
}
return nil
}
func (i *ITD) Motion(_ context.Context, _ *rpc.Empty) (*rpc.MotionResponse, error) {
@ -145,23 +160,33 @@ func (i *ITD) Motion(_ context.Context, _ *rpc.Empty) (*rpc.MotionResponse, erro
}
func (i *ITD) WatchMotion(_ *rpc.Empty, s rpc.DRPCITD_WatchMotionStream) error {
motionValsCh, err := i.dev.WatchMotion(s.Context())
errCh := make(chan error)
err := i.dev.WatchMotion(s.Context(), func(motion infinitime.MotionValues, err error) {
if err != nil {
errCh <- err
return
}
err = s.Send(&rpc.MotionResponse{
X: int32(motion.X),
Y: int32(motion.Y),
Z: int32(motion.Z),
})
if err != nil {
errCh <- err
}
})
if err != nil {
return err
}
for motionVals := range motionValsCh {
err = s.Send(&rpc.MotionResponse{
X: int32(motionVals.X),
Y: int32(motionVals.Y),
Z: int32(motionVals.Z),
})
if err != nil {
return err
}
select {
case <-errCh:
return err
case <-s.Context().Done():
return nil
}
return nil
}
func (i *ITD) StepCount(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
@ -170,19 +195,29 @@ func (i *ITD) StepCount(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, erro
}
func (i *ITD) WatchStepCount(_ *rpc.Empty, s rpc.DRPCITD_WatchStepCountStream) error {
stepCountCh, err := i.dev.WatchStepCount(s.Context())
errCh := make(chan error)
err := i.dev.WatchStepCount(s.Context(), func(count uint32, err error) {
if err != nil {
errCh <- err
return
}
err = s.Send(&rpc.IntResponse{Value: count})
if err != nil {
errCh <- err
}
})
if err != nil {
return err
}
for stepCount := range stepCountCh {
err = s.Send(&rpc.IntResponse{Value: stepCount})
if err != nil {
return err
}
select {
case <-errCh:
return err
case <-s.Context().Done():
return nil
}
return nil
}
func (i *ITD) Version(_ context.Context, _ *rpc.Empty) (*rpc.StringResponse, error) {
@ -207,39 +242,34 @@ func (i *ITD) WeatherUpdate(context.Context, *rpc.Empty) (*rpc.Empty, error) {
return &rpc.Empty{}, nil
}
func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_FirmwareUpgradeStream) error {
i.dev.DFU.Reset()
func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_FirmwareUpgradeStream) (err error) {
var fwimg, initpkt *os.File
switch data.Type {
case rpc.FirmwareUpgradeRequest_Archive:
// If less than one file, return error
if len(data.Files) < 1 {
return ErrDFUNotEnoughFiles
}
// If file is not zip archive, return error
if filepath.Ext(data.Files[0]) != ".zip" {
return ErrDFUInvalidFile
}
// Load DFU archive
err := i.dev.DFU.LoadArchive(data.Files[0])
fwimg, initpkt, err = extractDFU(data.Files[0])
if err != nil {
return err
}
case rpc.FirmwareUpgradeRequest_Files:
// If less than two files, return error
if len(data.Files) < 2 {
return ErrDFUNotEnoughFiles
}
// If first file is not init packet, return error
if filepath.Ext(data.Files[0]) != ".dat" {
return ErrDFUInvalidFile
}
// If second file is not firmware image, return error
if filepath.Ext(data.Files[1]) != ".bin" {
return ErrDFUInvalidFile
}
// Load individual DFU files
err := i.dev.DFU.LoadFiles(data.Files[0], data.Files[1])
initpkt, err = os.Open(data.Files[0])
if err != nil {
return err
}
fwimg, err = os.Open(data.Files[1])
if err != nil {
return err
}
@ -247,38 +277,33 @@ func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_Fi
return ErrDFUInvalidUpgType
}
go func() {
for event := range i.dev.DFU.Progress() {
_ = s.Send(&rpc.DFUProgress{
Sent: int64(event.Sent),
Recieved: int64(event.Received),
Total: event.Total,
})
}
defer os.Remove(fwimg.Name())
defer os.Remove(initpkt.Name())
defer fwimg.Close()
defer initpkt.Close()
firmwareUpdating = false
}()
// Set firmwareUpdating
firmwareUpdating = true
defer func() { firmwareUpdating = false }()
// Start DFU
err := i.dev.DFU.Start()
if err != nil {
firmwareUpdating = false
return err
}
return nil
return i.dev.UpgradeFirmware(infinitime.DFUOptions{
InitPacket: initpkt,
FirmwareImage: fwimg,
ProgressFunc: func(sent, received, total uint32) {
_ = s.Send(&rpc.DFUProgress{
Sent: int64(sent),
Recieved: int64(received),
Total: int64(total),
})
},
})
}
type FS struct {
dev *infinitime.Device
fs *blefs.FS
fs *infinitime.FS
}
func (fs *FS) RemoveAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.RemoveAll(path)
if err != nil {
@ -289,7 +314,6 @@ func (fs *FS) RemoveAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, e
}
func (fs *FS) Remove(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.Remove(path)
if err != nil {
@ -300,12 +324,10 @@ func (fs *FS) Remove(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, erro
}
func (fs *FS) Rename(_ context.Context, req *rpc.RenameRequest) (*rpc.Empty, error) {
fs.updateFS()
return &rpc.Empty{}, fs.fs.Rename(req.From, req.To)
}
func (fs *FS) MkdirAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.MkdirAll(path)
if err != nil {
@ -316,7 +338,6 @@ func (fs *FS) MkdirAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, er
}
func (fs *FS) Mkdir(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.Mkdir(path)
if err != nil {
@ -327,8 +348,6 @@ func (fs *FS) Mkdir(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error
}
func (fs *FS) ReadDir(_ context.Context, req *rpc.PathRequest) (*rpc.DirResponse, error) {
fs.updateFS()
entries, err := fs.fs.ReadDir(req.Path)
if err != nil {
return nil, err
@ -350,8 +369,6 @@ func (fs *FS) ReadDir(_ context.Context, req *rpc.PathRequest) (*rpc.DirResponse
}
func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error {
fs.updateFS()
localFile, err := os.Open(req.Source)
if err != nil {
return err
@ -367,15 +384,12 @@ func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error
return err
}
go func() {
// For every progress event
for sent := range remoteFile.Progress() {
_ = s.Send(&rpc.TransferProgress{
Total: remoteFile.Size(),
Sent: sent,
})
}
}()
remoteFile.ProgressFunc = func(transferred, total uint32) {
_ = s.Send(&rpc.TransferProgress{
Total: total,
Sent: transferred,
})
}
io.Copy(remoteFile, localFile)
localFile.Close()
@ -385,8 +399,6 @@ func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error
}
func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) error {
fs.updateFS()
localFile, err := os.Create(req.Destination)
if err != nil {
return err
@ -400,15 +412,12 @@ func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) er
defer localFile.Close()
defer remoteFile.Close()
go func() {
// For every progress event
for sent := range remoteFile.Progress() {
_ = s.Send(&rpc.TransferProgress{
Total: remoteFile.Size(),
Sent: sent,
})
}
}()
remoteFile.ProgressFunc = func(transferred, total uint32) {
_ = s.Send(&rpc.TransferProgress{
Total: total,
Sent: transferred,
})
}
_, err = io.Copy(localFile, remoteFile)
if err != nil {
@ -419,42 +428,86 @@ func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) er
}
func (fs *FS) LoadResources(req *rpc.PathRequest, s rpc.DRPCFS_LoadResourcesStream) error {
resFl, err := os.Open(req.Path)
if err != nil {
return err
}
progCh, err := infinitime.LoadResources(resFl, fs.fs)
if err != nil {
return err
}
for evt := range progCh {
err = s.Send(&rpc.ResourceLoadProgress{
return infinitime.LoadResources(req.Path, fs.fs, func(evt infinitime.ResourceLoadProgress) {
_ = s.Send(&rpc.ResourceLoadProgress{
Name: evt.Name,
Total: evt.Total,
Sent: evt.Sent,
Total: int64(evt.Total),
Sent: int64(evt.Transferred),
Operation: rpc.ResourceLoadProgress_Operation(evt.Operation),
})
if err != nil {
return err
})
}
func extractDFU(path string) (fwimg, initpkt *os.File, err error) {
zipReader, err := zip.OpenReader(path)
if err != nil {
return nil, nil, err
}
defer zipReader.Close()
for _, file := range zipReader.File {
if fwimg != nil && initpkt != nil {
break
}
switch filepath.Ext(file.Name) {
case ".bin":
fwimg, err = os.CreateTemp(os.TempDir(), "itd_dfu_fwimg_*.bin")
if err != nil {
return nil, nil, err
}
zipFile, err := file.Open()
if err != nil {
return nil, nil, err
}
defer zipFile.Close()
_, err = io.Copy(fwimg, zipFile)
if err != nil {
return nil, nil, err
}
err = zipFile.Close()
if err != nil {
return nil, nil, err
}
_, err = fwimg.Seek(0, io.SeekStart)
if err != nil {
return nil, nil, err
}
case ".dat":
initpkt, err = os.CreateTemp(os.TempDir(), "itd_dfu_initpkt_*.dat")
if err != nil {
return nil, nil, err
}
zipFile, err := file.Open()
if err != nil {
return nil, nil, err
}
_, err = io.Copy(initpkt, zipFile)
if err != nil {
return nil, nil, err
}
err = zipFile.Close()
if err != nil {
return nil, nil, err
}
_, err = initpkt.Seek(0, io.SeekStart)
if err != nil {
return nil, nil, err
}
}
}
return nil
}
func (fs *FS) updateFS() {
if fs.fs == nil || updateFS {
// Get new FS
newFS, err := fs.dev.FS()
if err != nil {
log.Warn("Error updating BLE filesystem").Err(err).Send()
} else {
// Set FS pointer to new FS
fs.fs = newFS
// Reset updateFS
updateFS = false
}
if fwimg == nil || initpkt == nil {
return nil, nil, errors.New("invalid dfu archive")
}
return fwimg, initpkt, nil
}

View File

@ -3,7 +3,7 @@ package main
import (
"sync"
"go.arsenm.dev/logger/log"
"go.elara.ws/logger/log"
)
type WaitGroup struct {

View File

@ -4,17 +4,14 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/infinitime/weather"
"go.arsenm.dev/logger/log"
"go.elara.ws/itd/infinitime"
"go.elara.ws/logger/log"
)
// METResponse represents a response from
@ -33,7 +30,7 @@ type METData struct {
Instant struct {
Details struct {
AirPressure float32 `json:"air_pressure_at_sea_level"`
AirTemperature float32 `json:"air_temperature"`
Temperature float32 `json:"air_temperature"`
DewPoint float32 `json:"dew_point_temperature"`
CloudAreaFraction float32 `json:"cloud_area_fraction"`
FogAreaFraction float32 `json:"fog_area_fraction"`
@ -51,6 +48,12 @@ type METData struct {
PrecipitationAmount float32 `json:"precipitation_amount"`
}
} `json:"next_1_hours"`
Next6Hours struct {
Details struct {
MaxTemp float32 `json:"air_temperature_max"`
MinTemp float32 `json:"air_temperature_min"`
}
} `json:"next_6_hours"`
}
// OSMData represents lat/long data from
@ -86,9 +89,12 @@ func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
go func() {
defer wg.Done("weather")
for {
_, ok := <-ctx.Done()
if !ok {
return
select {
case _, ok := <-ctx.Done():
if !ok {
return
}
default:
}
// Attempt to get weather
@ -104,81 +110,28 @@ func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
current := data.Properties.Timeseries[0]
currentData := current.Data.Instant.Details
// Add temperature event
err = dev.AddWeatherEvent(weather.TemperatureEvent{
TimelineHeader: weather.NewHeader(
weather.EventTypeTemperature,
time.Hour,
),
Temperature: int16(round(currentData.AirTemperature * 100)),
DewPoint: int16(round(currentData.DewPoint)),
})
if err != nil {
log.Error("Error adding temperature event").Err(err).Send()
icon := parseSymbol(current.Data.NextHour.Summary.SymbolCode)
if icon == infinitime.WeatherIconClear {
switch {
case currentData.CloudAreaFraction > 50:
icon = infinitime.WeatherIconHeavyClouds
case currentData.CloudAreaFraction == 50:
icon = infinitime.WeatherIconClouds
case currentData.CloudAreaFraction > 0:
icon = infinitime.WeatherIconFewClouds
}
}
// Add precipitation event
err = dev.AddWeatherEvent(weather.PrecipitationEvent{
TimelineHeader: weather.NewHeader(
weather.EventTypePrecipitation,
time.Hour,
),
Type: parseSymbol(current.Data.NextHour.Summary.SymbolCode),
Amount: uint8(round(current.Data.NextHour.Details.PrecipitationAmount)),
err = dev.SetCurrentWeather(infinitime.CurrentWeather{
Time: time.Now(),
CurrentTemp: currentData.Temperature,
MaxTemp: current.Data.Next6Hours.Details.MaxTemp,
MinTemp: current.Data.Next6Hours.Details.MinTemp,
Location: k.String("weather.location"),
Icon: icon,
})
if err != nil {
log.Error("Error adding precipitation event").Err(err).Send()
}
// Add wind event
err = dev.AddWeatherEvent(weather.WindEvent{
TimelineHeader: weather.NewHeader(
weather.EventTypeWind,
time.Hour,
),
SpeedMin: uint8(round(currentData.WindSpeed)),
SpeedMax: uint8(round(currentData.WindSpeed)),
DirectionMin: uint8(round(currentData.WindDirection)),
DirectionMax: uint8(round(currentData.WindDirection)),
})
if err != nil {
log.Error("Error adding wind event").Err(err).Send()
}
// Add cloud event
err = dev.AddWeatherEvent(weather.CloudsEvent{
TimelineHeader: weather.NewHeader(
weather.EventTypeClouds,
time.Hour,
),
Amount: uint8(round(currentData.CloudAreaFraction)),
})
if err != nil {
log.Error("Error adding clouds event").Err(err).Send()
}
// Add humidity event
err = dev.AddWeatherEvent(weather.HumidityEvent{
TimelineHeader: weather.NewHeader(
weather.EventTypeHumidity,
time.Hour,
),
Humidity: uint8(round(currentData.RelativeHumidity)),
})
if err != nil {
log.Error("Error adding humidity event").Err(err).Send()
}
// Add pressure event
err = dev.AddWeatherEvent(weather.PressureEvent{
TimelineHeader: weather.NewHeader(
weather.EventTypePressure,
time.Hour,
),
Pressure: int16(round(currentData.AirPressure)),
})
if err != nil {
log.Error("Error adding pressure event").Err(err).Send()
log.Error("Error setting weather").Err(err).Send()
}
// Reset timer to 1 hour
@ -206,6 +159,7 @@ func getLocation(ctx context.Context, loc string) (lat, lon float64, err error)
if err != nil {
return
}
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.elara.ws/Elara6331/itd", strings.TrimSpace(version)))
res, err := http.DefaultClient.Do(req)
if err != nil {
return
@ -257,7 +211,7 @@ func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
}
// Set identifying user agent as per NMI requirements
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.arsenm.dev/Arsen6331/itd", version))
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.elara.ws/Elara6331/itd", strings.TrimSpace(version)))
// Perform request
res, err := http.DefaultClient.Do(req)
@ -275,26 +229,19 @@ func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
return out, nil
}
// parseSymbol determines what type of precipitation a symbol code
// codes for.
func parseSymbol(symCode string) weather.PrecipitationType {
// parseSymbol determines what weather icon a symbol code codes for.
func parseSymbol(symCode string) infinitime.WeatherIcon {
switch {
case strings.Contains(symCode, "lightrain"):
return weather.PrecipitationTypeRain
return infinitime.WeatherIconRain
case strings.Contains(symCode, "rain"):
return weather.PrecipitationTypeRain
case strings.Contains(symCode, "snow"):
return weather.PrecipitationTypeSnow
case strings.Contains(symCode, "sleet"):
return weather.PrecipitationTypeSleet
case strings.Contains(symCode, "snow"):
return weather.PrecipitationTypeSnow
return infinitime.WeatherIconCloudsWithRain
case strings.Contains(symCode, "snow"),
strings.Contains(symCode, "sleet"):
return infinitime.WeatherIconSnow
case strings.Contains(symCode, "thunder"):
return infinitime.WeatherIconThunderstorm
default:
return weather.PrecipitationTypeNone
return infinitime.WeatherIconClear
}
}
// round rounds 32-bit floats to 32-bit integers
func round(f float32) int32 {
return int32(math.Round(float64(f)))
}