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
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
|
- riscv64
|
||||||
goarm:
|
goarm:
|
||||||
- 7
|
- 7
|
||||||
- id: itctl
|
- id: itctl
|
||||||
@ -43,6 +44,7 @@ archives:
|
|||||||
- README.md
|
- README.md
|
||||||
- itd.toml
|
- itd.toml
|
||||||
- itd.service
|
- itd.service
|
||||||
|
allow_different_binary_count: true
|
||||||
nfpms:
|
nfpms:
|
||||||
- id: itd
|
- id: itd
|
||||||
file_name_template: >-
|
file_name_template: >-
|
||||||
@ -54,7 +56,7 @@ nfpms:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||||
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
||||||
maintainer: 'Elara Musayelyan <elara@elara.ws>'
|
maintainer: 'Elara Ivy <elara@elara.ws>'
|
||||||
license: GPLv3
|
license: GPLv3
|
||||||
formats:
|
formats:
|
||||||
- apk
|
- apk
|
||||||
@ -77,7 +79,7 @@ aurs:
|
|||||||
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
||||||
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||||
maintainers:
|
maintainers:
|
||||||
- 'Elara Musayelyan <elara@elara.ws>'
|
- 'Elara Ivy <elara@elara.ws>'
|
||||||
license: GPLv3
|
license: GPLv3
|
||||||
private_key: '{{ .Env.AUR_KEY }}'
|
private_key: '{{ .Env.AUR_KEY }}'
|
||||||
git_url: 'ssh://aur@aur.archlinux.org/itd-bin.git'
|
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)
|
[![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-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/)
|
[![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
|
#### 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 `./`.
|
- 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`
|
- Example: `sudo apt install ~/Downloads/itd-0.0.7-linux-aarch64.deb`
|
||||||
|
|
||||||
#### Fedora
|
#### 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.
|
- 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`
|
- Example: `sudo dnf install ~/Downloads/itd-0.0.7-linux-aarch64.rpm`
|
||||||
|
|
||||||
#### Alpine (and postmarketOS)
|
#### 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.
|
- 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`
|
- 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
|
#### 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:
|
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`.
|
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.
|
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.
|
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
|
### 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.
|
This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directly control the daemon and, by extension, the connected watch.
|
||||||
|
@ -3,15 +3,15 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.elara.ws/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceOperation uint8
|
type ResourceOperation infinitime.ResourceOperation
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceOperationRemoveObsolete = infinitime.ResourceOperationRemoveObsolete
|
ResourceRemove = infinitime.ResourceRemove
|
||||||
ResourceOperationUpload = infinitime.ResourceOperationUpload
|
ResourceUpload = infinitime.ResourceUpload
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceLoadProgress struct {
|
type ResourceLoadProgress struct {
|
||||||
|
47
calls.go
47
calls.go
@ -2,10 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.elara.ws/itd/internal/utils"
|
||||||
"go.elara.ws/logger/log"
|
"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
|
// Notify channel upon received message
|
||||||
monitorConn.Eavesdrop(callCh)
|
monitorConn.Eavesdrop(callCh)
|
||||||
|
|
||||||
var respHandlerOnce sync.Once
|
|
||||||
var callObj dbus.BusObject
|
var callObj dbus.BusObject
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@ -83,33 +81,28 @@ func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send call notification to InfiniTime
|
// 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 {
|
if err != nil {
|
||||||
continue
|
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():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cheggaaa/pb/v3"
|
"github.com/cheggaaa/pb/v3"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resourcesLoad(c *cli.Context) error {
|
func resourcesLoad(c *cli.Context) error {
|
||||||
@ -39,7 +39,7 @@ func resLoad(ctx context.Context, args []string) error {
|
|||||||
return evt.Err
|
return evt.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
if evt.Operation == infinitime.ResourceOperationRemoveObsolete {
|
if evt.Operation == infinitime.ResourceRemove {
|
||||||
bar.SetTemplateString(rmTmpl)
|
bar.SetTemplateString(rmTmpl)
|
||||||
bar.Set("filename", evt.Name)
|
bar.Set("filename", evt.Name)
|
||||||
} else {
|
} else {
|
||||||
|
@ -11,8 +11,8 @@ import (
|
|||||||
"fyne.io/fyne/v2/storage"
|
"fyne.io/fyne/v2/storage"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"go.elara.ws/infinitime"
|
|
||||||
"go.elara.ws/itd/api"
|
"go.elara.ws/itd/api"
|
||||||
|
"go.elara.ws/itd/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan struct{}) fyne.CanvasObject {
|
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 {
|
for evt := range progCh {
|
||||||
switch evt.Operation {
|
switch evt.Operation {
|
||||||
case infinitime.ResourceOperationRemoveObsolete:
|
case infinitime.ResourceRemove:
|
||||||
progressDlg.SetText("Removing " + evt.Name)
|
progressDlg.SetText("Removing " + evt.Name)
|
||||||
case infinitime.ResourceOperationUpload:
|
case infinitime.ResourceUpload:
|
||||||
progressDlg.SetText("Uploading " + evt.Name)
|
progressDlg.SetText("Uploading " + evt.Name)
|
||||||
progressDlg.SetTotal(float64(evt.Total))
|
progressDlg.SetTotal(float64(evt.Total))
|
||||||
progressDlg.SetValue(float64(evt.Sent))
|
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/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"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/itd/internal/fusefs"
|
||||||
"go.elara.ws/logger/log"
|
"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 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 (
|
require (
|
||||||
fyne.io/fyne/v2 v2.3.0
|
fyne.io/fyne/v2 v2.3.0
|
||||||
fyne.io/x/fyne v0.0.0-20220107050838-c4a1de51d4ce
|
fyne.io/x/fyne v0.0.0-20220107050838-c4a1de51d4ce
|
||||||
@ -16,12 +18,12 @@ require (
|
|||||||
github.com/mozillazg/go-pinyin v0.19.0
|
github.com/mozillazg/go-pinyin v0.19.0
|
||||||
github.com/urfave/cli/v2 v2.23.7
|
github.com/urfave/cli/v2 v2.23.7
|
||||||
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d
|
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-20230928062203-85e135cf02ae
|
||||||
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090
|
|
||||||
golang.org/x/text v0.5.0
|
golang.org/x/text v0.5.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
modernc.org/sqlite v1.20.1
|
modernc.org/sqlite v1.20.1
|
||||||
storj.io/drpc v0.0.32
|
storj.io/drpc v0.0.32
|
||||||
|
tinygo.org/x/bluetooth v0.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -32,15 +34,14 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
github.com/fatih/color v1.13.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/fredbi/uri v1.0.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.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/gl-js v0.0.0-20220802150000-8e339395f381 // indirect
|
||||||
github.com/fyne-io/glfw-js v0.0.0-20220517201726-bebc2019cd33 // 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/fyne-io/image v0.0.0-20221020213044-f609c6a24345 // indirect
|
||||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // 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-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/go-text/typesetting v0.0.0-20221219135543-5d0d724ee181 // indirect
|
||||||
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
|
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
|
||||||
github.com/google/uuid v1.3.0 // 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/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // 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/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
|
||||||
github.com/rivo/uniseg v0.4.3 // indirect
|
github.com/rivo/uniseg v0.4.3 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // 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/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||||
github.com/stretchr/testify v1.8.1 // indirect
|
github.com/stretchr/testify v1.8.1 // indirect
|
||||||
github.com/tevino/abool v1.2.0 // 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/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
github.com/yuin/goldmark v1.5.3 // 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/mobile v0.0.0-20221110043201-43a038452099 // indirect
|
||||||
golang.org/x/mod v0.7.0 // indirect
|
golang.org/x/mod v0.7.0 // indirect
|
||||||
golang.org/x/net v0.4.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
|
golang.org/x/tools v0.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
honnef.co/go/js/dom v0.0.0-20221001195520-26252dedbe70 // 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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
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/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.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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
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.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 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
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/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.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
|
||||||
github.com/fredbi/uri v0.1.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
|
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.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
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-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 h1:SFtj9yo9C7F4CxyJeSJi9AjT6x9c88gnY1tjlXWh9QU=
|
||||||
github.com/fyne-io/gl-js v0.0.0-20220802150000-8e339395f381/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
|
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.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
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.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-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-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-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-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 h1:J6XG/Xx7uCCpskM71R6YAgPHd/E8FzhyPhL6Ll94uMY=
|
||||||
github.com/go-text/typesetting v0.0.0-20221219135543-5d0d724ee181/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI=
|
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.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 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
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=
|
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-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
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/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.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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/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 h1:p+J8/kjJ558KPvVGYLvqBhxf8jbZA2exSLCs2uUVN8c=
|
||||||
github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
|
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-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/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=
|
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/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.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/goe v0.1.0/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.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.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
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 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/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/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/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/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=
|
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/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.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
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.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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/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/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
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/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 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||||
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
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.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.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 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
|
||||||
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
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/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 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
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=
|
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/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 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
|
||||||
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
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 h1:ANb8YPtcxPipwKgmnW688e5PGpNaLh+22nO2LBpIPOU=
|
||||||
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d/go.mod h1:NDprjiVqKXQKVGzX7jp2g/jctsUbvOxz1nN15QOBEGk=
|
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/logger v0.0.0-20230928062203-85e135cf02ae h1:d+gJUhEWSrOjrrfgeydYWEr8TTnx0DLvcVhghaOsFeE=
|
||||||
go.elara.ws/infinitime v0.0.0-20230421025334-f2640203e9e9/go.mod h1:fu3+jGNBBtRQ1lcywtdZzoQgDFtqvvxBHvLw+XgPrFg=
|
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM=
|
||||||
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.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
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/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
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-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-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-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-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-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-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-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-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-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-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-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-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.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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=
|
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-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-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-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-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-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/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/fs"
|
||||||
"github.com/hanwen/go-fuse/v2/fuse"
|
"github.com/hanwen/go-fuse/v2/fuse"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/infinitime/blefs"
|
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,14 +46,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
myfs *blefs.FS = nil
|
myfs *infinitime.FS = nil
|
||||||
inodemap map[string]uint64 = nil
|
inodemap map[string]uint64 = nil
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildRootNode(dev *infinitime.Device) (*ITNode, error) {
|
func BuildRootNode(dev *infinitime.Device) (*ITNode, error) {
|
||||||
var err error
|
var err error
|
||||||
inodemap = make(map[string]uint64)
|
inodemap = make(map[string]uint64)
|
||||||
myfs, err = dev.FS()
|
myfs = dev.FS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("FUSE Failed to get filesystem").Err(err).Send()
|
log.Error("FUSE Failed to get filesystem").Err(err).Send()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -344,12 +343,9 @@ func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno)
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
fp.ProgressFunc = func(transferred, total uint32) {
|
||||||
// For every progress event
|
log.Debug("FUSE Read progress").Uint32("bytes", transferred).Uint32("total", total).Send()
|
||||||
for sent := range fp.Progress() {
|
}
|
||||||
log.Debug("FUSE Flush progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
r := bytes.NewReader(fh.content)
|
r := bytes.NewReader(fh.content)
|
||||||
nread, err := io.Copy(fp, r)
|
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{}
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
go func() {
|
fp.ProgressFunc = func(transferred, total uint32) {
|
||||||
// For every progress event
|
log.Debug("FUSE Read progress").Uint32("bytes", transferred).Uint32("total", total).Send()
|
||||||
for sent := range fp.Progress() {
|
}
|
||||||
log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = io.Copy(b, fp)
|
_, err = io.Copy(b, fp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,7 +3,7 @@ package fusefs
|
|||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"go.elara.ws/infinitime/blefs"
|
"go.elara.ws/itd/internal/fsproto"
|
||||||
)
|
)
|
||||||
|
|
||||||
func syscallErr(err error) syscall.Errno {
|
func syscallErr(err error) syscall.Errno {
|
||||||
@ -12,10 +12,10 @@ func syscallErr(err error) syscall.Errno {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case blefs.FSError:
|
case fsproto.Error:
|
||||||
switch err.Code {
|
switch err.Code {
|
||||||
case 0x02: // filesystem error
|
case 0x02: // filesystem error
|
||||||
return syscall.EIO // TODO
|
return syscall.EIO
|
||||||
case 0x05: // read-only filesystem
|
case 0x05: // read-only filesystem
|
||||||
return syscall.EROFS
|
return syscall.EROFS
|
||||||
case 0x03: // no such file
|
case 0x03: // no such file
|
||||||
@ -25,7 +25,7 @@ func syscallErr(err error) syscall.Errno {
|
|||||||
case -5: // input/output error
|
case -5: // input/output error
|
||||||
return syscall.EIO
|
return syscall.EIO
|
||||||
case -84: // filesystem is corrupted
|
case -84: // filesystem is corrupted
|
||||||
return syscall.ENOTRECOVERABLE // TODO
|
return syscall.ENOTRECOVERABLE
|
||||||
case -2: // no such directory entry
|
case -2: // no such directory entry
|
||||||
return syscall.ENOENT
|
return syscall.ENOENT
|
||||||
case -17: // entry already exists
|
case -17: // entry already exists
|
||||||
@ -45,28 +45,26 @@ func syscallErr(err error) syscall.Errno {
|
|||||||
case -12: // no more memory available
|
case -12: // no more memory available
|
||||||
return syscall.ENOMEM
|
return syscall.ENOMEM
|
||||||
case -61: // no attr available
|
case -61: // no attr available
|
||||||
return syscall.ENODATA // TODO
|
return syscall.ENODATA
|
||||||
case -36: // file name is too long
|
case -36: // file name is too long
|
||||||
return syscall.ENAMETOOLONG
|
return syscall.ENAMETOOLONG
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
switch err {
|
switch err {
|
||||||
case blefs.ErrFileNotExists: // file does not exist
|
case fsproto.ErrFileNotExists: // file does not exist
|
||||||
return syscall.ENOENT
|
return syscall.ENOENT
|
||||||
case blefs.ErrFileReadOnly: // file is read only
|
case fsproto.ErrFileReadOnly: // file is read only
|
||||||
return syscall.EACCES
|
return syscall.EACCES
|
||||||
case blefs.ErrFileWriteOnly: // file is write only
|
case fsproto.ErrFileWriteOnly: // file is write only
|
||||||
return syscall.EACCES
|
return syscall.EACCES
|
||||||
case blefs.ErrInvalidOffset: // invalid file offset
|
case fsproto.ErrInvalidOffset: // invalid file offset
|
||||||
return syscall.EINVAL
|
return syscall.EINVAL
|
||||||
case blefs.ErrOffsetChanged: // offset has already been changed
|
case fsproto.ErrNoRemoveRoot: // refusing to remove root directory
|
||||||
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
|
|
||||||
return syscall.EPERM
|
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"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gen2brain/dlgs"
|
"github.com/gen2brain/dlgs"
|
||||||
"github.com/knadh/koanf"
|
"github.com/knadh/koanf"
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/logger"
|
"go.elara.ws/logger"
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
)
|
)
|
||||||
@ -60,55 +59,43 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
level = logger.LogLevelInfo
|
level = logger.LogLevelInfo
|
||||||
}
|
}
|
||||||
|
log.Logger.SetLevel(level)
|
||||||
// Initialize infinitime library
|
|
||||||
infinitime.Init(k.String("bluetooth.adapter"))
|
|
||||||
// Cleanly exit after function
|
|
||||||
defer infinitime.Exit()
|
|
||||||
|
|
||||||
// Create infinitime options struct
|
// Create infinitime options struct
|
||||||
opts := &infinitime.Options{
|
opts := infinitime.Options{
|
||||||
AttemptReconnect: k.Bool("conn.reconnect"),
|
OnReconnect: func(dev *infinitime.Device) {
|
||||||
WhitelistEnabled: k.Bool("conn.whitelist.enabled"),
|
if k.Bool("on.reconnect.setTime") {
|
||||||
Whitelist: k.Strings("conn.whitelist.devices"),
|
// Set time to current time
|
||||||
OnReqPasskey: onReqPasskey,
|
err = dev.SetTime(time.Now())
|
||||||
Logger: log.Logger,
|
if err != nil {
|
||||||
LogLevel: level,
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Connect to InfiniTime with default options
|
// Connect to InfiniTime with default options
|
||||||
dev, err := infinitime.Connect(ctx, opts)
|
dev, err := infinitime.Connect(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error connecting to InfiniTime").Err(err).Send()
|
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
|
// Get firmware version
|
||||||
ver, err := dev.Version()
|
ver, err := dev.Version()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
18
maps.go
18
maps.go
@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.elara.ws/itd/internal/utils"
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
)
|
)
|
||||||
@ -100,7 +100,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
|
err = dev.SetNavFlag(infinitime.NavFlag(icon))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
||||||
continue
|
continue
|
||||||
@ -113,7 +113,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetNarrative(narrative)
|
err = dev.SetNavNarrative(narrative)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
||||||
continue
|
continue
|
||||||
@ -126,7 +126,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetManDist(manDist)
|
err = dev.SetNavManeuverDistance(manDist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
||||||
continue
|
continue
|
||||||
@ -139,7 +139,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetProgress(uint8(progress))
|
err = dev.SetNavProgress(uint8(progress))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
log.Error("Error setting flag").Err(err).Str("property", member).Send()
|
||||||
continue
|
continue
|
||||||
@ -165,7 +165,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
|
err = dev.SetNavFlag(infinitime.NavFlag(icon))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -176,7 +176,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetNarrative(narrative)
|
err = dev.SetNavNarrative(narrative)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -187,7 +187,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.Navigation.SetManDist(manDist)
|
err = dev.SetNavManeuverDistance(manDist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -198,7 +198,7 @@ func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
|
|||||||
return err
|
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
|
// pureMapsExists checks to make sure the PureMaps service exists on the bus
|
||||||
|
120
metrics.go
120
metrics.go
@ -4,10 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@ -48,82 +47,95 @@ func initMetrics(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If heart rate metrics enabled in config
|
// Watch heart rate
|
||||||
if k.Bool("metrics.heartRate.enabled") {
|
if k.Bool("metrics.heartRate.enabled") {
|
||||||
// Watch heart rate
|
err := dev.WatchHeartRate(ctx, func(heartRate uint8, err error) {
|
||||||
heartRateCh, err := dev.WatchHeartRate(ctx)
|
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 {
|
if err != nil {
|
||||||
return err
|
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 step count metrics enabled in config
|
||||||
if k.Bool("metrics.stepCount.enabled") {
|
if k.Bool("metrics.stepCount.enabled") {
|
||||||
// Watch step count
|
// 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 {
|
if err != nil {
|
||||||
return err
|
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") {
|
if k.Bool("metrics.battLevel.enabled") {
|
||||||
// Watch battery level
|
err := dev.WatchBatteryLevel(ctx, func(battLevel uint8, err error) {
|
||||||
battLevelCh, err := dev.WatchBatteryLevel(ctx)
|
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 {
|
if err != nil {
|
||||||
return err
|
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") {
|
if k.Bool("metrics.motion.enabled") {
|
||||||
// Watch motion values
|
err := dev.WatchMotion(ctx, func(motionVals infinitime.MotionValues, err error) {
|
||||||
motionCh, err := dev.WatchMotion(ctx)
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
wg.Add(1)
|
||||||
|
61
music.go
61
music.go
@ -21,8 +21,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/infinitime"
|
|
||||||
"go.elara.ws/itd/mpris"
|
"go.elara.ws/itd/mpris"
|
||||||
"go.elara.ws/itd/translit"
|
"go.elara.ws/itd/translit"
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
@ -39,51 +38,43 @@ func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) er
|
|||||||
if !firmwareUpdating {
|
if !firmwareUpdating {
|
||||||
switch ct {
|
switch ct {
|
||||||
case mpris.ChangeTypeStatus:
|
case mpris.ChangeTypeStatus:
|
||||||
dev.Music.SetStatus(val == "Playing")
|
dev.SetMusicStatus(val == "Playing")
|
||||||
case mpris.ChangeTypeTitle:
|
case mpris.ChangeTypeTitle:
|
||||||
dev.Music.SetTrack(newVal)
|
dev.SetMusicTrack(newVal)
|
||||||
case mpris.ChangeTypeAlbum:
|
case mpris.ChangeTypeAlbum:
|
||||||
dev.Music.SetAlbum(newVal)
|
dev.SetMusicAlbum(newVal)
|
||||||
case mpris.ChangeTypeArtist:
|
case mpris.ChangeTypeArtist:
|
||||||
dev.Music.SetArtist(newVal)
|
dev.SetMusicArtist(newVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for music events
|
// 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 {
|
if err != nil {
|
||||||
return err
|
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 completed initialization
|
||||||
log.Info("Initialized InfiniTime music controls").Send()
|
log.Info("Initialized InfiniTime music controls").Send()
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.elara.ws/itd/internal/utils"
|
||||||
"go.elara.ws/itd/translit"
|
"go.elara.ws/itd/translit"
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
|
337
socket.go
337
socket.go
@ -19,18 +19,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.elara.ws/drpc/muxserver"
|
"go.elara.ws/drpc/muxserver"
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/infinitime/blefs"
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.elara.ws/itd/internal/rpc"
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
"storj.io/drpc/drpcmux"
|
"storj.io/drpc/drpcmux"
|
||||||
@ -61,11 +60,7 @@ func startSocket(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fs, err := dev.FS()
|
fs := dev.FS()
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error getting BLE filesystem").Err(err).Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := drpcmux.New()
|
mux := drpcmux.New()
|
||||||
|
|
||||||
err = rpc.DRPCRegisterITD(mux, &ITD{dev})
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for heartRate := range heartRateCh {
|
select {
|
||||||
err = s.Send(&rpc.IntResponse{Value: uint32(heartRate)})
|
case <-errCh:
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
case <-s.Context().Done():
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) BatteryLevel(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for battLevel := range battLevelCh {
|
select {
|
||||||
err = s.Send(&rpc.IntResponse{Value: uint32(battLevel)})
|
case <-errCh:
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
case <-s.Context().Done():
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Motion(_ context.Context, _ *rpc.Empty) (*rpc.MotionResponse, error) {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for motionVals := range motionValsCh {
|
select {
|
||||||
err = s.Send(&rpc.MotionResponse{
|
case <-errCh:
|
||||||
X: int32(motionVals.X),
|
return err
|
||||||
Y: int32(motionVals.Y),
|
case <-s.Context().Done():
|
||||||
Z: int32(motionVals.Z),
|
return nil
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) StepCount(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for stepCount := range stepCountCh {
|
select {
|
||||||
err = s.Send(&rpc.IntResponse{Value: stepCount})
|
case <-errCh:
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
case <-s.Context().Done():
|
||||||
}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Version(_ context.Context, _ *rpc.Empty) (*rpc.StringResponse, error) {
|
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
|
return &rpc.Empty{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_FirmwareUpgradeStream) error {
|
func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_FirmwareUpgradeStream) (err error) {
|
||||||
i.dev.DFU.Reset()
|
var fwimg, initpkt *os.File
|
||||||
|
|
||||||
switch data.Type {
|
switch data.Type {
|
||||||
case rpc.FirmwareUpgradeRequest_Archive:
|
case rpc.FirmwareUpgradeRequest_Archive:
|
||||||
// If less than one file, return error
|
fwimg, initpkt, err = extractDFU(data.Files[0])
|
||||||
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])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case rpc.FirmwareUpgradeRequest_Files:
|
case rpc.FirmwareUpgradeRequest_Files:
|
||||||
// If less than two files, return error
|
|
||||||
if len(data.Files) < 2 {
|
if len(data.Files) < 2 {
|
||||||
return ErrDFUNotEnoughFiles
|
return ErrDFUNotEnoughFiles
|
||||||
}
|
}
|
||||||
// If first file is not init packet, return error
|
|
||||||
if filepath.Ext(data.Files[0]) != ".dat" {
|
if filepath.Ext(data.Files[0]) != ".dat" {
|
||||||
return ErrDFUInvalidFile
|
return ErrDFUInvalidFile
|
||||||
}
|
}
|
||||||
// If second file is not firmware image, return error
|
|
||||||
if filepath.Ext(data.Files[1]) != ".bin" {
|
if filepath.Ext(data.Files[1]) != ".bin" {
|
||||||
return ErrDFUInvalidFile
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -247,38 +277,33 @@ func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_Fi
|
|||||||
return ErrDFUInvalidUpgType
|
return ErrDFUInvalidUpgType
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
defer os.Remove(fwimg.Name())
|
||||||
for event := range i.dev.DFU.Progress() {
|
defer os.Remove(initpkt.Name())
|
||||||
_ = s.Send(&rpc.DFUProgress{
|
defer fwimg.Close()
|
||||||
Sent: int64(event.Sent),
|
defer initpkt.Close()
|
||||||
Recieved: int64(event.Received),
|
|
||||||
Total: event.Total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
firmwareUpdating = false
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set firmwareUpdating
|
|
||||||
firmwareUpdating = true
|
firmwareUpdating = true
|
||||||
|
defer func() { firmwareUpdating = false }()
|
||||||
|
|
||||||
// Start DFU
|
return i.dev.UpgradeFirmware(infinitime.DFUOptions{
|
||||||
err := i.dev.DFU.Start()
|
InitPacket: initpkt,
|
||||||
if err != nil {
|
FirmwareImage: fwimg,
|
||||||
firmwareUpdating = false
|
ProgressFunc: func(sent, received, total uint32) {
|
||||||
return err
|
_ = s.Send(&rpc.DFUProgress{
|
||||||
}
|
Sent: int64(sent),
|
||||||
|
Recieved: int64(received),
|
||||||
return nil
|
Total: int64(total),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type FS struct {
|
type FS struct {
|
||||||
dev *infinitime.Device
|
dev *infinitime.Device
|
||||||
fs *blefs.FS
|
fs *infinitime.FS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) RemoveAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
|
func (fs *FS) RemoveAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
|
||||||
fs.updateFS()
|
|
||||||
for _, path := range req.Paths {
|
for _, path := range req.Paths {
|
||||||
err := fs.fs.RemoveAll(path)
|
err := fs.fs.RemoveAll(path)
|
||||||
if err != nil {
|
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) {
|
func (fs *FS) Remove(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
|
||||||
fs.updateFS()
|
|
||||||
for _, path := range req.Paths {
|
for _, path := range req.Paths {
|
||||||
err := fs.fs.Remove(path)
|
err := fs.fs.Remove(path)
|
||||||
if err != nil {
|
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) {
|
func (fs *FS) Rename(_ context.Context, req *rpc.RenameRequest) (*rpc.Empty, error) {
|
||||||
fs.updateFS()
|
|
||||||
return &rpc.Empty{}, fs.fs.Rename(req.From, req.To)
|
return &rpc.Empty{}, fs.fs.Rename(req.From, req.To)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) MkdirAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
|
func (fs *FS) MkdirAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
|
||||||
fs.updateFS()
|
|
||||||
for _, path := range req.Paths {
|
for _, path := range req.Paths {
|
||||||
err := fs.fs.MkdirAll(path)
|
err := fs.fs.MkdirAll(path)
|
||||||
if err != nil {
|
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) {
|
func (fs *FS) Mkdir(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
|
||||||
fs.updateFS()
|
|
||||||
for _, path := range req.Paths {
|
for _, path := range req.Paths {
|
||||||
err := fs.fs.Mkdir(path)
|
err := fs.fs.Mkdir(path)
|
||||||
if err != nil {
|
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) {
|
func (fs *FS) ReadDir(_ context.Context, req *rpc.PathRequest) (*rpc.DirResponse, error) {
|
||||||
fs.updateFS()
|
|
||||||
|
|
||||||
entries, err := fs.fs.ReadDir(req.Path)
|
entries, err := fs.fs.ReadDir(req.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error {
|
||||||
fs.updateFS()
|
|
||||||
|
|
||||||
localFile, err := os.Open(req.Source)
|
localFile, err := os.Open(req.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -367,15 +384,12 @@ func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
remoteFile.ProgressFunc = func(transferred, total uint32) {
|
||||||
// For every progress event
|
_ = s.Send(&rpc.TransferProgress{
|
||||||
for sent := range remoteFile.Progress() {
|
Total: total,
|
||||||
_ = s.Send(&rpc.TransferProgress{
|
Sent: transferred,
|
||||||
Total: remoteFile.Size(),
|
})
|
||||||
Sent: sent,
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
io.Copy(remoteFile, localFile)
|
io.Copy(remoteFile, localFile)
|
||||||
localFile.Close()
|
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 {
|
func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) error {
|
||||||
fs.updateFS()
|
|
||||||
|
|
||||||
localFile, err := os.Create(req.Destination)
|
localFile, err := os.Create(req.Destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -400,15 +412,12 @@ func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) er
|
|||||||
defer localFile.Close()
|
defer localFile.Close()
|
||||||
defer remoteFile.Close()
|
defer remoteFile.Close()
|
||||||
|
|
||||||
go func() {
|
remoteFile.ProgressFunc = func(transferred, total uint32) {
|
||||||
// For every progress event
|
_ = s.Send(&rpc.TransferProgress{
|
||||||
for sent := range remoteFile.Progress() {
|
Total: total,
|
||||||
_ = s.Send(&rpc.TransferProgress{
|
Sent: transferred,
|
||||||
Total: remoteFile.Size(),
|
})
|
||||||
Sent: sent,
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = io.Copy(localFile, remoteFile)
|
_, err = io.Copy(localFile, remoteFile)
|
||||||
if err != nil {
|
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 {
|
func (fs *FS) LoadResources(req *rpc.PathRequest, s rpc.DRPCFS_LoadResourcesStream) error {
|
||||||
resFl, err := os.Open(req.Path)
|
return infinitime.LoadResources(req.Path, fs.fs, func(evt infinitime.ResourceLoadProgress) {
|
||||||
if err != nil {
|
_ = s.Send(&rpc.ResourceLoadProgress{
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
progCh, err := infinitime.LoadResources(resFl, fs.fs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for evt := range progCh {
|
|
||||||
err = s.Send(&rpc.ResourceLoadProgress{
|
|
||||||
Name: evt.Name,
|
Name: evt.Name,
|
||||||
Total: evt.Total,
|
Total: int64(evt.Total),
|
||||||
Sent: evt.Sent,
|
Sent: int64(evt.Transferred),
|
||||||
Operation: rpc.ResourceLoadProgress_Operation(evt.Operation),
|
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
|
if fwimg == nil || initpkt == nil {
|
||||||
}
|
return nil, nil, errors.New("invalid dfu archive")
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fwimg, initpkt, nil
|
||||||
}
|
}
|
||||||
|
141
weather.go
141
weather.go
@ -4,16 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.elara.ws/infinitime"
|
"go.elara.ws/itd/infinitime"
|
||||||
"go.elara.ws/infinitime/weather"
|
|
||||||
"go.elara.ws/logger/log"
|
"go.elara.ws/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +30,7 @@ type METData struct {
|
|||||||
Instant struct {
|
Instant struct {
|
||||||
Details struct {
|
Details struct {
|
||||||
AirPressure float32 `json:"air_pressure_at_sea_level"`
|
AirPressure float32 `json:"air_pressure_at_sea_level"`
|
||||||
AirTemperature float32 `json:"air_temperature"`
|
Temperature float32 `json:"air_temperature"`
|
||||||
DewPoint float32 `json:"dew_point_temperature"`
|
DewPoint float32 `json:"dew_point_temperature"`
|
||||||
CloudAreaFraction float32 `json:"cloud_area_fraction"`
|
CloudAreaFraction float32 `json:"cloud_area_fraction"`
|
||||||
FogAreaFraction float32 `json:"fog_area_fraction"`
|
FogAreaFraction float32 `json:"fog_area_fraction"`
|
||||||
@ -51,6 +48,12 @@ type METData struct {
|
|||||||
PrecipitationAmount float32 `json:"precipitation_amount"`
|
PrecipitationAmount float32 `json:"precipitation_amount"`
|
||||||
}
|
}
|
||||||
} `json:"next_1_hours"`
|
} `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
|
// OSMData represents lat/long data from
|
||||||
@ -86,9 +89,12 @@ func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done("weather")
|
defer wg.Done("weather")
|
||||||
for {
|
for {
|
||||||
_, ok := <-ctx.Done()
|
select {
|
||||||
if !ok {
|
case _, ok := <-ctx.Done():
|
||||||
return
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to get weather
|
// Attempt to get weather
|
||||||
@ -104,81 +110,28 @@ func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
|
|||||||
current := data.Properties.Timeseries[0]
|
current := data.Properties.Timeseries[0]
|
||||||
currentData := current.Data.Instant.Details
|
currentData := current.Data.Instant.Details
|
||||||
|
|
||||||
// Add temperature event
|
icon := parseSymbol(current.Data.NextHour.Summary.SymbolCode)
|
||||||
err = dev.AddWeatherEvent(weather.TemperatureEvent{
|
if icon == infinitime.WeatherIconClear {
|
||||||
TimelineHeader: weather.NewHeader(
|
switch {
|
||||||
weather.EventTypeTemperature,
|
case currentData.CloudAreaFraction > 50:
|
||||||
time.Hour,
|
icon = infinitime.WeatherIconHeavyClouds
|
||||||
),
|
case currentData.CloudAreaFraction == 50:
|
||||||
Temperature: int16(round(currentData.AirTemperature * 100)),
|
icon = infinitime.WeatherIconClouds
|
||||||
DewPoint: int16(round(currentData.DewPoint)),
|
case currentData.CloudAreaFraction > 0:
|
||||||
})
|
icon = infinitime.WeatherIconFewClouds
|
||||||
if err != nil {
|
}
|
||||||
log.Error("Error adding temperature event").Err(err).Send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add precipitation event
|
err = dev.SetCurrentWeather(infinitime.CurrentWeather{
|
||||||
err = dev.AddWeatherEvent(weather.PrecipitationEvent{
|
Time: time.Now(),
|
||||||
TimelineHeader: weather.NewHeader(
|
CurrentTemp: currentData.Temperature,
|
||||||
weather.EventTypePrecipitation,
|
MaxTemp: current.Data.Next6Hours.Details.MaxTemp,
|
||||||
time.Hour,
|
MinTemp: current.Data.Next6Hours.Details.MinTemp,
|
||||||
),
|
Location: k.String("weather.location"),
|
||||||
Type: parseSymbol(current.Data.NextHour.Summary.SymbolCode),
|
Icon: icon,
|
||||||
Amount: uint8(round(current.Data.NextHour.Details.PrecipitationAmount)),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error adding precipitation event").Err(err).Send()
|
log.Error("Error setting weather").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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset timer to 1 hour
|
// Reset timer to 1 hour
|
||||||
@ -206,6 +159,7 @@ func getLocation(ctx context.Context, loc string) (lat, lon float64, err error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.elara.ws/Elara6331/itd", strings.TrimSpace(version)))
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -257,7 +211,7 @@ func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set identifying user agent as per NMI requirements
|
// 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
|
// Perform request
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
@ -275,26 +229,19 @@ func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSymbol determines what type of precipitation a symbol code
|
// parseSymbol determines what weather icon a symbol code codes for.
|
||||||
// codes for.
|
func parseSymbol(symCode string) infinitime.WeatherIcon {
|
||||||
func parseSymbol(symCode string) weather.PrecipitationType {
|
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(symCode, "lightrain"):
|
case strings.Contains(symCode, "lightrain"):
|
||||||
return weather.PrecipitationTypeRain
|
return infinitime.WeatherIconRain
|
||||||
case strings.Contains(symCode, "rain"):
|
case strings.Contains(symCode, "rain"):
|
||||||
return weather.PrecipitationTypeRain
|
return infinitime.WeatherIconCloudsWithRain
|
||||||
case strings.Contains(symCode, "snow"):
|
case strings.Contains(symCode, "snow"),
|
||||||
return weather.PrecipitationTypeSnow
|
strings.Contains(symCode, "sleet"):
|
||||||
case strings.Contains(symCode, "sleet"):
|
return infinitime.WeatherIconSnow
|
||||||
return weather.PrecipitationTypeSleet
|
case strings.Contains(symCode, "thunder"):
|
||||||
case strings.Contains(symCode, "snow"):
|
return infinitime.WeatherIconThunderstorm
|
||||||
return weather.PrecipitationTypeSnow
|
|
||||||
default:
|
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