forked from Elara6331/itd
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
37c61695aa | |||
243826bd67 | |||
cb5abc05e5 | |||
39a3603c8e | |||
e6b36494e7 | |||
02532437ea | |||
7e68d5541c | |||
2a8013e63e | |||
6904e95913 | |||
c0937918b1 | |||
57d5eafbe7 | |||
d93672c997 | |||
35163198c9 | |||
881a103c56 | |||
395cded975 | |||
254b675f2c | |||
669788034f | |||
ac7c626f56 | |||
1c623051ec | |||
cec539f85b |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
liberapay: Elara6331
|
@ -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: >-
|
||||
@ -54,7 +56,7 @@ nfpms:
|
||||
{{- end }}
|
||||
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
||||
maintainer: 'Elara Musayelyan <elara@elara.ws>'
|
||||
maintainer: 'Elara Ivy <elara@elara.ws>'
|
||||
license: GPLv3
|
||||
formats:
|
||||
- apk
|
||||
@ -77,7 +79,7 @@ aurs:
|
||||
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
||||
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||
maintainers:
|
||||
- 'Elara Musayelyan <elara@elara.ws>'
|
||||
- 'Elara Ivy <elara@elara.ws>'
|
||||
license: GPLv3
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/itd-bin.git'
|
||||
|
27
README.md
27
README.md
@ -6,6 +6,12 @@
|
||||
[![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.
|
||||
|
@ -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 {
|
||||
|
@ -3,15 +3,15 @@ package api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.elara.ws/infinitime"
|
||||
"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 {
|
||||
|
47
calls.go
47
calls.go
@ -2,10 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/itd/internal/utils"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
@ -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)
|
||||
@ -83,33 +81,28 @@ func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) e
|
||||
}
|
||||
|
||||
// Send call notification to InfiniTime
|
||||
resCh, err := dev.NotifyCall(phoneNum)
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.elara.ws/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 {
|
||||
|
@ -11,8 +11,8 @@ import (
|
||||
"fyne.io/fyne/v2/storage"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.elara.ws/infinitime"
|
||||
"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))
|
||||
|
2
fuse.go
2
fuse.go
@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/itd/internal/fusefs"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
|
17
go.mod
17
go.mod
@ -4,6 +4,8 @@ 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
|
||||
@ -16,12 +18,12 @@ require (
|
||||
github.com/mozillazg/go-pinyin v0.19.0
|
||||
github.com/urfave/cli/v2 v2.23.7
|
||||
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d
|
||||
go.elara.ws/infinitime v0.0.0-20230421025334-f2640203e9e9
|
||||
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090
|
||||
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
|
||||
|
38
go.sum
38
go.sum
@ -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,13 +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/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/infinitime v0.0.0-20230421025334-f2640203e9e9 h1:HczkQCAHHmOHHdzTxFC1tEGzBJ7F/fuOWNUGrYQhMOg=
|
||||
go.elara.ws/infinitime v0.0.0-20230421025334-f2640203e9e9/go.mod h1:fu3+jGNBBtRQ1lcywtdZzoQgDFtqvvxBHvLw+XgPrFg=
|
||||
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090 h1:RVC8XvWo6Yw4HUshqx4TSzuBDScDghafU6QFRJ4xPZg=
|
||||
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM=
|
||||
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=
|
||||
@ -727,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=
|
||||
@ -749,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=
|
||||
@ -757,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=
|
||||
@ -821,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
142
infinitime/chars.go
Normal 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
222
infinitime/dfu.go
Normal 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
617
infinitime/fs.go
Normal 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
142
infinitime/fstypes.go
Normal 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
173
infinitime/infinitime.go
Normal 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
101
infinitime/info.go
Normal 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
68
infinitime/music.go
Normal 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
137
infinitime/navigation.go
Normal 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
42
infinitime/notifs.go
Normal 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
135
infinitime/resources.go
Normal 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
58
infinitime/time.go
Normal 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
108
infinitime/watch.go
Normal 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
124
infinitime/weather.go
Normal 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
|
||||
}
|
62
internal/fsproto/errors.go
Normal file
62
internal/fsproto/errors.go
Normal 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
212
internal/fsproto/fsproto.go
Normal 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
|
||||
}
|
@ -9,8 +9,7 @@ import (
|
||||
|
||||
"github.com/hanwen/go-fuse/v2/fs"
|
||||
"github.com/hanwen/go-fuse/v2/fuse"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/infinitime/blefs"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
|
||||
@ -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
|
||||
@ -344,12 +343,9 @@ 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 {
|
||||
|
@ -3,7 +3,7 @@ package fusefs
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"go.elara.ws/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
|
||||
}
|
||||
}
|
||||
|
||||
|
67
main.go
67
main.go
@ -27,14 +27,13 @@ import (
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gen2brain/dlgs"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/mattn/go-isatty"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/logger"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
@ -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 {
|
||||
|
18
maps.go
18
maps.go
@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/itd/internal/utils"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
@ -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
|
||||
|
120
metrics.go
120
metrics.go
@ -4,10 +4,9 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
|
||||
"time"
|
||||
|
||||
"go.elara.ws/infinitime"
|
||||
"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)
|
||||
|
61
music.go
61
music.go
@ -21,8 +21,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/itd/mpris"
|
||||
"go.elara.ws/itd/translit"
|
||||
"go.elara.ws/logger/log"
|
||||
@ -39,51 +38,43 @@ 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()
|
||||
|
||||
|
@ -23,7 +23,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/itd/internal/utils"
|
||||
"go.elara.ws/itd/translit"
|
||||
"go.elara.ws/logger/log"
|
||||
|
337
socket.go
337
socket.go
@ -19,18 +19,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"time"
|
||||
|
||||
"go.elara.ws/drpc/muxserver"
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/infinitime/blefs"
|
||||
"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
|
||||
}
|
||||
|
141
weather.go
141
weather.go
@ -4,16 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
|
||||
"go.elara.ws/infinitime"
|
||||
"go.elara.ws/infinitime/weather"
|
||||
"go.elara.ws/itd/infinitime"
|
||||
"go.elara.ws/logger/log"
|
||||
)
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user