Compare commits
No commits in common. "master" and "master" have entirely different histories.
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
|||||||
liberapay: Elara6331
|
|
@ -14,7 +14,6 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
- riscv64
|
|
||||||
goarm:
|
goarm:
|
||||||
- 7
|
- 7
|
||||||
- id: itctl
|
- id: itctl
|
||||||
@ -44,7 +43,6 @@ 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: >-
|
||||||
@ -55,8 +53,8 @@ nfpms:
|
|||||||
{{- else }}{{.Arch}}
|
{{- else }}{{.Arch}}
|
||||||
{{- 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.arsenm.dev/Arsen6331/itd'
|
||||||
maintainer: 'Elara Ivy <elara@elara.ws>'
|
maintainer: 'Arsen Musyaelyan <arsen@arsenm.dev>'
|
||||||
license: GPLv3
|
license: GPLv3
|
||||||
formats:
|
formats:
|
||||||
- apk
|
- apk
|
||||||
@ -76,10 +74,10 @@ nfpms:
|
|||||||
mode: 0755
|
mode: 0755
|
||||||
aurs:
|
aurs:
|
||||||
- name: itd-bin
|
- name: itd-bin
|
||||||
homepage: 'https://gitea.elara.ws/Elara6331/itd'
|
homepage: 'https://gitea.arsenm.dev/Arsen6331/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 Ivy <elara@elara.ws>'
|
- 'Arsen Musyaelyan <arsen@arsenm.dev>'
|
||||||
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'
|
||||||
@ -107,11 +105,11 @@ aurs:
|
|||||||
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/itd/LICENSE"
|
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/itd/LICENSE"
|
||||||
release:
|
release:
|
||||||
gitea:
|
gitea:
|
||||||
owner: Elara6331
|
owner: Arsen6331
|
||||||
name: itd
|
name: itd
|
||||||
gitea_urls:
|
gitea_urls:
|
||||||
api: 'https://gitea.elara.ws/api/v1/'
|
api: 'https://gitea.arsenm.dev/api/v1/'
|
||||||
download: 'https://gitea.elara.ws'
|
download: 'https://gitea.arsenm.dev'
|
||||||
skip_tls_verify: false
|
skip_tls_verify: false
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
|
31
README.md
31
README.md
@ -1,17 +1,11 @@
|
|||||||
# ITD
|
# ITD
|
||||||
## InfiniTime Daemon
|
## InfiniTime Daemon
|
||||||
|
|
||||||
`itd` is a daemon that uses my infinitime [library](https://go.elara.ws/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io).
|
`itd` is a daemon that uses my infinitime [library](https://go.arsenm.dev/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io).
|
||||||
|
|
||||||
[![status-badge](https://ci.elara.ws/api/badges/Elara6331/itd/status.svg)](https://ci.elara.ws/Elara6331/itd)
|
[![status-badge](https://ci.arsenm.dev/api/badges/Arsen6331/itd/status.svg)](https://ci.arsenm.dev/Arsen6331/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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -42,19 +36,19 @@ Use the `itd-bin` or `itd-git` AUR packages.
|
|||||||
|
|
||||||
#### Debian/Ubuntu
|
#### Debian/Ubuntu
|
||||||
|
|
||||||
- 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.
|
- 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.
|
||||||
- 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.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.
|
- 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.
|
||||||
- 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.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.
|
- 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.
|
||||||
- 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`
|
||||||
|
|
||||||
@ -97,11 +91,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.elara.ws/Elara6331/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.arsenm.dev/Arsen6331/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.elara.ws/lure.sh | bash`.
|
1. Install LURE. This can be done with the following command: `curl https://www.arsenm.dev/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.
|
||||||
@ -144,6 +138,17 @@ 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.
|
||||||
|
@ -4,8 +4,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"go.elara.ws/drpc/muxconn"
|
"go.arsenm.dev/drpc/muxconn"
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
"storj.io/drpc"
|
"storj.io/drpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DFUProgress struct {
|
type DFUProgress struct {
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FSClient struct {
|
type FSClient struct {
|
||||||
@ -42,10 +42,7 @@ func (c *FSClient) Mkdir(ctx context.Context, paths ...string) error {
|
|||||||
|
|
||||||
func (c *FSClient) ReadDir(ctx context.Context, dir string) ([]FileInfo, error) {
|
func (c *FSClient) ReadDir(ctx context.Context, dir string) ([]FileInfo, error) {
|
||||||
res, err := c.client.ReadDir(ctx, &rpc.PathRequest{Path: dir})
|
res, err := c.client.ReadDir(ctx, &rpc.PathRequest{Path: dir})
|
||||||
if err != nil {
|
return convertEntries(res.Entries), err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return convertEntries(res.Entries), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertEntries(e []*rpc.FileInfo) []FileInfo {
|
func convertEntries(e []*rpc.FileInfo) []FileInfo {
|
||||||
|
@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) HeartRate(ctx context.Context) (uint8, error) {
|
func (c *Client) HeartRate(ctx context.Context) (uint8, error) {
|
||||||
|
@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) Notify(ctx context.Context, title, body string) error {
|
func (c *Client) Notify(ctx context.Context, title, body string) error {
|
||||||
|
@ -3,15 +3,15 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceOperation infinitime.ResourceOperation
|
type ResourceOperation uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceRemove = infinitime.ResourceRemove
|
ResourceOperationRemoveObsolete = infinitime.ResourceOperationRemoveObsolete
|
||||||
ResourceUpload = infinitime.ResourceUpload
|
ResourceOperationUpload = infinitime.ResourceOperationUpload
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceLoadProgress struct {
|
type ResourceLoadProgress struct {
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) SetTime(ctx context.Context, t time.Time) error {
|
func (c *Client) SetTime(ctx context.Context, t time.Time) error {
|
||||||
|
@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) WeatherUpdate(ctx context.Context) error {
|
func (c *Client) WeatherUpdate(ctx context.Context) error {
|
||||||
|
@ -3,7 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
|
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
|
||||||
|
81
calls.go
81
calls.go
@ -2,11 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.arsenm.dev/itd/internal/utils"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
||||||
@ -49,6 +50,7 @@ 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)
|
||||||
@ -69,40 +71,34 @@ func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) e
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get direction of call object using method call connection
|
|
||||||
direction, err := getDirection(conn, callObj)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error getting call direction").Err(err).Send()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if direction != MMCallDirectionIncoming {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send call notification to InfiniTime
|
// Send call notification to InfiniTime
|
||||||
err = dev.NotifyCall(phoneNum, func(cs infinitime.CallStatus) {
|
resCh, err := dev.NotifyCall(phoneNum)
|
||||||
switch cs {
|
if err != nil {
|
||||||
case infinitime.CallStatusAccepted:
|
continue
|
||||||
// Attempt to accept call
|
}
|
||||||
err = acceptCall(ctx, conn, callObj)
|
|
||||||
if err != nil {
|
go respHandlerOnce.Do(func() {
|
||||||
log.Warn("Error accepting call").Err(err).Send()
|
// 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 infinitime.CallStatusDeclined:
|
|
||||||
// Attempt to decline call
|
|
||||||
err = declineCall(ctx, conn, callObj)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Error declining call").Err(err).Send()
|
|
||||||
}
|
|
||||||
case infinitime.CallStatusMuted:
|
|
||||||
// Warn about unimplemented muting
|
|
||||||
log.Warn("Muting calls is not implemented").Send()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -135,25 +131,6 @@ func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MMCallDirection int
|
|
||||||
|
|
||||||
const (
|
|
||||||
MMCallDirectionUnknown MMCallDirection = iota
|
|
||||||
MMCallDirectionIncoming
|
|
||||||
MMCallDirectionOutgoing
|
|
||||||
)
|
|
||||||
|
|
||||||
// getDirection gets the direction of a call object using a DBus connection
|
|
||||||
func getDirection(conn *dbus.Conn, callObj dbus.BusObject) (MMCallDirection, error) {
|
|
||||||
var out MMCallDirection
|
|
||||||
// Get number property on DBus object and store return value in out
|
|
||||||
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Direction", &out)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPhoneNum accepts a call using a DBus connection
|
// getPhoneNum accepts a call using a DBus connection
|
||||||
func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
|
func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||||
// Call Accept() method on DBus object
|
// Call Accept() method on DBus object
|
||||||
|
@ -7,8 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/cheggaaa/pb/v3"
|
"github.com/cheggaaa/pb/v3"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fwUpgrade(c *cli.Context) error {
|
func fwUpgrade(c *cli.Context) error {
|
||||||
|
@ -8,9 +8,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
"go.elara.ws/logger"
|
"go.arsenm.dev/logger"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client *api.Client
|
var client *api.Client
|
||||||
|
@ -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/itd/infinitime"
|
"go.arsenm.dev/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.ResourceRemove {
|
if evt.Operation == infinitime.ResourceOperationRemoveObsolete {
|
||||||
bar.SetTemplateString(rmTmpl)
|
bar.SetTemplateString(rmTmpl)
|
||||||
bar.Set("filename", evt.Name)
|
bar.Set("filename", evt.Name)
|
||||||
} else {
|
} else {
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/storage"
|
"fyne.io/fyne/v2/storage"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
@ -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/itd/api"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
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.ResourceRemove:
|
case infinitime.ResourceOperationRemoveObsolete:
|
||||||
progressDlg.SetText("Removing " + evt.Name)
|
progressDlg.SetText("Removing " + evt.Name)
|
||||||
case infinitime.ResourceUpload:
|
case infinitime.ResourceOperationUpload:
|
||||||
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))
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/x/fyne/widget/charts"
|
"fyne.io/x/fyne/widget/charts"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"go.elara.ws/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/knadh/koanf/providers/confmap"
|
"github.com/knadh/koanf/providers/confmap"
|
||||||
"github.com/knadh/koanf/providers/env"
|
"github.com/knadh/koanf/providers/env"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"go.elara.ws/logger"
|
"go.arsenm.dev/logger"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cfgDir string
|
var cfgDir string
|
||||||
|
6
fuse.go
6
fuse.go
@ -6,9 +6,9 @@ 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/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/internal/fusefs"
|
"go.arsenm.dev/itd/internal/fusefs"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startFUSE(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
func startFUSE(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
||||||
|
21
go.mod
21
go.mod
@ -1,11 +1,9 @@
|
|||||||
module go.elara.ws/itd
|
module go.arsenm.dev/itd
|
||||||
|
|
||||||
go 1.18
|
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
|
||||||
@ -17,13 +15,13 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.17
|
github.com/mattn/go-isatty v0.0.17
|
||||||
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.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794
|
||||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae
|
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469
|
||||||
|
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df
|
||||||
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 (
|
||||||
@ -34,14 +32,15 @@ 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
|
||||||
@ -55,18 +54,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/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4 // indirect
|
github.com/sirupsen/logrus v1.9.0 // 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/tinygo-org/cbgo v0.0.4 // indirect
|
github.com/x448/float16 v0.8.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
|
||||||
@ -75,7 +74,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.11.0 // indirect
|
golang.org/x/sys v0.6.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
|
||||||
|
41
go.sum
41
go.sum
@ -107,8 +107,6 @@ 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=
|
||||||
@ -121,6 +119,7 @@ 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=
|
||||||
@ -130,6 +129,8 @@ 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=
|
||||||
@ -160,14 +161,15 @@ 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=
|
||||||
@ -238,6 +240,7 @@ 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=
|
||||||
@ -395,6 +398,8 @@ 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=
|
||||||
@ -406,6 +411,7 @@ 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=
|
||||||
@ -450,8 +456,6 @@ 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=
|
||||||
@ -459,11 +463,10 @@ 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.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.0/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=
|
||||||
@ -498,16 +501,17 @@ 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=
|
||||||
@ -525,10 +529,12 @@ 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.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d h1:ANb8YPtcxPipwKgmnW688e5PGpNaLh+22nO2LBpIPOU=
|
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794 h1:8KQpRoQmCTgDvyHFStaIiz5NUNNqMHqVlZoGvIk+OwQ=
|
||||||
go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d/go.mod h1:NDprjiVqKXQKVGzX7jp2g/jctsUbvOxz1nN15QOBEGk=
|
go.arsenm.dev/drpc v0.0.0-20230328202554-c1f2aa71e794/go.mod h1:K5cFls42m5q1RIphTVojRdXLaoCknq/kBqQt8Ow3XuA=
|
||||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae h1:d+gJUhEWSrOjrrfgeydYWEr8TTnx0DLvcVhghaOsFeE=
|
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469 h1:LsJHg+8rQSYnTE1sSCjBCACxUUVMZIOQani8J6wF2/E=
|
||||||
go.elara.ws/logger v0.0.0-20230928062203-85e135cf02ae/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM=
|
go.arsenm.dev/infinitime v0.0.0-20230104230015-512d48bc2469/go.mod h1:scUyDmLmCHn6CanGbau8yjTjzyhUbLJcsjmDCCKMIII=
|
||||||
|
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df h1:8mBHvEe7BJmpOeKSMA5YLqrGo9dCpePocTeR0C1+/2w=
|
||||||
|
go.arsenm.dev/logger v0.0.0-20230104225304-d706171ea6df/go.mod h1:RV2qydKDdoyaRkhAq8JEGvojR8eJ6bjq5WnSIlH7gYw=
|
||||||
go.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=
|
||||||
@ -720,6 +726,7 @@ 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=
|
||||||
@ -741,6 +748,7 @@ 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=
|
||||||
@ -748,8 +756,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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.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=
|
||||||
@ -812,6 +820,7 @@ 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=
|
||||||
|
@ -1,142 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
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
617
infinitime/fs.go
@ -1,617 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,173 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,108 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
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,9 @@ 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/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/infinitime/blefs"
|
||||||
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ITProperty struct {
|
type ITProperty struct {
|
||||||
@ -46,14 +47,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
myfs *infinitime.FS = nil
|
myfs *blefs.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 = dev.FS()
|
myfs, err = 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
|
||||||
@ -343,9 +344,12 @@ func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno)
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fp.ProgressFunc = func(transferred, total uint32) {
|
go func() {
|
||||||
log.Debug("FUSE Read progress").Uint32("bytes", transferred).Uint32("total", total).Send()
|
// For every progress event
|
||||||
}
|
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)
|
||||||
@ -426,9 +430,12 @@ func (f *ITNode) Open(ctx context.Context, openFlags uint32) (fh fs.FileHandle,
|
|||||||
|
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
fp.ProgressFunc = func(transferred, total uint32) {
|
go func() {
|
||||||
log.Debug("FUSE Read progress").Uint32("bytes", transferred).Uint32("total", total).Send()
|
// For every progress event
|
||||||
}
|
for sent := range fp.Progress() {
|
||||||
|
log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, 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/itd/internal/fsproto"
|
"go.arsenm.dev/infinitime/blefs"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 fsproto.Error:
|
case blefs.FSError:
|
||||||
switch err.Code {
|
switch err.Code {
|
||||||
case 0x02: // filesystem error
|
case 0x02: // filesystem error
|
||||||
return syscall.EIO
|
return syscall.EIO // TODO
|
||||||
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
|
return syscall.ENOTRECOVERABLE // TODO
|
||||||
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,26 +45,28 @@ 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
|
return syscall.ENODATA // TODO
|
||||||
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 fsproto.ErrFileNotExists: // file does not exist
|
case blefs.ErrFileNotExists: // file does not exist
|
||||||
return syscall.ENOENT
|
return syscall.ENOENT
|
||||||
case fsproto.ErrFileReadOnly: // file is read only
|
case blefs.ErrFileReadOnly: // file is read only
|
||||||
return syscall.EACCES
|
return syscall.EACCES
|
||||||
case fsproto.ErrFileWriteOnly: // file is write only
|
case blefs.ErrFileWriteOnly: // file is write only
|
||||||
return syscall.EACCES
|
return syscall.EACCES
|
||||||
case fsproto.ErrInvalidOffset: // invalid file offset
|
case blefs.ErrInvalidOffset: // invalid file offset
|
||||||
return syscall.EINVAL
|
return syscall.EINVAL
|
||||||
case fsproto.ErrNoRemoveRoot: // refusing to remove root directory
|
case blefs.ErrOffsetChanged: // offset has already been changed
|
||||||
|
return syscall.ESPIPE
|
||||||
|
case blefs.ErrReadOpen: // only one file can be opened for reading at a time
|
||||||
|
return syscall.ENFILE
|
||||||
|
case blefs.ErrWriteOpen: // only one file can be opened for writing at a time
|
||||||
|
return syscall.ENFILE
|
||||||
|
case blefs.ErrNoRemoveRoot: // refusing to remove root directory
|
||||||
return syscall.EPERM
|
return syscall.EPERM
|
||||||
case fsproto.ErrFileClosed: // cannot perform operation on closed file
|
|
||||||
return syscall.EBADF
|
|
||||||
default:
|
|
||||||
return syscall.EINVAL
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
71
main.go
71
main.go
@ -27,15 +27,16 @@ 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/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/logger"
|
"go.arsenm.dev/logger"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var k = koanf.New(".")
|
var k = koanf.New(".")
|
||||||
@ -59,43 +60,55 @@ 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{
|
||||||
OnReconnect: func(dev *infinitime.Device) {
|
AttemptReconnect: k.Bool("conn.reconnect"),
|
||||||
if k.Bool("on.reconnect.setTime") {
|
WhitelistEnabled: k.Bool("conn.whitelist.enabled"),
|
||||||
// Set time to current time
|
Whitelist: k.Strings("conn.whitelist.devices"),
|
||||||
err = dev.SetTime(time.Now())
|
OnReqPasskey: onReqPasskey,
|
||||||
if err != nil {
|
Logger: log.Logger,
|
||||||
return
|
LogLevel: level,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(opts)
|
dev, err := infinitime.Connect(ctx, 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 {
|
||||||
|
22
maps.go
22
maps.go
@ -5,9 +5,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.arsenm.dev/itd/internal/utils"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -100,7 +100,7 @@ func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dev.SetNavFlag(infinitime.NavFlag(icon))
|
err = dev.Navigation.SetFlag(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.SetNavNarrative(narrative)
|
err = dev.Navigation.SetNarrative(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.SetNavManeuverDistance(manDist)
|
err = dev.Navigation.SetManDist(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.SetNavProgress(uint8(progress))
|
err = dev.Navigation.SetProgress(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.SetNavFlag(infinitime.NavFlag(icon))
|
err = dev.Navigation.SetFlag(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.SetNavNarrative(narrative)
|
err = dev.Navigation.SetNarrative(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.SetNavManeuverDistance(manDist)
|
err = dev.Navigation.SetManDist(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.SetNavProgress(uint8(progress))
|
return dev.Navigation.SetProgress(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,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,95 +48,82 @@ func initMetrics(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch heart rate
|
// If heart rate metrics enabled in config
|
||||||
if k.Bool("metrics.heartRate.enabled") {
|
if k.Bool("metrics.heartRate.enabled") {
|
||||||
err := dev.WatchHeartRate(ctx, func(heartRate uint8, err error) {
|
// Watch heart rate
|
||||||
if err != nil {
|
heartRateCh, err := dev.WatchHeartRate(ctx)
|
||||||
// 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
|
||||||
err := dev.WatchStepCount(ctx, func(count uint32, err error) {
|
stepCountCh, err := dev.WatchStepCount(ctx)
|
||||||
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
|
||||||
// Watch step count
|
for stepCount := range stepCountCh {
|
||||||
if k.Bool("metrics.stepCount.enabled") {
|
// Get current time
|
||||||
err := dev.WatchStepCount(ctx, func(count uint32, err error) {
|
unixTime := time.Now().UnixNano()
|
||||||
if err != nil {
|
// Insert sample and time into database
|
||||||
// Handle error
|
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, stepCount)
|
||||||
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 battery level metrics enabled in config
|
||||||
if k.Bool("metrics.battLevel.enabled") {
|
if k.Bool("metrics.battLevel.enabled") {
|
||||||
err := dev.WatchBatteryLevel(ctx, func(battLevel uint8, err error) {
|
// Watch battery level
|
||||||
if err != nil {
|
battLevelCh, err := dev.WatchBatteryLevel(ctx)
|
||||||
// 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch motion values
|
// If motion metrics enabled in config
|
||||||
if k.Bool("metrics.motion.enabled") {
|
if k.Bool("metrics.motion.enabled") {
|
||||||
err := dev.WatchMotion(ctx, func(motionVals infinitime.MotionValues, err error) {
|
// Watch motion values
|
||||||
if err != nil {
|
motionCh, err := dev.WatchMotion(ctx)
|
||||||
// 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)
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.arsenm.dev/itd/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
67
music.go
67
music.go
@ -21,10 +21,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.elara.ws/itd/infinitime"
|
|
||||||
"go.elara.ws/itd/mpris"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/translit"
|
"go.arsenm.dev/itd/mpris"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/itd/translit"
|
||||||
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
||||||
@ -38,43 +39,51 @@ 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.SetMusicStatus(val == "Playing")
|
dev.Music.SetStatus(val == "Playing")
|
||||||
case mpris.ChangeTypeTitle:
|
case mpris.ChangeTypeTitle:
|
||||||
dev.SetMusicTrack(newVal)
|
dev.Music.SetTrack(newVal)
|
||||||
case mpris.ChangeTypeAlbum:
|
case mpris.ChangeTypeAlbum:
|
||||||
dev.SetMusicAlbum(newVal)
|
dev.Music.SetAlbum(newVal)
|
||||||
case mpris.ChangeTypeArtist:
|
case mpris.ChangeTypeArtist:
|
||||||
dev.SetMusicArtist(newVal)
|
dev.Music.SetArtist(newVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for music events
|
// Watch for music events
|
||||||
err := dev.WatchMusicEvents(ctx, func(event infinitime.MusicEvent, err error) {
|
musicEvtCh, err := dev.Music.WatchEvents()
|
||||||
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,10 +23,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/internal/utils"
|
"go.arsenm.dev/itd/internal/utils"
|
||||||
"go.elara.ws/itd/translit"
|
"go.arsenm.dev/itd/translit"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initNotifRelay(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
func initNotifRelay(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
|
||||||
|
347
socket.go
347
socket.go
@ -19,19 +19,20 @@
|
|||||||
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.arsenm.dev/drpc/muxserver"
|
||||||
"go.elara.ws/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/itd/internal/rpc"
|
"go.arsenm.dev/infinitime/blefs"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/itd/internal/rpc"
|
||||||
|
"go.arsenm.dev/logger/log"
|
||||||
"storj.io/drpc/drpcmux"
|
"storj.io/drpc/drpcmux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,7 +61,11 @@ func startSocket(ctx context.Context, wg WaitGroup, dev *infinitime.Device) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fs := dev.FS()
|
fs, err := 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})
|
||||||
@ -94,29 +99,19 @@ 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 {
|
||||||
errCh := make(chan error)
|
heartRateCh, err := i.dev.WatchHeartRate(s.Context())
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
for heartRate := range heartRateCh {
|
||||||
case <-errCh:
|
err = s.Send(&rpc.IntResponse{Value: uint32(heartRate)})
|
||||||
return err
|
if err != nil {
|
||||||
case <-s.Context().Done():
|
return err
|
||||||
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) {
|
||||||
@ -125,29 +120,19 @@ 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 {
|
||||||
errCh := make(chan error)
|
battLevelCh, err := i.dev.WatchBatteryLevel(s.Context())
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
for battLevel := range battLevelCh {
|
||||||
case <-errCh:
|
err = s.Send(&rpc.IntResponse{Value: uint32(battLevel)})
|
||||||
return err
|
if err != nil {
|
||||||
case <-s.Context().Done():
|
return err
|
||||||
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) {
|
||||||
@ -160,33 +145,23 @@ 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 {
|
||||||
errCh := make(chan error)
|
motionValsCh, err := i.dev.WatchMotion(s.Context())
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
for motionVals := range motionValsCh {
|
||||||
case <-errCh:
|
err = s.Send(&rpc.MotionResponse{
|
||||||
return err
|
X: int32(motionVals.X),
|
||||||
case <-s.Context().Done():
|
Y: int32(motionVals.Y),
|
||||||
return nil
|
Z: int32(motionVals.Z),
|
||||||
|
})
|
||||||
|
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) {
|
||||||
@ -195,29 +170,19 @@ 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 {
|
||||||
errCh := make(chan error)
|
stepCountCh, err := i.dev.WatchStepCount(s.Context())
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
for stepCount := range stepCountCh {
|
||||||
case <-errCh:
|
err = s.Send(&rpc.IntResponse{Value: stepCount})
|
||||||
return err
|
if err != nil {
|
||||||
case <-s.Context().Done():
|
return err
|
||||||
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) {
|
||||||
@ -242,34 +207,39 @@ 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) (err error) {
|
func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_FirmwareUpgradeStream) error {
|
||||||
var fwimg, initpkt *os.File
|
i.dev.DFU.Reset()
|
||||||
|
|
||||||
switch data.Type {
|
switch data.Type {
|
||||||
case rpc.FirmwareUpgradeRequest_Archive:
|
case rpc.FirmwareUpgradeRequest_Archive:
|
||||||
fwimg, initpkt, err = extractDFU(data.Files[0])
|
// If less than one file, return error
|
||||||
|
if len(data.Files) < 1 {
|
||||||
|
return ErrDFUNotEnoughFiles
|
||||||
|
}
|
||||||
|
// If file is not zip archive, return error
|
||||||
|
if filepath.Ext(data.Files[0]) != ".zip" {
|
||||||
|
return ErrDFUInvalidFile
|
||||||
|
}
|
||||||
|
// Load DFU archive
|
||||||
|
err := i.dev.DFU.LoadArchive(data.Files[0])
|
||||||
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
|
||||||
initpkt, err = os.Open(data.Files[0])
|
err := i.dev.DFU.LoadFiles(data.Files[0], data.Files[1])
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fwimg, err = os.Open(data.Files[1])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -277,33 +247,38 @@ func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_Fi
|
|||||||
return ErrDFUInvalidUpgType
|
return ErrDFUInvalidUpgType
|
||||||
}
|
}
|
||||||
|
|
||||||
defer os.Remove(fwimg.Name())
|
go func() {
|
||||||
defer os.Remove(initpkt.Name())
|
for event := range i.dev.DFU.Progress() {
|
||||||
defer fwimg.Close()
|
|
||||||
defer initpkt.Close()
|
|
||||||
|
|
||||||
firmwareUpdating = true
|
|
||||||
defer func() { firmwareUpdating = false }()
|
|
||||||
|
|
||||||
return i.dev.UpgradeFirmware(infinitime.DFUOptions{
|
|
||||||
InitPacket: initpkt,
|
|
||||||
FirmwareImage: fwimg,
|
|
||||||
ProgressFunc: func(sent, received, total uint32) {
|
|
||||||
_ = s.Send(&rpc.DFUProgress{
|
_ = s.Send(&rpc.DFUProgress{
|
||||||
Sent: int64(sent),
|
Sent: int64(event.Sent),
|
||||||
Recieved: int64(received),
|
Recieved: int64(event.Received),
|
||||||
Total: int64(total),
|
Total: event.Total,
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
})
|
|
||||||
|
firmwareUpdating = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set firmwareUpdating
|
||||||
|
firmwareUpdating = true
|
||||||
|
|
||||||
|
// Start DFU
|
||||||
|
err := i.dev.DFU.Start()
|
||||||
|
if err != nil {
|
||||||
|
firmwareUpdating = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type FS struct {
|
type FS struct {
|
||||||
dev *infinitime.Device
|
dev *infinitime.Device
|
||||||
fs *infinitime.FS
|
fs *blefs.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 {
|
||||||
@ -314,6 +289,7 @@ 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 {
|
||||||
@ -324,10 +300,12 @@ 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 {
|
||||||
@ -338,6 +316,7 @@ 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 {
|
||||||
@ -348,6 +327,8 @@ 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
|
||||||
@ -369,6 +350,8 @@ 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
|
||||||
@ -384,12 +367,15 @@ func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteFile.ProgressFunc = func(transferred, total uint32) {
|
go func() {
|
||||||
_ = s.Send(&rpc.TransferProgress{
|
// For every progress event
|
||||||
Total: total,
|
for sent := range remoteFile.Progress() {
|
||||||
Sent: transferred,
|
_ = s.Send(&rpc.TransferProgress{
|
||||||
})
|
Total: remoteFile.Size(),
|
||||||
}
|
Sent: sent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
io.Copy(remoteFile, localFile)
|
io.Copy(remoteFile, localFile)
|
||||||
localFile.Close()
|
localFile.Close()
|
||||||
@ -399,6 +385,8 @@ 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
|
||||||
@ -412,12 +400,15 @@ func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) er
|
|||||||
defer localFile.Close()
|
defer localFile.Close()
|
||||||
defer remoteFile.Close()
|
defer remoteFile.Close()
|
||||||
|
|
||||||
remoteFile.ProgressFunc = func(transferred, total uint32) {
|
go func() {
|
||||||
_ = s.Send(&rpc.TransferProgress{
|
// For every progress event
|
||||||
Total: total,
|
for sent := range remoteFile.Progress() {
|
||||||
Sent: transferred,
|
_ = s.Send(&rpc.TransferProgress{
|
||||||
})
|
Total: remoteFile.Size(),
|
||||||
}
|
Sent: sent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
_, err = io.Copy(localFile, remoteFile)
|
_, err = io.Copy(localFile, remoteFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -428,86 +419,42 @@ 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 {
|
||||||
return infinitime.LoadResources(req.Path, fs.fs, func(evt infinitime.ResourceLoadProgress) {
|
resFl, err := os.Open(req.Path)
|
||||||
_ = s.Send(&rpc.ResourceLoadProgress{
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
progCh, err := infinitime.LoadResources(resFl, fs.fs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for evt := range progCh {
|
||||||
|
err = s.Send(&rpc.ResourceLoadProgress{
|
||||||
Name: evt.Name,
|
Name: evt.Name,
|
||||||
Total: int64(evt.Total),
|
Total: evt.Total,
|
||||||
Sent: int64(evt.Transferred),
|
Sent: evt.Sent,
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fwimg == nil || initpkt == nil {
|
return nil
|
||||||
return nil, nil, errors.New("invalid dfu archive")
|
}
|
||||||
}
|
|
||||||
|
func (fs *FS) updateFS() {
|
||||||
return fwimg, initpkt, nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WaitGroup struct {
|
type WaitGroup struct {
|
||||||
|
147
weather.go
147
weather.go
@ -4,14 +4,17 @@ 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/itd/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.elara.ws/logger/log"
|
"go.arsenm.dev/infinitime/weather"
|
||||||
|
"go.arsenm.dev/logger/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// METResponse represents a response from
|
// METResponse represents a response from
|
||||||
@ -30,7 +33,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"`
|
||||||
Temperature float32 `json:"air_temperature"`
|
AirTemperature 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"`
|
||||||
@ -48,12 +51,6 @@ 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
|
||||||
@ -89,12 +86,9 @@ 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 {
|
||||||
select {
|
_, ok := <-ctx.Done()
|
||||||
case _, ok := <-ctx.Done():
|
if !ok {
|
||||||
if !ok {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to get weather
|
// Attempt to get weather
|
||||||
@ -110,28 +104,81 @@ 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
|
||||||
|
|
||||||
icon := parseSymbol(current.Data.NextHour.Summary.SymbolCode)
|
// Add temperature event
|
||||||
if icon == infinitime.WeatherIconClear {
|
err = dev.AddWeatherEvent(weather.TemperatureEvent{
|
||||||
switch {
|
TimelineHeader: weather.NewHeader(
|
||||||
case currentData.CloudAreaFraction > 50:
|
weather.EventTypeTemperature,
|
||||||
icon = infinitime.WeatherIconHeavyClouds
|
time.Hour,
|
||||||
case currentData.CloudAreaFraction == 50:
|
),
|
||||||
icon = infinitime.WeatherIconClouds
|
Temperature: int16(round(currentData.AirTemperature * 100)),
|
||||||
case currentData.CloudAreaFraction > 0:
|
DewPoint: int16(round(currentData.DewPoint)),
|
||||||
icon = infinitime.WeatherIconFewClouds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dev.SetCurrentWeather(infinitime.CurrentWeather{
|
|
||||||
Time: time.Now(),
|
|
||||||
CurrentTemp: currentData.Temperature,
|
|
||||||
MaxTemp: current.Data.Next6Hours.Details.MaxTemp,
|
|
||||||
MinTemp: current.Data.Next6Hours.Details.MinTemp,
|
|
||||||
Location: k.String("weather.location"),
|
|
||||||
Icon: icon,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error setting weather").Err(err).Send()
|
log.Error("Error adding temperature event").Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add precipitation event
|
||||||
|
err = dev.AddWeatherEvent(weather.PrecipitationEvent{
|
||||||
|
TimelineHeader: weather.NewHeader(
|
||||||
|
weather.EventTypePrecipitation,
|
||||||
|
time.Hour,
|
||||||
|
),
|
||||||
|
Type: parseSymbol(current.Data.NextHour.Summary.SymbolCode),
|
||||||
|
Amount: uint8(round(current.Data.NextHour.Details.PrecipitationAmount)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error adding precipitation event").Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add wind event
|
||||||
|
err = dev.AddWeatherEvent(weather.WindEvent{
|
||||||
|
TimelineHeader: weather.NewHeader(
|
||||||
|
weather.EventTypeWind,
|
||||||
|
time.Hour,
|
||||||
|
),
|
||||||
|
SpeedMin: uint8(round(currentData.WindSpeed)),
|
||||||
|
SpeedMax: uint8(round(currentData.WindSpeed)),
|
||||||
|
DirectionMin: uint8(round(currentData.WindDirection)),
|
||||||
|
DirectionMax: uint8(round(currentData.WindDirection)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error adding wind event").Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add cloud event
|
||||||
|
err = dev.AddWeatherEvent(weather.CloudsEvent{
|
||||||
|
TimelineHeader: weather.NewHeader(
|
||||||
|
weather.EventTypeClouds,
|
||||||
|
time.Hour,
|
||||||
|
),
|
||||||
|
Amount: uint8(round(currentData.CloudAreaFraction)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error adding clouds event").Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add humidity event
|
||||||
|
err = dev.AddWeatherEvent(weather.HumidityEvent{
|
||||||
|
TimelineHeader: weather.NewHeader(
|
||||||
|
weather.EventTypeHumidity,
|
||||||
|
time.Hour,
|
||||||
|
),
|
||||||
|
Humidity: uint8(round(currentData.RelativeHumidity)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error adding humidity event").Err(err).Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pressure event
|
||||||
|
err = dev.AddWeatherEvent(weather.PressureEvent{
|
||||||
|
TimelineHeader: weather.NewHeader(
|
||||||
|
weather.EventTypePressure,
|
||||||
|
time.Hour,
|
||||||
|
),
|
||||||
|
Pressure: int16(round(currentData.AirPressure)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error adding pressure event").Err(err).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset timer to 1 hour
|
// Reset timer to 1 hour
|
||||||
@ -159,7 +206,6 @@ 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
|
||||||
@ -211,7 +257,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.elara.ws/Elara6331/itd", strings.TrimSpace(version)))
|
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.arsenm.dev/Arsen6331/itd", version))
|
||||||
|
|
||||||
// Perform request
|
// Perform request
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
@ -229,19 +275,26 @@ func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSymbol determines what weather icon a symbol code codes for.
|
// parseSymbol determines what type of precipitation a symbol code
|
||||||
func parseSymbol(symCode string) infinitime.WeatherIcon {
|
// codes for.
|
||||||
|
func parseSymbol(symCode string) weather.PrecipitationType {
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(symCode, "lightrain"):
|
case strings.Contains(symCode, "lightrain"):
|
||||||
return infinitime.WeatherIconRain
|
return weather.PrecipitationTypeRain
|
||||||
case strings.Contains(symCode, "rain"):
|
case strings.Contains(symCode, "rain"):
|
||||||
return infinitime.WeatherIconCloudsWithRain
|
return weather.PrecipitationTypeRain
|
||||||
case strings.Contains(symCode, "snow"),
|
case strings.Contains(symCode, "snow"):
|
||||||
strings.Contains(symCode, "sleet"):
|
return weather.PrecipitationTypeSnow
|
||||||
return infinitime.WeatherIconSnow
|
case strings.Contains(symCode, "sleet"):
|
||||||
case strings.Contains(symCode, "thunder"):
|
return weather.PrecipitationTypeSleet
|
||||||
return infinitime.WeatherIconThunderstorm
|
case strings.Contains(symCode, "snow"):
|
||||||
|
return weather.PrecipitationTypeSnow
|
||||||
default:
|
default:
|
||||||
return infinitime.WeatherIconClear
|
return weather.PrecipitationTypeNone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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