Compare commits
130 Commits
Author | SHA1 | Date | |
---|---|---|---|
03f3968fe1 | |||
dea92c6404 | |||
006f245c10 | |||
d232340edd | |||
c6458720e9 | |||
1e072a3540 | |||
f639fef992 | |||
2d0db1dcf1 | |||
4efa4380c4 | |||
fca64afbf3 | |||
cf24c5ace8 | |||
a25b2e3e62 | |||
2d0b64d92f | |||
5efafe9be7 | |||
271510d528 | |||
4d72a063b2 | |||
643245f16c | |||
6f87980d4b | |||
851f1975d6 | |||
5e66fe82ac | |||
645541e079 | |||
1012be6e5b | |||
5973290d6c | |||
19bacf29b2 | |||
a78650e526 | |||
71e9caf0bc | |||
1f5a6365bc | |||
958f2af516 | |||
60f1eedc9a | |||
c05147518d | |||
422f844943 | |||
66618e5bf0 | |||
0c2e57ced0 | |||
6d9f6fc6e6 | |||
0cbd6a48ae | |||
b614138f6b | |||
3a0491f069 | |||
093a5632c7 | |||
91662e6f38 | |||
931966bf1e | |||
ed01700e26 | |||
e9269e8eb8 | |||
52b85ab361 | |||
bc45943bdc | |||
6933f45683 | |||
0f22d67395 | |||
6da03181a9 | |||
44a25625da | |||
14a38351e4 | |||
4c27f424b2 | |||
86fbef2e8a | |||
7b8658e072 | |||
73c46cfa66 | |||
1e0f1c5b76 | |||
78b5ca1de8 | |||
b0c4574481 | |||
01975f207c | |||
428e7967c1 | |||
56dbf0540e | |||
240e7a5ee4 | |||
625805fe96 | |||
4b6f7d408e | |||
9034ef7c6b | |||
9939f724c4 | |||
8dce33f7b1 | |||
563009c44d | |||
d4a8a9f8c9 | |||
7fd9af3288 | |||
4508559bfd | |||
0cdf8a4bed | |||
2af6c1887f | |||
3a3f95acdf | |||
d318c584da | |||
c8c617c10a | |||
365414f951 | |||
9b04d06560 | |||
23e9195e70 | |||
cd68fbd7f3 | |||
205a041758 | |||
62597f70ee | |||
32bb141244 | |||
f28c68438a | |||
aa90e9eb26 | |||
553709ce8d | |||
2ded0d36b1 | |||
a885eacc70 | |||
9e63401db3 | |||
2f14e70721 | |||
614d14e399 | |||
c08ddfd810 | |||
4bdb82b1bc | |||
b4d302caf6 | |||
4b2694ee0d | |||
4c36144b0b | |||
e88dea40fb | |||
23b9cfe8a3 | |||
518fe74e96 | |||
27aabdceba | |||
c019d7523b | |||
03c3c6b22f | |||
69d1027f01 | |||
873df67d1f | |||
a9ef386883 | |||
24cfda82d7 | |||
655af5c446 | |||
b363a20a9d | |||
f5d326124d | |||
cb8fb2c0bc | |||
38119435f1 | |||
034a69c12f | |||
70006a3d7b | |||
8aada58d64 | |||
5d231207cd | |||
584d9426e6 | |||
7a772a5458 | |||
079c733b60 | |||
e24a8e9088 | |||
0b5d777077 | |||
75327286ef | |||
099b0cd849 | |||
c9c00e0072 | |||
b2ffb2062a | |||
2e8c825fff | |||
7b870950d1 | |||
3a877c41a4 | |||
04fb390bee | |||
50b17d3266 | |||
763d408405 | |||
fbb7cd9bc1 | |||
f1b7f70313 |
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
/itctl
|
||||
/itd
|
||||
/itgui
|
||||
/version.txt
|
||||
dist/
|
||||
|
108
.goreleaser.yaml
Normal file
@ -0,0 +1,108 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- id: itd
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
binary: itd
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- id: itctl
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
main: ./cmd/itctl
|
||||
binary: itctl
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
archives:
|
||||
- replacements:
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
arm64: aarch64
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- itd.toml
|
||||
- itd.service
|
||||
nfpms:
|
||||
- id: itd
|
||||
file_name_template: '{{.PackageName}}-{{.Version}}-{{.Os}}-{{.Arch}}'
|
||||
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||
replacements:
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
arm64: aarch64
|
||||
homepage: 'https://gitea.arsenm.dev/Arsen6331/itd'
|
||||
maintainer: 'Arsen Musyaelyan <arsen@arsenm.dev>'
|
||||
license: GPLv3
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
dependencies:
|
||||
- dbus
|
||||
- bluez
|
||||
- pulseaudio-utils
|
||||
contents:
|
||||
- src: itd.toml
|
||||
dst: /etc/itd.toml
|
||||
type: "config|noreplace"
|
||||
- src: itd.service
|
||||
dst: /usr/lib/systemd/user/itd.service
|
||||
aurs:
|
||||
- name: itd-bin
|
||||
homepage: 'https://gitea.arsenm.dev/Arsen6331/itd'
|
||||
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||
maintainers:
|
||||
- 'Arsen Musyaelyan <arsen@arsenm.dev>'
|
||||
license: GPLv3
|
||||
private_key: '{{ .Env.AUR_KEY }}'
|
||||
git_url: 'ssh://aur@aur.archlinux.org/itd-bin.git'
|
||||
provides:
|
||||
- itd
|
||||
- itctl
|
||||
conflicts:
|
||||
- itd
|
||||
- itctl
|
||||
depends:
|
||||
- dbus
|
||||
- bluez
|
||||
- libpulse
|
||||
package: |-
|
||||
# binaries
|
||||
install -Dm755 "./itd" "${pkgdir}/usr/bin/itd"
|
||||
install -Dm755 "./itctl" "${pkgdir}/usr/bin/itctl"
|
||||
|
||||
# service
|
||||
install -Dm644 "./itd.service" ${pkgdir}/usr/lib/systemd/user/itd.service
|
||||
|
||||
# config
|
||||
install -Dm644 "./itd.toml" ${pkgdir}/etc/itd.toml
|
||||
|
||||
# license
|
||||
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/itd/LICENSE"
|
||||
release:
|
||||
gitea:
|
||||
owner: Arsen6331
|
||||
name: itd
|
||||
gitea_urls:
|
||||
api: 'https://gitea.arsenm.dev/api/v1/'
|
||||
download: 'https://gitea.arsenm.dev'
|
||||
skip_tls_verify: false
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
10
Makefile
@ -3,13 +3,14 @@ BIN_PREFIX = $(DESTDIR)$(PREFIX)/bin
|
||||
SERVICE_PREFIX = $(DESTDIR)$(PREFIX)/lib/systemd/user
|
||||
CFG_PREFIX = $(DESTDIR)/etc
|
||||
|
||||
all:
|
||||
go build $(GOFLAGS)
|
||||
go build ./cmd/itctl $(GOFLAGS)
|
||||
all: version.txt
|
||||
go build
|
||||
go build ./cmd/itctl
|
||||
|
||||
clean:
|
||||
rm -f itctl
|
||||
rm -f itd
|
||||
rm -f version.txt
|
||||
|
||||
install:
|
||||
install -Dm755 ./itd $(BIN_PREFIX)/itd
|
||||
@ -23,4 +24,7 @@ uninstall:
|
||||
rm $(SERVICE_PREFIX)/itd.service
|
||||
rm $(CFG_PREFIX)/itd.toml
|
||||
|
||||
version.txt:
|
||||
printf "r%s.%s" "$(shell git rev-list --count HEAD)" "$(shell git rev-parse --short HEAD)" > version.txt
|
||||
|
||||
.PHONY: all clean install uninstall
|
102
README.md
@ -3,9 +3,9 @@
|
||||
|
||||
`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).
|
||||
|
||||
[![Build status](https://ci.appveyor.com/api/projects/status/xgj5sobw76ndqaod?svg=true)](https://ci.appveyor.com/project/moussaelianarsen/itd)
|
||||
[![Binary downloads](https://img.shields.io/badge/download-binary-orange)](https://minio.arsenm.dev/minio/itd/)
|
||||
[![AUR package](https://img.shields.io/aur/version/itd-git?label=itd-git&logo=archlinux)](https://aur.archlinux.org/packages/itd-git/)
|
||||
[![Build status](https://ci.appveyor.com/api/projects/status/01qpwa2bn7c7fdi2?svg=true)](https://ci.appveyor.com/project/moussaelianarsen/itd-7t6ko)
|
||||
[![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/)
|
||||
|
||||
---
|
||||
|
||||
@ -19,6 +19,38 @@
|
||||
- Set current time
|
||||
- Control socket
|
||||
- Firmware upgrades
|
||||
- Weather
|
||||
- BLE Filesystem
|
||||
|
||||
---
|
||||
|
||||
### Installation
|
||||
|
||||
Since ITD 0.0.7, packages are built and uploaded whenever a new release is created.
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
Use the `itd-bin` or `itd-git` AUR packages.
|
||||
|
||||
#### Debian/Ubuntu
|
||||
|
||||
- Go to the [latest release](https://gitea.arsenm.dev/Arsen6331/itd/releases/latest) and download the `.deb` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
|
||||
- Run `sudo apt install <package>`, replacing `<package>` with the path to the downloaded file. Note: relative paths must begin with `./`.
|
||||
- Example: `sudo apt install ~/Downloads/itd-0.0.7-linux-aarch64.deb`
|
||||
|
||||
#### Fedora
|
||||
|
||||
- Go to the [latest release](https://gitea.arsenm.dev/Arsen6331/itd/releases/latest) and download the `.rpm` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
|
||||
- Run `sudo dnf install <package>`, replacing `<package>` with the path to the downloaded file.
|
||||
- Example: `sudo dnf install ~/Downloads/itd-0.0.7-linux-aarch64.rpm`
|
||||
|
||||
#### Alpine (and postmarketOS)
|
||||
|
||||
- Go to the [latest release](https://gitea.arsenm.dev/Arsen6331/itd/releases/latest) and download the `.apk` package for your CPU architecture. You can find your architecture by running `uname -m` in the terminal.
|
||||
- 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`
|
||||
|
||||
Note: `--allow-untrusted` is required because ITD isn't part of a repository, and therefore is not signed.
|
||||
|
||||
---
|
||||
|
||||
@ -26,15 +58,13 @@
|
||||
|
||||
This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directly control the daemon and, by extension, the connected watch.
|
||||
|
||||
The socket accepts JSON requests. For example, sending a notification looks like this:
|
||||
The socket uses my [lrpc](https://gitea.arsenm.dev/Arsen6331/lrpc) library for requests. This library accepts requests in msgpack, with the following format:
|
||||
|
||||
```json
|
||||
{"type": 5, "data": {"title": "title1", "body": "body1"}}
|
||||
{"Receiver": "ITD", "Method": "Notify", "Arg": {"title": "title1", "body": "body1"}, "ID": "some-id-here"}
|
||||
```
|
||||
|
||||
It will return a JSON response. A response can have 3 fields: `error`, `msg`, and `value`. Error is a boolean that signals whether an error was returned. If error is true, the msg field will contain the error. Value can contain any data and depends on what the request was.
|
||||
|
||||
The various request types and their data requirements can be seen in `internal/types`. I can make separate docs for it if I get enough requests.
|
||||
It will return a msgpack response, the format of which can be found [here](https://gitea.arsenm.dev/Arsen6331/lrpc/src/branch/master/internal/types/types.go#L30). The response will have the same ID as was sent in the request in order to allow the client to keep track of which request the response belongs to.
|
||||
|
||||
---
|
||||
|
||||
@ -55,11 +85,12 @@ Since the PineTime does not have enough space to store all unicode glyphs, it on
|
||||
- Lithuanian
|
||||
- Estonian
|
||||
- Icelandic
|
||||
- Czeck
|
||||
- Czech
|
||||
- French
|
||||
- Armenian
|
||||
- Korean
|
||||
- Chinese
|
||||
- Romanian
|
||||
- Emoji
|
||||
|
||||
Place the desired map names in an array as `notifs.translit.use`. They will be evaluated in order. You can also put custom transliterations in `notifs.translit.custom`. These take priority over any other maps. The `notifs.translit` config section should look like this:
|
||||
@ -104,48 +135,47 @@ Use "itctl [command] --help" for more information about a command.
|
||||
|
||||
### `itgui`
|
||||
|
||||
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [fyne library](https://fyne.io/) for Go. It can be compiled by running:
|
||||
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [Fyne library](https://fyne.io/) for Go.
|
||||
|
||||
#### Compilation
|
||||
|
||||
Before compiling, certain prerequisites must be installed. These are listed on the following page: https://developer.fyne.io/started/#prerequisites
|
||||
|
||||
It can be compiled by running:
|
||||
|
||||
```shell
|
||||
go build ./cmd/itgui
|
||||
```
|
||||
|
||||
#### Cross-compilation
|
||||
|
||||
Due to the use of OpenGL, cross-compilation of `itgui` isn't as simple as that of `itd` and `itctl`. The following guide from the Fyne website should work for `itgui`: https://developer.fyne.io/started/cross-compiling.
|
||||
|
||||
#### Screenshots
|
||||
|
||||
![Info tab](https://i.imgur.com/okxG9EI.png)
|
||||
![Info tab](cmd/itgui/screenshots/info.png)
|
||||
|
||||
![Notify tab](https://i.imgur.com/DrVhOAq.png)
|
||||
![Motion tab](cmd/itgui/screenshots/motion.png)
|
||||
|
||||
![Set time tab](https://i.imgur.com/j9civeY.png)
|
||||
![Notify tab](cmd/itgui/screenshots/notify.png)
|
||||
|
||||
![Upgrade tab](https://i.imgur.com/1KY6fG4.png)
|
||||
![FS tab](cmd/itgui/screenshots/fs.png)
|
||||
|
||||
![Upgrade in progress](https://i.imgur.com/w5qbWAw.png)
|
||||
![FS mkdir](cmd/itgui/screenshots/mkdir.png)
|
||||
|
||||
---
|
||||
![Time tab](cmd/itgui/screenshots/time.png)
|
||||
|
||||
#### Interactive mode
|
||||
![Firmware tab](cmd/itgui/screenshots/firmware.png)
|
||||
|
||||
Running `itctl` by itself will open interactive mode. It's essentially a shell where you can enter commands. For example:
|
||||
![Upgrade in progress](cmd/itgui/screenshots/progress.png)
|
||||
|
||||
```
|
||||
$ itctl
|
||||
itctl> fw ver
|
||||
1.3.0
|
||||
itctl> get batt
|
||||
81%
|
||||
itctl> get heart
|
||||
92 BPM
|
||||
itctl> set time 2021-08-22T00:06:18-07:00
|
||||
itctl> set time now
|
||||
itctl> exit
|
||||
```
|
||||
![Metrics tab](cmd/itgui/screenshots/metrics.png)
|
||||
|
||||
---
|
||||
|
||||
### 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.16 or newer for the `io/fs` module.
|
||||
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
|
||||
@ -177,7 +207,7 @@ To cross compile, simply set the go environment variables. For example, for Pine
|
||||
make GOOS=linux GOARCH=arm64
|
||||
```
|
||||
|
||||
This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the PinePhone. This daemon only runs on Linux due to the library's dependencies (`dbus`, `bluez`, and `playerctl` specifically).
|
||||
This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the PinePhone. This daemon only runs on Linux due to the library's dependencies (`dbus`, and `bluez` specifically).
|
||||
|
||||
---
|
||||
|
||||
@ -186,3 +216,11 @@ This will compile `itd` and `itctl` for Linux aarch64 which is what runs on the
|
||||
This daemon places a config file at `/etc/itd.toml`. This is the global config. `itd` will also look for a config at `~/.config/itd.toml`.
|
||||
|
||||
Most of the time, the daemon does not need to be restarted for config changes to take effect.
|
||||
|
||||
---
|
||||
|
||||
### Attribution
|
||||
|
||||
Location data from OpenStreetMap Nominatim, © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors
|
||||
|
||||
Weather data from the [Norwegian Meteorological Institute](https://www.met.no/en)
|
37
api/api.go
Normal file
@ -0,0 +1,37 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"go.arsenm.dev/lrpc/client"
|
||||
"go.arsenm.dev/lrpc/codec"
|
||||
)
|
||||
|
||||
const DefaultAddr = "/tmp/itd/socket"
|
||||
|
||||
type Client struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func New(sockPath string) (*Client, error) {
|
||||
conn, err := net.Dial("unix", sockPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &Client{
|
||||
client: client.New(conn, codec.Default),
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func NewFromConn(conn io.ReadWriteCloser) *Client {
|
||||
return &Client{
|
||||
client: client.New(conn, codec.Default),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.client.Close()
|
||||
}
|
133
api/client.go
@ -1,133 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
// Default socket address
|
||||
const DefaultAddr = "/tmp/itd/socket"
|
||||
|
||||
// Client is the socket API client
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
respCh chan types.Response
|
||||
heartRateCh chan types.Response
|
||||
battLevelCh chan types.Response
|
||||
stepCountCh chan types.Response
|
||||
motionCh chan types.Response
|
||||
dfuProgressCh chan types.Response
|
||||
}
|
||||
|
||||
// New creates a new client and sets it up
|
||||
func New(addr string) (*Client, error) {
|
||||
conn, err := net.Dial("unix", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := &Client{
|
||||
conn: conn,
|
||||
respCh: make(chan types.Response, 5),
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
var res types.Response
|
||||
err = json.Unmarshal(scanner.Bytes(), &res)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out.handleResp(res)
|
||||
}
|
||||
}()
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
err := c.conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
close(c.respCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
// request sends a request to itd and waits for and returns the response
|
||||
func (c *Client) request(req types.Request) (types.Response, error) {
|
||||
// Encode request into connection
|
||||
err := json.NewEncoder(c.conn).Encode(req)
|
||||
if err != nil {
|
||||
return types.Response{}, err
|
||||
}
|
||||
|
||||
res := <-c.respCh
|
||||
|
||||
if res.Error {
|
||||
return res, errors.New(res.Message)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// requestNoRes sends a request to itd and does not wait for the response
|
||||
func (c *Client) requestNoRes(req types.Request) error {
|
||||
// Encode request into connection
|
||||
err := json.NewEncoder(c.conn).Encode(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleResp handles the received response as needed
|
||||
func (c *Client) handleResp(res types.Response) error {
|
||||
switch res.Type {
|
||||
case types.ReqTypeWatchHeartRate:
|
||||
c.heartRateCh <- res
|
||||
case types.ReqTypeWatchBattLevel:
|
||||
c.battLevelCh <- res
|
||||
case types.ReqTypeWatchStepCount:
|
||||
c.stepCountCh <- res
|
||||
case types.ReqTypeWatchMotion:
|
||||
c.motionCh <- res
|
||||
case types.ReqTypeFwUpgrade:
|
||||
c.dfuProgressCh <- res
|
||||
default:
|
||||
c.respCh <- res
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeUint8(val interface{}) uint8 {
|
||||
return uint8(val.(float64))
|
||||
}
|
||||
|
||||
func decodeUint32(val interface{}) uint32 {
|
||||
return uint32(val.(float64))
|
||||
}
|
||||
|
||||
func decodeMotion(val interface{}) (infinitime.MotionValues, error) {
|
||||
out := infinitime.MotionValues{}
|
||||
err := mapstructure.Decode(val, &out)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func decodeDFUProgress(val interface{}) (DFUProgress, error) {
|
||||
out := DFUProgress{}
|
||||
err := mapstructure.Decode(val, &out)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
26
api/firmware.go
Normal file
@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
func (c *Client) FirmwareUpgrade(ctx context.Context, upgType UpgradeType, files ...string) (chan infinitime.DFUProgress, error) {
|
||||
progressCh := make(chan infinitime.DFUProgress, 5)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"FirmwareUpgrade",
|
||||
FwUpgradeData{
|
||||
Type: upgType,
|
||||
Files: files,
|
||||
},
|
||||
progressCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return progressCh, nil
|
||||
}
|
96
api/fs.go
Normal file
@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import "context"
|
||||
|
||||
func (c *Client) RemoveAll(ctx context.Context, paths ...string) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"RemoveAll",
|
||||
paths,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) Remove(ctx context.Context, paths ...string) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"Remove",
|
||||
paths,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) Rename(ctx context.Context, old, new string) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"Rename",
|
||||
[2]string{old, new},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) MkdirAll(ctx context.Context, paths ...string) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"MkdirAll",
|
||||
paths,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) Mkdir(ctx context.Context, paths ...string) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"Mkdir",
|
||||
paths,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) ReadDir(ctx context.Context, dir string) (out []FileInfo, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"ReadDir",
|
||||
dir,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Upload(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
|
||||
progressCh := make(chan FSTransferProgress, 5)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"Upload",
|
||||
[2]string{dst, src},
|
||||
progressCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return progressCh, nil
|
||||
}
|
||||
|
||||
func (c *Client) Download(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
|
||||
progressCh := make(chan FSTransferProgress, 5)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"Download",
|
||||
[2]string{dst, src},
|
||||
progressCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return progressCh, nil
|
||||
}
|
73
api/get.go
Normal file
@ -0,0 +1,73 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
func (c *Client) HeartRate(ctx context.Context) (out uint8, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"HeartRate",
|
||||
nil,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) BatteryLevel(ctx context.Context) (out uint8, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"BatteryLevel",
|
||||
nil,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Motion(ctx context.Context) (out infinitime.MotionValues, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"Motion",
|
||||
nil,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) StepCount(ctx context.Context) (out uint32, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"StepCount",
|
||||
nil,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Version(ctx context.Context) (out string, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"Version",
|
||||
nil,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Address(ctx context.Context) (out string, err error) {
|
||||
err = c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"Address",
|
||||
nil,
|
||||
&out,
|
||||
)
|
||||
return
|
||||
}
|
209
api/info.go
@ -1,209 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
// Address gets the bluetooth address of the connected device
|
||||
func (c *Client) Address() (string, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeBtAddress,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Value.(string), nil
|
||||
}
|
||||
|
||||
// Version gets the firmware version of the connected device
|
||||
func (c *Client) Version() (string, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeFwVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Value.(string), nil
|
||||
}
|
||||
|
||||
// BatteryLevel gets the battery level of the connected device
|
||||
func (c *Client) BatteryLevel() (uint8, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeBattLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return uint8(res.Value.(float64)), nil
|
||||
}
|
||||
|
||||
// WatchBatteryLevel returns a channel which will contain
|
||||
// new battery level values as they update. Do not use after
|
||||
// calling cancellation function
|
||||
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
|
||||
c.battLevelCh = make(chan types.Response, 2)
|
||||
err := c.requestNoRes(types.Request{
|
||||
Type: types.ReqTypeWatchBattLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
res := <-c.battLevelCh
|
||||
done, cancel := c.cancelFn(res.ID, c.battLevelCh)
|
||||
out := make(chan uint8, 2)
|
||||
go func() {
|
||||
for res := range c.battLevelCh {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
out <- decodeUint8(res.Value)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, cancel, nil
|
||||
}
|
||||
|
||||
// HeartRate gets the heart rate from the connected device
|
||||
func (c *Client) HeartRate() (uint8, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeHeartRate,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return decodeUint8(res.Value), nil
|
||||
}
|
||||
|
||||
// WatchHeartRate returns a channel which will contain
|
||||
// new heart rate values as they update. Do not use after
|
||||
// calling cancellation function
|
||||
func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) {
|
||||
c.heartRateCh = make(chan types.Response, 2)
|
||||
err := c.requestNoRes(types.Request{
|
||||
Type: types.ReqTypeWatchHeartRate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
res := <-c.heartRateCh
|
||||
done, cancel := c.cancelFn(res.ID, c.heartRateCh)
|
||||
out := make(chan uint8, 2)
|
||||
go func() {
|
||||
for res := range c.heartRateCh {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
out <- decodeUint8(res.Value)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, cancel, nil
|
||||
}
|
||||
|
||||
// cancelFn generates a cancellation function for the given
|
||||
// request type and channel
|
||||
func (c *Client) cancelFn(reqID string, ch chan types.Response) (chan struct{}, func()) {
|
||||
done := make(chan struct{}, 1)
|
||||
return done, func() {
|
||||
done <- struct{}{}
|
||||
close(ch)
|
||||
c.requestNoRes(types.Request{
|
||||
Type: types.ReqTypeCancel,
|
||||
Data: reqID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// StepCount gets the step count from the connected device
|
||||
func (c *Client) StepCount() (uint32, error) {
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeStepCount,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return uint32(res.Value.(float64)), nil
|
||||
}
|
||||
|
||||
// WatchStepCount returns a channel which will contain
|
||||
// new step count values as they update. Do not use after
|
||||
// calling cancellation function
|
||||
func (c *Client) WatchStepCount() (<-chan uint32, func(), error) {
|
||||
c.stepCountCh = make(chan types.Response, 2)
|
||||
err := c.requestNoRes(types.Request{
|
||||
Type: types.ReqTypeWatchStepCount,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
res := <-c.stepCountCh
|
||||
done, cancel := c.cancelFn(res.ID, c.stepCountCh)
|
||||
out := make(chan uint32, 2)
|
||||
go func() {
|
||||
for res := range c.stepCountCh {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
out <- decodeUint32(res.Value)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, cancel, nil
|
||||
}
|
||||
|
||||
// Motion gets the motion values from the connected device
|
||||
func (c *Client) Motion() (infinitime.MotionValues, error) {
|
||||
out := infinitime.MotionValues{}
|
||||
res, err := c.request(types.Request{
|
||||
Type: types.ReqTypeMotion,
|
||||
})
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
err = mapstructure.Decode(res.Value, &out)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WatchMotion returns a channel which will contain
|
||||
// new motion values as they update. Do not use after
|
||||
// calling cancellation function
|
||||
func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) {
|
||||
c.motionCh = make(chan types.Response, 5)
|
||||
err := c.requestNoRes(types.Request{
|
||||
Type: types.ReqTypeWatchMotion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
res := <-c.motionCh
|
||||
done, cancel := c.cancelFn(res.ID, c.motionCh)
|
||||
out := make(chan infinitime.MotionValues, 5)
|
||||
go func() {
|
||||
for res := range c.motionCh {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
motion, err := decodeMotion(res.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out <- motion
|
||||
}
|
||||
}
|
||||
}()
|
||||
return out, cancel, nil
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
package api
|
||||
|
||||
import "go.arsenm.dev/itd/internal/types"
|
||||
import "context"
|
||||
|
||||
func (c *Client) Notify(title string, body string) error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeNotify,
|
||||
Data: types.ReqDataNotify{
|
||||
func (c *Client) Notify(ctx context.Context, title, body string) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"Notify",
|
||||
NotifyData{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Body: body,
|
||||
},
|
||||
})
|
||||
return err
|
||||
nil,
|
||||
)
|
||||
}
|
26
api/resources.go
Normal file
@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
// LoadResources loads resources onto the watch from the given
|
||||
// file path to the resources zip
|
||||
func (c *Client) LoadResources(ctx context.Context, path string) (<-chan infinitime.ResourceLoadProgress, error) {
|
||||
progCh := make(chan infinitime.ResourceLoadProgress)
|
||||
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"FS",
|
||||
"LoadResources",
|
||||
path,
|
||||
progCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return progCh, nil
|
||||
}
|
16
api/set.go
Normal file
@ -0,0 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) SetTime(ctx context.Context, t time.Time) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"SetTime",
|
||||
t,
|
||||
nil,
|
||||
)
|
||||
}
|
33
api/time.go
@ -1,33 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
// SetTime sets the given time on the connected device
|
||||
func (c *Client) SetTime(t time.Time) error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeSetTime,
|
||||
Data: t.Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimeNow sets the time on the connected device to
|
||||
// the current time. This is more accurate than
|
||||
// SetTime(time.Now()) due to RFC3339 formatting
|
||||
func (c *Client) SetTimeNow() error {
|
||||
_, err := c.request(types.Request{
|
||||
Type: types.ReqTypeSetTime,
|
||||
Data: "now",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
96
api/types.go
Normal file
@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type UpgradeType uint8
|
||||
|
||||
const (
|
||||
UpgradeTypeArchive UpgradeType = iota
|
||||
UpgradeTypeFiles
|
||||
)
|
||||
|
||||
type FSData struct {
|
||||
Files []string
|
||||
Data string
|
||||
}
|
||||
|
||||
type FwUpgradeData struct {
|
||||
Type UpgradeType
|
||||
Files []string
|
||||
}
|
||||
|
||||
type NotifyData struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
type FSTransferProgress struct {
|
||||
Total uint32
|
||||
Sent uint32
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Name string
|
||||
Size int64
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
func (fi FileInfo) String() string {
|
||||
var isDirChar rune
|
||||
if fi.IsDir {
|
||||
isDirChar = 'd'
|
||||
} else {
|
||||
isDirChar = '-'
|
||||
}
|
||||
|
||||
// Get human-readable value for file size
|
||||
val, unit := bytesHuman(fi.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,
|
||||
fi.Name,
|
||||
)
|
||||
}
|
||||
|
||||
// bytesHuman returns a human-readable string for
|
||||
// the amount of bytes inputted.
|
||||
func bytesHuman(b int64) (float64, string) {
|
||||
const unit = 1000
|
||||
// Set possible units prefixes (PineTime flash is 4MB)
|
||||
units := [2]rune{'k', 'M'}
|
||||
// If amount of bytes is less than smallest unit
|
||||
if b < unit {
|
||||
// Return unchanged with unit "B"
|
||||
return float64(b), "B"
|
||||
}
|
||||
|
||||
div, exp := int64(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
|
||||
}
|
13
api/update.go
Normal file
@ -0,0 +1,13 @@
|
||||
package api
|
||||
|
||||
import "context"
|
||||
|
||||
func (c *Client) WeatherUpdate(ctx context.Context) error {
|
||||
return c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"WeatherUpdate",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
// DFUProgress stores the progress of a DFU upfate
|
||||
type DFUProgress types.DFUProgress
|
||||
|
||||
// UpgradeType indicates the type of upgrade to be performed
|
||||
type UpgradeType uint8
|
||||
|
||||
// Type of DFU upgrade
|
||||
const (
|
||||
UpgradeTypeArchive UpgradeType = iota
|
||||
UpgradeTypeFiles
|
||||
)
|
||||
|
||||
// FirmwareUpgrade initiates a DFU update and returns the progress channel
|
||||
func (c *Client) FirmwareUpgrade(upgType UpgradeType, files ...string) (<-chan DFUProgress, error) {
|
||||
err := json.NewEncoder(c.conn).Encode(types.Request{
|
||||
Type: types.ReqTypeFwUpgrade,
|
||||
Data: types.ReqDataFwUpgrade{
|
||||
Type: int(upgType),
|
||||
Files: files,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.dfuProgressCh = make(chan types.Response, 5)
|
||||
|
||||
out := make(chan DFUProgress, 5)
|
||||
go func() {
|
||||
for res := range c.dfuProgressCh {
|
||||
progress, err := decodeDFUProgress(res.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out <- progress
|
||||
}
|
||||
}()
|
||||
|
||||
return out, nil
|
||||
}
|
71
api/watch.go
Normal file
@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
|
||||
outCh := make(chan uint8, 2)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"WatchHeartRate",
|
||||
nil,
|
||||
outCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return outCh, nil
|
||||
}
|
||||
|
||||
func (c *Client) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
|
||||
outCh := make(chan uint8, 2)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"WatchBatteryLevel",
|
||||
nil,
|
||||
outCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return outCh, nil
|
||||
}
|
||||
|
||||
func (c *Client) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
|
||||
outCh := make(chan uint32, 2)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"WatchStepCount",
|
||||
nil,
|
||||
outCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return outCh, nil
|
||||
}
|
||||
|
||||
func (c *Client) WatchMotion(ctx context.Context) (<-chan infinitime.MotionValues, error) {
|
||||
outCh := make(chan infinitime.MotionValues, 2)
|
||||
err := c.client.Call(
|
||||
ctx,
|
||||
"ITD",
|
||||
"WatchMotion",
|
||||
nil,
|
||||
outCh,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return outCh, nil
|
||||
}
|
227
calls.go
@ -1,85 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
func initCallNotifs(dev *infinitime.Device) error {
|
||||
// Define rule to filter dbus messages
|
||||
rule := "type='signal',sender='org.freedesktop.ModemManager1',interface='org.freedesktop.ModemManager1.Modem.Voice',member='CallAdded'"
|
||||
|
||||
// Use dbus-monitor command with profiling output as a workaround
|
||||
// because go-bluetooth seems to monopolize the system bus connection
|
||||
// which makes monitoring show only bluez-related messages.
|
||||
cmd := exec.Command("dbus-monitor", "--system", "--profile", rule)
|
||||
// Get command output pipe
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Run command asynchronously
|
||||
err = cmd.Start()
|
||||
func initCallNotifs(ctx context.Context, dev *infinitime.Device) error {
|
||||
// Connect to system bus. This connection is for method calls.
|
||||
conn, err := newSystemBusConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new scanner for command output
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
// Check if modem manager interface exists
|
||||
exists, err := modemManagerExists(ctx, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If it does not exist, stop function
|
||||
if !exists {
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to system bus. This connection is for monitoring.
|
||||
monitorConn, err := newSystemBusConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add match for new calls to monitor connection
|
||||
err = monitorConn.AddMatchSignal(
|
||||
dbus.WithMatchSender("org.freedesktop.ModemManager1"),
|
||||
dbus.WithMatchInterface("org.freedesktop.ModemManager1.Modem.Voice"),
|
||||
dbus.WithMatchMember("CallAdded"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create channel to receive calls
|
||||
callCh := make(chan *dbus.Message, 5)
|
||||
// Notify channel upon received message
|
||||
monitorConn.Eavesdrop(callCh)
|
||||
|
||||
var respHandlerOnce sync.Once
|
||||
var callObj dbus.BusObject
|
||||
|
||||
go func() {
|
||||
// For each line in output
|
||||
for scanner.Scan() {
|
||||
// Get line as string
|
||||
text := scanner.Text()
|
||||
// For every message received
|
||||
for event := range callCh {
|
||||
// Get path to call object
|
||||
callPath := event.Body[0].(dbus.ObjectPath)
|
||||
// Get call object
|
||||
callObj = conn.Object("org.freedesktop.ModemManager1", callPath)
|
||||
|
||||
// If line starts with "#", it is part of
|
||||
// the field format, skip it.
|
||||
if strings.HasPrefix(text, "#") {
|
||||
// Get phone number from call object using method call connection
|
||||
phoneNum, err := getPhoneNum(conn, callObj)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error getting phone number")
|
||||
continue
|
||||
}
|
||||
|
||||
// Split line into fields. The order is as follows:
|
||||
// type timestamp serial sender destination path interface member
|
||||
fields := strings.Fields(text)
|
||||
// Field 7 is Member. Make sure it is "CallAdded".
|
||||
if fields[7] == "CallAdded" {
|
||||
// Get Modem ID from modem path
|
||||
modemID := parseModemID(fields[5])
|
||||
// Get call ID of current call
|
||||
callID, err := getCurrentCallID(modemID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Get phone number of current call
|
||||
phoneNum, err := getPhoneNum(callID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Send call notification to PineTime
|
||||
resCh, err := dev.NotifyCall(phoneNum)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
// Wait for PineTime response
|
||||
res := <-resCh
|
||||
// Send call notification to InfiniTime
|
||||
resCh, err := dev.NotifyCall(phoneNum)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go respHandlerOnce.Do(func() {
|
||||
// Wait for PineTime response
|
||||
for res := range resCh {
|
||||
switch res {
|
||||
case infinitime.CallStatusAccepted:
|
||||
// Attempt to accept call
|
||||
err = acceptCall(callID)
|
||||
err = acceptCall(ctx, conn, callObj)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error accepting call")
|
||||
}
|
||||
case infinitime.CallStatusDeclined:
|
||||
// Attempt to decline call
|
||||
err = declineCall(callID)
|
||||
err = declineCall(ctx, conn, callObj)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error declining call")
|
||||
}
|
||||
@ -87,90 +93,57 @@ func initCallNotifs(dev *infinitime.Device) error {
|
||||
// Warn about unimplemented muting
|
||||
log.Warn().Msg("Muting calls is not implemented")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info().Msg("Relaying calls to InfiniTime")
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseModemID(modemPath string) int {
|
||||
// Split path by "/"
|
||||
splitPath := strings.Split(modemPath, "/")
|
||||
// Get last element and convert to integer
|
||||
id, _ := strconv.Atoi(splitPath[len(splitPath)-1])
|
||||
return id
|
||||
func modemManagerExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
|
||||
var names []string
|
||||
err := conn.BusObject().CallWithContext(
|
||||
ctx, "org.freedesktop.DBus.ListNames", 0,
|
||||
).Store(&names)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strSlcContains(names, "org.freedesktop.ModemManager1"), nil
|
||||
}
|
||||
|
||||
func getCurrentCallID(modemID int) (int, error) {
|
||||
// Create mmcli command
|
||||
cmd := exec.Command("mmcli", "--voice-list-calls", "-m", fmt.Sprint(modemID), "-J")
|
||||
// Run command and get output
|
||||
data, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var calls map[string][]string
|
||||
// Decode JSON from command output
|
||||
err = json.Unmarshal(data, &calls)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Get first call in output
|
||||
firstCall := calls["modem.voice.call"][0]
|
||||
// Split path by "/"
|
||||
splitCall := strings.Split(firstCall, "/")
|
||||
// Return last element converted to integer
|
||||
return strconv.Atoi(splitCall[len(splitCall)-1])
|
||||
}
|
||||
|
||||
func getPhoneNum(callID int) (string, error) {
|
||||
// Create dbus-send command
|
||||
cmd := exec.Command("dbus-send",
|
||||
"--dest=org.freedesktop.ModemManager1",
|
||||
"--system",
|
||||
"--print-reply=literal",
|
||||
"--type=method_call",
|
||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
||||
"org.freedesktop.DBus.Properties.Get",
|
||||
"string:org.freedesktop.ModemManager1.Call",
|
||||
"string:Number",
|
||||
)
|
||||
// Run command and get output
|
||||
numData, err := cmd.Output()
|
||||
// getPhoneNum gets a phone number from a call object using a DBus connection
|
||||
func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
|
||||
var out string
|
||||
// Get number property on DBus object and store return value in out
|
||||
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Number", &out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Split output into fields
|
||||
num := strings.Fields(string(numData))
|
||||
// Return last field
|
||||
return num[len(num)-1], nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func acceptCall(callID int) error {
|
||||
// Create dbus-send command
|
||||
cmd := exec.Command("dbus-send",
|
||||
"--dest=org.freedesktop.ModemManager1",
|
||||
"--print-reply",
|
||||
"--system",
|
||||
"--type=method_call",
|
||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
||||
"org.freedesktop.ModemManager1.Call.Accept",
|
||||
// getPhoneNum accepts a call using a DBus connection
|
||||
func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||
// Call Accept() method on DBus object
|
||||
call := callObj.CallWithContext(
|
||||
ctx, "org.freedesktop.ModemManager1.Call.Accept", 0,
|
||||
)
|
||||
// Run command and return errpr
|
||||
return cmd.Run()
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func declineCall(callID int) error {
|
||||
// Create dbus-send command
|
||||
cmd := exec.Command("dbus-send",
|
||||
"--dest=org.freedesktop.ModemManager1",
|
||||
"--print-reply",
|
||||
"--system",
|
||||
"--type=method_call",
|
||||
fmt.Sprintf("/org/freedesktop/ModemManager1/Call/%d", callID),
|
||||
"org.freedesktop.ModemManager1.Call.Hangup",
|
||||
// getPhoneNum declines a call using a DBus connection
|
||||
func declineCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||
// Call Hangup() method on DBus object
|
||||
call := callObj.CallWithContext(
|
||||
ctx, "org.freedesktop.ModemManager1.Call.Hangup", 0,
|
||||
)
|
||||
// Run command and return errpr
|
||||
return cmd.Run()
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
94
cmd/itctl/firmware.go
Normal file
@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func fwUpgrade(c *cli.Context) error {
|
||||
resources := c.String("resources")
|
||||
if resources != "" {
|
||||
absRes, err := filepath.Abs(resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = resLoad(c.Context, []string{absRes})
|
||||
if err != nil {
|
||||
log.Error().Msg("Resource loading has returned an error. This can happen if your current version of InfiniTime doesn't support BLE FS. Try updating without resource loading, and then load them after using the `itctl res load` command.")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
var upgType api.UpgradeType
|
||||
var files []string
|
||||
// Get relevant data struct
|
||||
if c.String("archive") != "" {
|
||||
// Get archive data struct
|
||||
upgType = api.UpgradeTypeArchive
|
||||
files = []string{c.String("archive")}
|
||||
} else if c.String("init-packet") != "" && c.String("firmware") != "" {
|
||||
// Get files data struct
|
||||
upgType = api.UpgradeTypeFiles
|
||||
files = []string{c.String("init-packet"), c.String("firmware")}
|
||||
} else {
|
||||
return cli.Exit("Upgrade command requires either archive or init packet and firmware.", 1)
|
||||
}
|
||||
|
||||
progress, err := client.FirmwareUpgrade(c.Context, upgType, abs(files)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create progress bar template
|
||||
barTmpl := `{{counters . }} B {{bar . "|" "-" (cycle .) " " "|"}} {{percent . }} {{rtime . "%s"}}`
|
||||
// Start full bar at 0 total
|
||||
bar := pb.ProgressBarTemplate(barTmpl).Start(0)
|
||||
// Create new scanner of connection
|
||||
for event := range progress {
|
||||
// Set total bytes in progress bar
|
||||
bar.SetTotal(event.Total)
|
||||
// Set amount of bytes received in progress bar
|
||||
bar.SetCurrent(int64(event.Received))
|
||||
// If transfer finished, break
|
||||
if int64(event.Sent) == event.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Finish progress bar
|
||||
bar.Finish()
|
||||
|
||||
fmt.Printf("Transferred %d B in %s.\n", bar.Total(), time.Since(start))
|
||||
fmt.Println("Remember to validate the new firmware in the InfiniTime settings.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fwVersion(c *cli.Context) error {
|
||||
version, err := client.Version(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func abs(paths []string) []string {
|
||||
for index, path := range paths {
|
||||
newPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
paths[index] = newPath
|
||||
}
|
||||
return paths
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package firmware
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
)
|
||||
|
||||
// firmwareCmd represents the firmware command
|
||||
var firmwareCmd = &cobra.Command{
|
||||
Use: "firmware",
|
||||
Short: "Manage InfiniTime firmware",
|
||||
Aliases: []string{"fw"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
root.RootCmd.AddCommand(firmwareCmd)
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package firmware
|
||||
|
||||
import (
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
type DFUProgress struct {
|
||||
Received int64 `mapstructure:"recvd"`
|
||||
Total int64 `mapstructure:"total"`
|
||||
}
|
||||
|
||||
// upgradeCmd represents the upgrade command
|
||||
var upgradeCmd = &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "Upgrade InfiniTime firmware using files or archive",
|
||||
Aliases: []string{"upg"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
var upgType api.UpgradeType
|
||||
var files []string
|
||||
// Get relevant data struct
|
||||
if viper.GetString("archive") != "" {
|
||||
// Get archive data struct
|
||||
upgType = types.UpgradeTypeArchive
|
||||
files = []string{viper.GetString("archive")}
|
||||
} else if viper.GetString("initPkt") != "" && viper.GetString("firmware") != "" {
|
||||
// Get files data struct
|
||||
upgType = types.UpgradeTypeFiles
|
||||
files = []string{viper.GetString("initPkt"), viper.GetString("firmware")}
|
||||
} else {
|
||||
cmd.Usage()
|
||||
log.Warn().Msg("Upgrade command requires either archive or init packet and firmware.")
|
||||
return
|
||||
}
|
||||
|
||||
progress, err := client.FirmwareUpgrade(upgType, files...)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error initiating DFU")
|
||||
}
|
||||
|
||||
// Create progress bar template
|
||||
barTmpl := `{{counters . }} B {{bar . "|" "-" (cycle .) " " "|"}} {{percent . }} {{rtime . "%s"}}`
|
||||
// Start full bar at 0 total
|
||||
bar := pb.ProgressBarTemplate(barTmpl).Start(0)
|
||||
// Create new scanner of connection
|
||||
for event := range progress {
|
||||
// Set total bytes in progress bar
|
||||
bar.SetTotal(event.Total)
|
||||
// Set amount of bytes received in progress bar
|
||||
bar.SetCurrent(event.Received)
|
||||
// If transfer finished, break
|
||||
if event.Sent == event.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Finish progress bar
|
||||
bar.Finish()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
firmwareCmd.AddCommand(upgradeCmd)
|
||||
|
||||
// Register flags
|
||||
upgradeCmd.Flags().StringP("archive", "a", "", "Path to firmware archive")
|
||||
upgradeCmd.Flags().StringP("init-pkt", "i", "", "Path to init packet (.dat file)")
|
||||
upgradeCmd.Flags().StringP("firmware", "f", "", "Path to firmware image (.bin file)")
|
||||
|
||||
// Bind flags to viper keys
|
||||
viper.BindPFlag("archive", upgradeCmd.Flags().Lookup("archive"))
|
||||
viper.BindPFlag("initPkt", upgradeCmd.Flags().Lookup("init-pkt"))
|
||||
viper.BindPFlag("firmware", upgradeCmd.Flags().Lookup("firmware"))
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package firmware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"ver"},
|
||||
Short: "Get firmware version of InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
version, err := client.Version()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting firmware version")
|
||||
}
|
||||
|
||||
fmt.Println(version)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
firmwareCmd.AddCommand(versionCmd)
|
||||
}
|
175
cmd/itctl/fs.go
Normal file
@ -0,0 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func fsList(c *cli.Context) error {
|
||||
dirPath := "/"
|
||||
if c.Args().Len() > 0 {
|
||||
dirPath = c.Args().Get(0)
|
||||
}
|
||||
|
||||
listing, err := client.ReadDir(c.Context, dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range listing {
|
||||
fmt.Println(entry)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fsMkdir(c *cli.Context) error {
|
||||
if c.Args().Len() < 1 {
|
||||
return cli.Exit("Command mkdir requires one or more arguments", 1)
|
||||
}
|
||||
|
||||
var err error
|
||||
if c.Bool("parents") {
|
||||
err = client.MkdirAll(c.Context, c.Args().Slice()...)
|
||||
} else {
|
||||
err = client.Mkdir(c.Context, c.Args().Slice()...)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fsMove(c *cli.Context) error {
|
||||
if c.Args().Len() != 2 {
|
||||
return cli.Exit("Command move requires two arguments", 1)
|
||||
}
|
||||
|
||||
err := client.Rename(c.Context, c.Args().Get(0), c.Args().Get(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fsRead(c *cli.Context) error {
|
||||
if c.Args().Len() != 2 {
|
||||
return cli.Exit("Command read requires two arguments", 1)
|
||||
}
|
||||
|
||||
var tmpFile *os.File
|
||||
var path string
|
||||
var err error
|
||||
if c.Args().Get(1) == "-" {
|
||||
tmpFile, err = ioutil.TempFile("/tmp", "itctl.*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path = tmpFile.Name()
|
||||
} else {
|
||||
path, err = filepath.Abs(c.Args().Get(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
progress, err := client.Download(c.Context, path, c.Args().Get(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create progress bar template
|
||||
barTmpl := `{{counters . }} B {{bar . "|" "-" (cycle .) " " "|"}} {{percent . }} {{rtime . "%s"}}`
|
||||
// Start full bar at 0 total
|
||||
bar := pb.ProgressBarTemplate(barTmpl).Start(0)
|
||||
// Get progress events
|
||||
for event := range progress {
|
||||
// Set total bytes in progress bar
|
||||
bar.SetTotal(int64(event.Total))
|
||||
// Set amount of bytes sent in progress bar
|
||||
bar.SetCurrent(int64(event.Sent))
|
||||
}
|
||||
bar.Finish()
|
||||
|
||||
if c.Args().Get(1) == "-" {
|
||||
io.Copy(os.Stdout, tmpFile)
|
||||
os.Stdout.WriteString("\n")
|
||||
os.Stdout.Sync()
|
||||
tmpFile.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fsRemove(c *cli.Context) error {
|
||||
if c.Args().Len() < 1 {
|
||||
return cli.Exit("Command remove requires one or more arguments", 1)
|
||||
}
|
||||
|
||||
var err error
|
||||
if c.Bool("recursive") {
|
||||
err = client.RemoveAll(c.Context, c.Args().Slice()...)
|
||||
} else {
|
||||
err = client.Remove(c.Context, c.Args().Slice()...)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fsWrite(c *cli.Context) error {
|
||||
if c.Args().Len() != 2 {
|
||||
return cli.Exit("Command write requires two arguments", 1)
|
||||
}
|
||||
|
||||
var tmpFile *os.File
|
||||
var path string
|
||||
var err error
|
||||
if c.Args().Get(0) == "-" {
|
||||
tmpFile, err = ioutil.TempFile("/tmp", "itctl.*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path = tmpFile.Name()
|
||||
} else {
|
||||
path, err = filepath.Abs(c.Args().Get(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Args().Get(0) == "-" {
|
||||
io.Copy(tmpFile, os.Stdin)
|
||||
defer tmpFile.Close()
|
||||
defer os.Remove(path)
|
||||
}
|
||||
|
||||
progress, err := client.Upload(c.Context, c.Args().Get(1), path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create progress bar template
|
||||
barTmpl := `{{counters . }} B {{bar . "|" "-" (cycle .) " " "|"}} {{percent . }} {{rtime . "%s"}}`
|
||||
// Start full bar at 0 total
|
||||
bar := pb.ProgressBarTemplate(barTmpl).Start(0)
|
||||
// Get progress events
|
||||
for event := range progress {
|
||||
// Set total bytes in progress bar
|
||||
bar.SetTotal(int64(event.Total))
|
||||
// Set amount of bytes sent in progress bar
|
||||
bar.SetCurrent(int64(event.Sent))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
71
cmd/itctl/get.go
Normal file
@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func getAddress(c *cli.Context) error {
|
||||
address, err := client.Address(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(address)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBattery(c *cli.Context) error {
|
||||
battLevel, err := client.BatteryLevel(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print returned percentage
|
||||
fmt.Printf("%d%%\n", battLevel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHeart(c *cli.Context) error {
|
||||
heartRate, err := client.HeartRate(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print returned BPM
|
||||
fmt.Printf("%d BPM\n", heartRate)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getMotion(c *cli.Context) error {
|
||||
motionVals, err := client.Motion(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool("shell") {
|
||||
fmt.Printf(
|
||||
"X=%d\nY=%d\nZ=%d\n",
|
||||
motionVals.X,
|
||||
motionVals.Y,
|
||||
motionVals.Z,
|
||||
)
|
||||
} else {
|
||||
return json.NewEncoder(os.Stdout).Encode(motionVals)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSteps(c *cli.Context) error {
|
||||
stepCount, err := client.StepCount(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Print returned BPM
|
||||
fmt.Printf("%d Steps\n", stepCount)
|
||||
return nil
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// addressCmd represents the address command
|
||||
var addressCmd = &cobra.Command{
|
||||
Use: "address",
|
||||
Aliases: []string{"addr"},
|
||||
Short: "Get InfiniTime's bluetooth address",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
address, err := client.Address()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting bluetooth address")
|
||||
}
|
||||
|
||||
fmt.Println(address)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getCmd.AddCommand(addressCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// batteryCmd represents the batt command
|
||||
var batteryCmd = &cobra.Command{
|
||||
Use: "battery",
|
||||
Aliases: []string{"batt"},
|
||||
Short: "Get battery level from InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
battLevel, err := client.BatteryLevel()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting battery level")
|
||||
}
|
||||
|
||||
// Print returned percentage
|
||||
fmt.Printf("%d%%\n", battLevel)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getCmd.AddCommand(batteryCmd)
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
)
|
||||
|
||||
// getCmd represents the get command
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get information from InfiniTime",
|
||||
}
|
||||
|
||||
func init() {
|
||||
root.RootCmd.AddCommand(getCmd)
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// heartCmd represents the heart command
|
||||
var heartCmd = &cobra.Command{
|
||||
Use: "heart",
|
||||
Short: "Get heart rate from InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
heartRate, err := client.HeartRate()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting heart rate")
|
||||
}
|
||||
|
||||
// Print returned BPM
|
||||
fmt.Printf("%d BPM\n", heartRate)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getCmd.AddCommand(heartCmd)
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// steps.goCmd represents the steps.go command
|
||||
var motionCmd = &cobra.Command{
|
||||
Use: "motion",
|
||||
Short: "Get motion values from InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
motionVals, err := client.Motion()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting motion values")
|
||||
}
|
||||
|
||||
if viper.GetBool("shell") {
|
||||
fmt.Printf(
|
||||
"X=%d\nY=%d\nZ=%d",
|
||||
motionVals.X,
|
||||
motionVals.Y,
|
||||
motionVals.Z,
|
||||
)
|
||||
} else {
|
||||
fmt.Printf("%+v\n", motionVals)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getCmd.AddCommand(motionCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// steps.goCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
motionCmd.Flags().BoolP("shell", "s", false, "Output data in shell-compatible format")
|
||||
viper.BindPFlag("shell", motionCmd.Flags().Lookup("shell"))
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package get
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// steps.goCmd represents the steps.go command
|
||||
var stepsCmd = &cobra.Command{
|
||||
Use: "steps",
|
||||
Short: "Get step count from InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
stepCount, err := client.StepCount()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting step count")
|
||||
}
|
||||
|
||||
// Print returned BPM
|
||||
fmt.Printf("%d Steps\n", stepCount)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
getCmd.AddCommand(stepsCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// steps.goCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// steps.goCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
@ -1,41 +1,319 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/firmware"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/get"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/notify"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/set"
|
||||
_ "go.arsenm.dev/itd/cmd/itctl/watch"
|
||||
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
var client *api.Client
|
||||
|
||||
func main() {
|
||||
root.Execute()
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, _ = signal.NotifyContext(
|
||||
ctx,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
// This goroutine ensures that itctl will exit
|
||||
// at most 200ms after the user sends SIGINT/SIGTERM.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
app := cli.App{
|
||||
Name: "itctl",
|
||||
HideHelpCommand: true,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "socket-path",
|
||||
Aliases: []string{"s"},
|
||||
Value: api.DefaultAddr,
|
||||
Usage: "Path to itd socket",
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "help",
|
||||
ArgsUsage: "<command>",
|
||||
Usage: "Display help screen for a command",
|
||||
Action: helpCmd,
|
||||
},
|
||||
{
|
||||
Name: "resources",
|
||||
Aliases: []string{"res"},
|
||||
Usage: "Handle InfiniTime resource loading",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "load",
|
||||
ArgsUsage: "<path>",
|
||||
Usage: "Load an InifiniTime resources package",
|
||||
Action: resourcesLoad,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "filesystem",
|
||||
Aliases: []string{"fs"},
|
||||
Usage: "Perform filesystem operations on the PineTime",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
ArgsUsage: "[dir]",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "List a directory",
|
||||
Action: fsList,
|
||||
},
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "parents",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Make parent directories if needed, no error if already existing",
|
||||
},
|
||||
},
|
||||
Name: "mkdir",
|
||||
ArgsUsage: "<paths...>",
|
||||
Usage: "Create new directories",
|
||||
Action: fsMkdir,
|
||||
},
|
||||
{
|
||||
Name: "move",
|
||||
ArgsUsage: "<old> <new>",
|
||||
Aliases: []string{"mv"},
|
||||
Usage: "Move a file or directory",
|
||||
Action: fsMove,
|
||||
},
|
||||
{
|
||||
Name: "read",
|
||||
ArgsUsage: `<remote path> <local path>`,
|
||||
Usage: "Read a file from InfiniTime.",
|
||||
Description: `Read is used to read files from InfiniTime's filesystem. A "-" can be used to signify stdout`,
|
||||
Action: fsRead,
|
||||
},
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "recursive",
|
||||
Aliases: []string{"r", "R"},
|
||||
Usage: "Remove directories and their contents recursively",
|
||||
},
|
||||
},
|
||||
Name: "remove",
|
||||
ArgsUsage: "<paths...>",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "Remove a file from InfiniTime",
|
||||
Action: fsRemove,
|
||||
},
|
||||
{
|
||||
Name: "write",
|
||||
ArgsUsage: `<local path> <remote path>`,
|
||||
Usage: "Write a file to InfiniTime",
|
||||
Description: `Write is used to write files to InfiniTime's filesystem. A "-" can be used to signify stdin`,
|
||||
Action: fsWrite,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "firmware",
|
||||
Aliases: []string{"fw"},
|
||||
Usage: "Manage InfiniTime firmware",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.PathFlag{
|
||||
Name: "init-packet",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "Path to init packet (.dat file)",
|
||||
},
|
||||
&cli.PathFlag{
|
||||
Name: "firmware",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Path to firmware image (.bin file)",
|
||||
},
|
||||
&cli.PathFlag{
|
||||
Name: "resources",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Path to resources file (.zip file)",
|
||||
},
|
||||
&cli.PathFlag{
|
||||
Name: "archive",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Path to firmware archive (.zip file)",
|
||||
},
|
||||
},
|
||||
Name: "upgrade",
|
||||
Aliases: []string{"upg"},
|
||||
Usage: "Upgrade InfiniTime firmware using files or archive",
|
||||
Action: fwUpgrade,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Aliases: []string{"ver"},
|
||||
Usage: "Get firmware version of InfiniTime",
|
||||
Action: fwVersion,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get",
|
||||
Usage: "Get information from InfiniTime",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "address",
|
||||
Aliases: []string{"addr"},
|
||||
Usage: "Get InfiniTime's bluetooth address",
|
||||
Action: getAddress,
|
||||
},
|
||||
{
|
||||
Name: "battery",
|
||||
Aliases: []string{"batt"},
|
||||
Usage: "Get InfiniTime's battery percentage",
|
||||
Action: getBattery,
|
||||
},
|
||||
{
|
||||
Name: "heart",
|
||||
Usage: "Get heart rate from InfiniTime",
|
||||
Action: getHeart,
|
||||
},
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "shell"},
|
||||
},
|
||||
Name: "motion",
|
||||
Usage: "Get motion values from InfiniTime",
|
||||
Action: getMotion,
|
||||
},
|
||||
{
|
||||
Name: "steps",
|
||||
Usage: "Get step count from InfiniTime",
|
||||
Action: getSteps,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "notify",
|
||||
Usage: "Send notification to InfiniTime",
|
||||
Action: notify,
|
||||
},
|
||||
{
|
||||
Name: "set",
|
||||
Usage: "Set information on InfiniTime",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "time",
|
||||
ArgsUsage: `<ISO8601|"now">`,
|
||||
Usage: "Set InfiniTime's clock to specified time",
|
||||
Action: setTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "Update information on InfiniTime",
|
||||
Aliases: []string{"upd"},
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "weather",
|
||||
Usage: "Force an immediate update of weather data",
|
||||
Action: updateWeather,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "watch",
|
||||
Usage: "Watch a value for changes",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json"},
|
||||
&cli.BoolFlag{Name: "shell"},
|
||||
},
|
||||
Name: "heart",
|
||||
Usage: "Watch heart rate value for changes",
|
||||
Action: watchHeart,
|
||||
},
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json"},
|
||||
&cli.BoolFlag{Name: "shell"},
|
||||
},
|
||||
Name: "steps",
|
||||
Usage: "Watch step count value for changes",
|
||||
Action: watchStepCount,
|
||||
},
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json"},
|
||||
&cli.BoolFlag{Name: "shell"},
|
||||
},
|
||||
Name: "motion",
|
||||
Usage: "Watch motion coordinates for changes",
|
||||
Action: watchMotion,
|
||||
},
|
||||
{
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json"},
|
||||
&cli.BoolFlag{Name: "shell"},
|
||||
},
|
||||
Name: "battery",
|
||||
Aliases: []string{"batt"},
|
||||
Usage: "Watch battery level value for changes",
|
||||
Action: watchBattLevel,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
if !isHelpCmd() {
|
||||
newClient, err := api.New(c.String("socket-path"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client = newClient
|
||||
}
|
||||
return nil
|
||||
},
|
||||
After: func(*cli.Context) error {
|
||||
if client != nil {
|
||||
client.Close()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := app.RunContext(ctx, os.Args)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error while running app")
|
||||
}
|
||||
}
|
||||
|
||||
func helpCmd(c *cli.Context) error {
|
||||
cmdArgs := append([]string{os.Args[0]}, c.Args().Slice()...)
|
||||
cmdArgs = append(cmdArgs, "-h")
|
||||
return c.App.RunContext(c.Context, cmdArgs)
|
||||
}
|
||||
|
||||
func isHelpCmd() bool {
|
||||
if len(os.Args) == 1 {
|
||||
return true
|
||||
}
|
||||
for _, arg := range os.Args {
|
||||
if arg == "-h" || arg == "help" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
17
cmd/itctl/notify.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
|
||||
func notify(c *cli.Context) error {
|
||||
// Ensure required arguments
|
||||
if c.Args().Len() != 2 {
|
||||
return cli.Exit("Command notify requires two arguments", 1)
|
||||
}
|
||||
|
||||
err := client.Notify(c.Context, c.Args().Get(0), c.Args().Get(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
)
|
||||
|
||||
// notifyCmd represents the notify command
|
||||
var notifyCmd = &cobra.Command{
|
||||
Use: "notify <title> <body>",
|
||||
Short: "Send notification to InfiniTime",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Ensure required arguments
|
||||
if len(args) != 2 {
|
||||
cmd.Usage()
|
||||
log.Fatal().Msg("Command notify requires two arguments")
|
||||
}
|
||||
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
err := client.Notify(args[0], args[1])
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error sending notification")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
root.RootCmd.AddCommand(notifyCmd)
|
||||
}
|
57
cmd/itctl/resources.go
Normal file
@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cheggaaa/pb/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
func resourcesLoad(c *cli.Context) error {
|
||||
return resLoad(c.Context, c.Args().Slice())
|
||||
}
|
||||
|
||||
func resLoad(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return cli.Exit("Command load requires one argument.", 1)
|
||||
}
|
||||
|
||||
// Create progress bar templates
|
||||
rmTmpl := `Removing {{string . "filename"}}`
|
||||
upTmpl := `Uploading {{string . "filename"}} {{counters . }} B {{bar . "|" "-" (cycle .) " " "|"}} {{percent . }} {{rtime . "%s"}}`
|
||||
// Start full bar at 0 total
|
||||
bar := pb.ProgressBarTemplate(rmTmpl).Start(0)
|
||||
|
||||
path, err := filepath.Abs(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progCh, err := client.LoadResources(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for evt := range progCh {
|
||||
if evt.Err != nil {
|
||||
return evt.Err
|
||||
}
|
||||
|
||||
if evt.Operation == infinitime.ResourceOperationRemoveObsolete {
|
||||
bar.SetTemplateString(rmTmpl)
|
||||
bar.Set("filename", evt.Name)
|
||||
} else {
|
||||
bar.SetTemplateString(upTmpl)
|
||||
bar.Set("filename", evt.Name)
|
||||
|
||||
bar.SetTotal(evt.Total)
|
||||
bar.SetCurrent(evt.Sent)
|
||||
}
|
||||
}
|
||||
|
||||
bar.Finish()
|
||||
|
||||
return nil
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package root
|
||||
|
||||
import (
|
||||
"github.com/abiosoft/ishell"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "itctl",
|
||||
Short: "Control the itd daemon for InfiniTime smartwatches",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Create new shell
|
||||
sh := ishell.New()
|
||||
sh.SetPrompt("itctl> ")
|
||||
|
||||
// For every command in cobra
|
||||
for _, subCmd := range cmd.Commands() {
|
||||
// Add top level command to ishell
|
||||
sh.AddCmd(&ishell.Cmd{
|
||||
Name: subCmd.Name(),
|
||||
Help: subCmd.Short,
|
||||
Aliases: subCmd.Aliases,
|
||||
LongHelp: subCmd.Long,
|
||||
Func: func(ctx *ishell.Context) {
|
||||
// Append name and arguments of command
|
||||
args := append([]string{ctx.Cmd.Name}, ctx.Args...)
|
||||
// Set root command arguments
|
||||
cmd.SetArgs(args)
|
||||
// Execute root command with new arguments
|
||||
cmd.Execute()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Start shell
|
||||
sh.Run()
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
client, err := api.New(viper.GetString("sockPath"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error connecting to socket. Is itd running?")
|
||||
}
|
||||
defer client.Close()
|
||||
viper.Set("client", client)
|
||||
RootCmd.CompletionOptions.DisableDefaultCmd = true
|
||||
cobra.CheckErr(RootCmd.Execute())
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register flag for socket path
|
||||
RootCmd.Flags().StringP("socket-path", "s", api.DefaultAddr, "Path to itd socket")
|
||||
|
||||
// Bind flag and environment variable to viper key
|
||||
viper.BindPFlag("sockPath", RootCmd.Flags().Lookup("socket-path"))
|
||||
viper.BindEnv("sockPath", "ITCTL_SOCKET_PATH")
|
||||
|
||||
// Set default value for socket path
|
||||
viper.SetDefault("sockPath", api.DefaultAddr)
|
||||
}
|
24
cmd/itctl/set.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func setTime(c *cli.Context) error {
|
||||
// Ensure required arguments
|
||||
if c.Args().Len() < 1 {
|
||||
return cli.Exit("Command time requires one argument", 1)
|
||||
}
|
||||
|
||||
if c.Args().Get(0) == "now" {
|
||||
return client.SetTime(c.Context, time.Now())
|
||||
} else {
|
||||
parsed, err := time.Parse(time.RFC3339, c.Args().Get(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.SetTime(c.Context, parsed)
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package set
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
)
|
||||
|
||||
// setCmd represents the set command
|
||||
var setCmd = &cobra.Command{
|
||||
Use: "set",
|
||||
Short: "Set information on InfiniTime",
|
||||
}
|
||||
|
||||
func init() {
|
||||
root.RootCmd.AddCommand(setCmd)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package set
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
// timeCmd represents the time command
|
||||
var timeCmd = &cobra.Command{
|
||||
Use: `time <ISO8601|"now">`,
|
||||
Short: "Set InfiniTime's clock to specified time",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Ensure required arguments
|
||||
if len(args) != 1 {
|
||||
cmd.Usage()
|
||||
log.Warn().Msg("Command time requires one argument")
|
||||
return
|
||||
}
|
||||
|
||||
// Connect to itd UNIX socket
|
||||
conn, err := net.Dial("unix", viper.GetString("sockPath"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error dialing socket. Is itd running?")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Encode request into connection
|
||||
err = json.NewEncoder(conn).Encode(types.Request{
|
||||
Type: types.ReqTypeSetTime,
|
||||
Data: args[0],
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error making request")
|
||||
}
|
||||
|
||||
// Read one line from connetion
|
||||
line, _, err := bufio.NewReader(conn).ReadLine()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error reading line from connection")
|
||||
}
|
||||
|
||||
var res types.Response
|
||||
// Decode line into response
|
||||
err = json.Unmarshal(line, &res)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error decoding JSON data")
|
||||
}
|
||||
|
||||
if res.Error {
|
||||
log.Fatal().Msg(res.Message)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
setCmd.AddCommand(timeCmd)
|
||||
}
|
7
cmd/itctl/update.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
|
||||
func updateWeather(c *cli.Context) error {
|
||||
return client.WeatherUpdate(c.Context)
|
||||
}
|
108
cmd/itctl/watch.go
Normal file
@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func watchHeart(c *cli.Context) error {
|
||||
heartCh, err := client.WatchHeartRate(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case heartRate := <-heartCh:
|
||||
if c.Bool("json") {
|
||||
json.NewEncoder(os.Stdout).Encode(
|
||||
map[string]uint8{"heartRate": heartRate},
|
||||
)
|
||||
} else if c.Bool("shell") {
|
||||
fmt.Printf("HEART_RATE=%d\n", heartRate)
|
||||
} else {
|
||||
fmt.Println(heartRate, "BPM")
|
||||
}
|
||||
case <-c.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchBattLevel(c *cli.Context) error {
|
||||
battLevelCh, err := client.WatchBatteryLevel(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case battLevel := <-battLevelCh:
|
||||
if c.Bool("json") {
|
||||
json.NewEncoder(os.Stdout).Encode(
|
||||
map[string]uint8{"battLevel": battLevel},
|
||||
)
|
||||
} else if c.Bool("shell") {
|
||||
fmt.Printf("BATTERY_LEVEL=%d\n", battLevel)
|
||||
} else {
|
||||
fmt.Printf("%d%%\n", battLevel)
|
||||
}
|
||||
case <-c.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchStepCount(c *cli.Context) error {
|
||||
stepCountCh, err := client.WatchStepCount(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case stepCount := <-stepCountCh:
|
||||
if c.Bool("json") {
|
||||
json.NewEncoder(os.Stdout).Encode(
|
||||
map[string]uint32{"stepCount": stepCount},
|
||||
)
|
||||
} else if c.Bool("shell") {
|
||||
fmt.Printf("STEP_COUNT=%d\n", stepCount)
|
||||
} else {
|
||||
fmt.Println(stepCount, "Steps")
|
||||
}
|
||||
case <-c.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchMotion(c *cli.Context) error {
|
||||
motionCh, err := client.WatchMotion(c.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case motionVals := <-motionCh:
|
||||
if c.Bool("json") {
|
||||
json.NewEncoder(os.Stdout).Encode(motionVals)
|
||||
} else if c.Bool("shell") {
|
||||
fmt.Printf(
|
||||
"X=%d\nY=%d\nZ=%d\n",
|
||||
motionVals.X,
|
||||
motionVals.Y,
|
||||
motionVals.Z,
|
||||
)
|
||||
} else {
|
||||
fmt.Println(motionVals)
|
||||
}
|
||||
case <-c.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// heartCmd represents the address command
|
||||
var batteryCmd = &cobra.Command{
|
||||
Use: "battery",
|
||||
Aliases: []string{"batt"},
|
||||
Short: "Watch InfiniTime's battery level for changes",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
battLevelCh, cancel, err := client.WatchBatteryLevel()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting battery level channel")
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
go func() {
|
||||
<-signalCh
|
||||
cancel()
|
||||
os.Exit(0)
|
||||
}()
|
||||
signal.Notify(signalCh,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
for battlevel := range battLevelCh {
|
||||
fmt.Printf("%d%%\n", battlevel)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
watchCmd.AddCommand(batteryCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// heartCmd represents the address command
|
||||
var heartCmd = &cobra.Command{
|
||||
Use: "heart",
|
||||
Short: "Watch InfiniTime's heart rate for changes",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
heartRateCh, cancel, err := client.WatchHeartRate()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting heart rate channel")
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
go func() {
|
||||
<-signalCh
|
||||
cancel()
|
||||
os.Exit(0)
|
||||
}()
|
||||
signal.Notify(signalCh,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
for heartRate := range heartRateCh {
|
||||
fmt.Println(heartRate, "BPM")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
watchCmd.AddCommand(heartCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// heartCmd represents the address command
|
||||
var motionCmd = &cobra.Command{
|
||||
Use: "motion",
|
||||
Short: "Watch InfiniTime's motion values for changes",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
motionValCh, cancel, err := client.WatchMotion()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting motion value channel")
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
go func() {
|
||||
<-signalCh
|
||||
cancel()
|
||||
os.Exit(0)
|
||||
}()
|
||||
signal.Notify(signalCh,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
for motionVals := range motionValCh {
|
||||
if viper.GetBool("shell") {
|
||||
fmt.Printf(
|
||||
"X=%d\nY=%d\nZ=%d\n",
|
||||
motionVals.X,
|
||||
motionVals.Y,
|
||||
motionVals.Z,
|
||||
)
|
||||
} else {
|
||||
fmt.Printf("%+v\n", motionVals)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
watchCmd.AddCommand(motionCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
motionCmd.Flags().BoolP("shell", "s", false, "Output data in shell-compatible format")
|
||||
viper.BindPFlag("shell", motionCmd.Flags().Lookup("shell"))
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
// heartCmd represents the address command
|
||||
var stepsCmd = &cobra.Command{
|
||||
Use: "steps",
|
||||
Short: "Watch InfiniTime's step count for changes",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := viper.Get("client").(*api.Client)
|
||||
|
||||
stepCountCh, cancel, err := client.WatchStepCount()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting step count channel")
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
go func() {
|
||||
<-signalCh
|
||||
cancel()
|
||||
os.Exit(0)
|
||||
}()
|
||||
signal.Notify(signalCh,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
for stepCount := range stepCountCh {
|
||||
fmt.Println(stepCount, "Steps")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
watchCmd.AddCommand(stepsCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// addressCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// addressCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* itd uses bluetooth low energy to communicate with InfiniTime devices
|
||||
* Copyright (C) 2021 Arsen Musayelyan
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package watch
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.arsenm.dev/itd/cmd/itctl/root"
|
||||
)
|
||||
|
||||
// watchCmd represents the watch command
|
||||
var watchCmd = &cobra.Command{
|
||||
Use: "watch",
|
||||
Short: "Watch values from InfiniTime for changes",
|
||||
}
|
||||
|
||||
func init() {
|
||||
root.RootCmd.AddCommand(watchCmd)
|
||||
}
|
@ -28,10 +28,15 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
||||
)
|
||||
if err != nil {
|
||||
// Create new label containing error text
|
||||
errLbl := widget.NewLabel(err.Error())
|
||||
errEntry := widget.NewEntry()
|
||||
errEntry.SetText(err.Error())
|
||||
// If text changed, change it back
|
||||
errEntry.OnChanged = func(string) {
|
||||
errEntry.SetText(err.Error())
|
||||
}
|
||||
// Create new dropdown containing error label
|
||||
content.Add(widget.NewAccordion(
|
||||
widget.NewAccordionItem("More Details", errLbl),
|
||||
widget.NewAccordionItem("More Details", errEntry),
|
||||
))
|
||||
}
|
||||
if fatal {
|
||||
@ -49,5 +54,4 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
||||
// Show error dialog
|
||||
dialog.NewCustom("Error", "Ok", content, parent).Show()
|
||||
}
|
||||
|
||||
}
|
||||
|
163
cmd/itgui/firmware.go
Normal file
@ -0,0 +1,163 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
// Create select to chose between archive and files upgrade
|
||||
typeSelect := widget.NewSelect([]string{"Archive", "Files"}, nil)
|
||||
typeSelect.PlaceHolder = "Upgrade Type"
|
||||
|
||||
// Create map to store files
|
||||
files := map[string]string{}
|
||||
|
||||
// Create and disable start button
|
||||
startBtn := widget.NewButton("Start", nil)
|
||||
startBtn.Disable()
|
||||
|
||||
// Create new file open dialog for archive
|
||||
archiveDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
// Set archive path in map
|
||||
files[".zip"] = uc.URI().Path()
|
||||
// Enable start button
|
||||
startBtn.Enable()
|
||||
}, w)
|
||||
// Only allow .zip files
|
||||
archiveDlg.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
|
||||
// Create button to show dialog
|
||||
archiveBtn := widget.NewButton("Select Archive (.zip)", archiveDlg.Show)
|
||||
|
||||
// Create new file open dialog for firmware image
|
||||
imageDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
|
||||
// Set firmware image path in map
|
||||
files[".bin"] = uc.URI().Path()
|
||||
|
||||
// If the init packet was already selected
|
||||
_, datOk := files[".dat"]
|
||||
if datOk {
|
||||
// Enable start button
|
||||
startBtn.Enable()
|
||||
}
|
||||
}, w)
|
||||
// Only allow .bin files
|
||||
imageDlg.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
|
||||
// Create button to show dialog
|
||||
imageBtn := widget.NewButton("Select Firmware (.bin)", imageDlg.Show)
|
||||
|
||||
// Create new file open dialog for init packet
|
||||
initDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
|
||||
// Set init packet path in map
|
||||
files[".dat"] = uc.URI().Path()
|
||||
|
||||
// If the firmware image was already selected
|
||||
_, binOk := files[".bin"]
|
||||
if binOk {
|
||||
// Enable start button
|
||||
startBtn.Enable()
|
||||
}
|
||||
}, w)
|
||||
// Only allow .dat files
|
||||
initDlg.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
|
||||
// Create button to show dialog
|
||||
initBtn := widget.NewButton("Select Init Packet (.dat)", initDlg.Show)
|
||||
|
||||
var upgType api.UpgradeType = 255
|
||||
// When upgrade type changes
|
||||
typeSelect.OnChanged = func(s string) {
|
||||
// Delete all files from map
|
||||
delete(files, ".bin")
|
||||
delete(files, ".dat")
|
||||
delete(files, ".zip")
|
||||
// Hide all dialog buttons
|
||||
imageBtn.Hide()
|
||||
initBtn.Hide()
|
||||
archiveBtn.Hide()
|
||||
// Disable start button
|
||||
startBtn.Disable()
|
||||
|
||||
switch s {
|
||||
case "Files":
|
||||
// Set file upgrade type
|
||||
upgType = api.UpgradeTypeFiles
|
||||
// Show firmware image and init packet buttons
|
||||
imageBtn.Show()
|
||||
initBtn.Show()
|
||||
case "Archive":
|
||||
// Set archive upgrade type
|
||||
upgType = api.UpgradeTypeArchive
|
||||
// Show archive button
|
||||
archiveBtn.Show()
|
||||
}
|
||||
}
|
||||
// Select archive by default
|
||||
typeSelect.SetSelectedIndex(0)
|
||||
|
||||
// When start button pressed
|
||||
startBtn.OnTapped = func() {
|
||||
var args []string
|
||||
// Append the appropriate files for upgrade type
|
||||
switch upgType {
|
||||
case api.UpgradeTypeArchive:
|
||||
args = append(args, files[".zip"])
|
||||
case api.UpgradeTypeFiles:
|
||||
args = append(args, files[".dat"], files[".bin"])
|
||||
}
|
||||
|
||||
// If args are nil (invalid upgrade type)
|
||||
if args == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create new progress dialog
|
||||
progress := newProgress(w)
|
||||
// Start firmware upgrade
|
||||
progressCh, err := client.FirmwareUpgrade(ctx, upgType, args...)
|
||||
if err != nil {
|
||||
guiErr(err, "Error performing firmware upgrade", false, w)
|
||||
return
|
||||
}
|
||||
// Show progress dialog
|
||||
progress.Show()
|
||||
// For every progress event
|
||||
for progressEvt := range progressCh {
|
||||
// Set progress bar values
|
||||
progress.SetTotal(float64(progressEvt.Total))
|
||||
progress.SetValue(float64(progressEvt.Sent))
|
||||
}
|
||||
// Hide progress dialog
|
||||
progress.Hide()
|
||||
}
|
||||
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
typeSelect,
|
||||
archiveBtn,
|
||||
imageBtn,
|
||||
initBtn,
|
||||
startBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
}
|
360
cmd/itgui/fs.go
Normal file
@ -0,0 +1,360 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/data/binding"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan struct{}) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
|
||||
// Create new binding to store current directory
|
||||
cwdData := binding.NewString()
|
||||
cwdData.Set("/")
|
||||
|
||||
// Create new list binding to store fs listing entries
|
||||
lsData := binding.NewUntypedList()
|
||||
|
||||
// This goroutine waits until the fs tab is opened to
|
||||
// request the listing from the watch
|
||||
go func() {
|
||||
// Wait for opened signal
|
||||
<-opened
|
||||
|
||||
// Show loading pop up
|
||||
loading := newLoadingPopUp(w)
|
||||
loading.Show()
|
||||
|
||||
// Read root directory
|
||||
ls, err := client.ReadDir(ctx, "/")
|
||||
if err != nil {
|
||||
guiErr(err, "Error reading directory", false, w)
|
||||
return
|
||||
}
|
||||
// Set ls binding
|
||||
lsData.Set(lsToAny(ls))
|
||||
|
||||
// Hide loading pop up
|
||||
loading.Hide()
|
||||
}()
|
||||
|
||||
toolbar := widget.NewToolbar(
|
||||
widget.NewToolbarAction(
|
||||
theme.ViewRefreshIcon(),
|
||||
func() {
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
},
|
||||
),
|
||||
widget.NewToolbarAction(
|
||||
theme.UploadIcon(),
|
||||
func() {
|
||||
// Create open dialog for file that will be uploaded
|
||||
dlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
// Get filepath and close
|
||||
localPath := uc.URI().Path()
|
||||
uc.Close()
|
||||
|
||||
// Create new entry to store filepath
|
||||
filenameEntry := widget.NewEntry()
|
||||
// Set entry text to the file name of the selected file
|
||||
filenameEntry.SetText(filepath.Base(localPath))
|
||||
// Create new dialog asking for the filename of the file to be stored on the watch
|
||||
uploadDlg := dialog.NewForm("Upload", "Upload", "Cancel", []*widget.FormItem{
|
||||
widget.NewFormItem("Filename", filenameEntry),
|
||||
}, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Get remote path by joining current directory with filename
|
||||
remotePath := filepath.Join(cwd, filenameEntry.Text)
|
||||
|
||||
// Create new progress dialog
|
||||
progressDlg := newProgress(w)
|
||||
progressDlg.Show()
|
||||
|
||||
// Upload file
|
||||
progressCh, err := client.Upload(ctx, remotePath, localPath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error uploading file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
for progressEvt := range progressCh {
|
||||
progressDlg.SetTotal(float64(progressEvt.Total))
|
||||
progressDlg.SetValue(float64(progressEvt.Sent))
|
||||
if progressEvt.Sent == progressEvt.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Close progress dialog
|
||||
progressDlg.Hide()
|
||||
|
||||
// Add file to listing (avoids full refresh)
|
||||
lsData.Append(api.FileInfo{
|
||||
IsDir: false,
|
||||
Name: filepath.Base(remotePath),
|
||||
})
|
||||
}, w)
|
||||
uploadDlg.Show()
|
||||
}, w)
|
||||
dlg.Show()
|
||||
|
||||
},
|
||||
),
|
||||
widget.NewToolbarAction(
|
||||
theme.FolderNewIcon(),
|
||||
func() {
|
||||
// Create new entry for filename
|
||||
filenameEntry := widget.NewEntry()
|
||||
// Create new dialog to ask for the filename
|
||||
mkdirDialog := dialog.NewForm("Make Directory", "Create", "Cancel", []*widget.FormItem{
|
||||
widget.NewFormItem("Filename", filenameEntry),
|
||||
}, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Get remote path by joining current directory and filename
|
||||
remotePath := filepath.Join(cwd, filenameEntry.Text)
|
||||
|
||||
// Make directory
|
||||
err := client.Mkdir(ctx, remotePath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error creating directory", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Add directory to listing (avoids full refresh)
|
||||
lsData.Append(api.FileInfo{
|
||||
IsDir: true,
|
||||
Name: filepath.Base(remotePath),
|
||||
})
|
||||
}, w)
|
||||
mkdirDialog.Show()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// Add listener to listing data to create the new items on the GUI
|
||||
// whenever the listing changes
|
||||
lsData.AddListener(binding.NewDataListener(func() {
|
||||
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
|
||||
c.Refresh()
|
||||
}))
|
||||
|
||||
return container.NewBorder(
|
||||
nil,
|
||||
toolbar,
|
||||
nil,
|
||||
nil,
|
||||
container.NewVScroll(c),
|
||||
)
|
||||
}
|
||||
|
||||
// makeItems creates GUI objects from listing data
|
||||
func makeItems(
|
||||
ctx context.Context,
|
||||
client *api.Client,
|
||||
lsData binding.UntypedList,
|
||||
cwdData binding.String,
|
||||
w fyne.Window,
|
||||
c *fyne.Container,
|
||||
) []fyne.CanvasObject {
|
||||
// Get listing data
|
||||
ls, _ := lsData.Get()
|
||||
|
||||
// Create output slice with dame length as listing
|
||||
out := make([]fyne.CanvasObject, len(ls))
|
||||
for index, val := range ls {
|
||||
// Assert value as file info
|
||||
item := val.(api.FileInfo)
|
||||
|
||||
var icon fyne.Resource
|
||||
// Decide which icon to use
|
||||
if item.IsDir {
|
||||
if item.Name == ".." {
|
||||
icon = theme.NavigateBackIcon()
|
||||
} else {
|
||||
icon = theme.FolderIcon()
|
||||
}
|
||||
} else {
|
||||
icon = theme.FileIcon()
|
||||
}
|
||||
|
||||
// Create new button with the decided icon and the item name
|
||||
btn := widget.NewButtonWithIcon(item.Name, icon, nil)
|
||||
// Align left
|
||||
btn.Alignment = widget.ButtonAlignLeading
|
||||
// Decide which callback function to use
|
||||
if item.IsDir {
|
||||
btn.OnTapped = func() {
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
cwd = filepath.Join(cwd, item.Name)
|
||||
// Set new current directory
|
||||
cwdData.Set(cwd)
|
||||
// Refresh GUI to display new directory
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
}
|
||||
} else {
|
||||
btn.OnTapped = func() {
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
remotePath := filepath.Join(cwd, item.Name)
|
||||
// Create new save dialog
|
||||
dlg := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
|
||||
if err != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
// Get path of selected file
|
||||
localPath := uc.URI().Path()
|
||||
// Close WriteCloser (it's not needed)
|
||||
uc.Close()
|
||||
|
||||
// Create new progress dialog
|
||||
progressDlg := newProgress(w)
|
||||
progressDlg.Show()
|
||||
|
||||
// Download file
|
||||
progressCh, err := client.Download(ctx, localPath, remotePath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error downloading file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// For every progress event
|
||||
for progressEvt := range progressCh {
|
||||
progressDlg.SetTotal(float64(progressEvt.Total))
|
||||
progressDlg.SetValue(float64(progressEvt.Sent))
|
||||
}
|
||||
|
||||
// Close progress dialog
|
||||
progressDlg.Hide()
|
||||
}, w)
|
||||
// Set filename to the item name
|
||||
dlg.SetFileName(item.Name)
|
||||
dlg.Show()
|
||||
}
|
||||
}
|
||||
|
||||
if item.Name == ".." {
|
||||
out[index] = btn
|
||||
continue
|
||||
}
|
||||
|
||||
moveBtn := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
|
||||
moveEntry := widget.NewEntry()
|
||||
dlg := dialog.NewForm("Move", "Move", "Cancel", []*widget.FormItem{
|
||||
widget.NewFormItem("New Path", moveEntry),
|
||||
}, func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
oldPath := filepath.Join(cwd, item.Name)
|
||||
|
||||
// Rename file
|
||||
err := client.Rename(ctx, oldPath, moveEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error renaming file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh GUI
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
}, w)
|
||||
dlg.Show()
|
||||
})
|
||||
|
||||
removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Join current directory with item name
|
||||
path := filepath.Join(cwd, item.Name)
|
||||
|
||||
// Remove file
|
||||
err := client.Remove(ctx, path)
|
||||
if err != nil {
|
||||
guiErr(err, "Error removing file", false, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh GUI
|
||||
refresh(ctx, cwdData, lsData, client, w, c)
|
||||
})
|
||||
|
||||
// Add button to GUI component list
|
||||
out[index] = container.NewBorder(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
container.NewHBox(moveBtn, removeBtn),
|
||||
btn,
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func refresh(
|
||||
ctx context.Context,
|
||||
cwdData binding.String,
|
||||
lsData binding.UntypedList,
|
||||
client *api.Client,
|
||||
w fyne.Window,
|
||||
c *fyne.Container,
|
||||
) {
|
||||
// Create and show new loading pop up
|
||||
loading := newLoadingPopUp(w)
|
||||
loading.Show()
|
||||
// Close pop up at the end of the function
|
||||
defer loading.Hide()
|
||||
|
||||
// Get current directory
|
||||
cwd, _ := cwdData.Get()
|
||||
// Read directory
|
||||
ls, err := client.ReadDir(ctx, cwd)
|
||||
if err != nil {
|
||||
guiErr(err, "Error reading directory", false, w)
|
||||
return
|
||||
}
|
||||
// Set new listing data
|
||||
lsData.Set(lsToAny(ls))
|
||||
// Create new GUI objects
|
||||
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
|
||||
// Refresh GUI
|
||||
c.Refresh()
|
||||
}
|
||||
|
||||
func lsToAny(ls []api.FileInfo) []interface{} {
|
||||
out := make([]interface{}, len(ls)-1)
|
||||
for i, e := range ls {
|
||||
// Skip first element as it is always "."
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
out[i-1] = e
|
||||
}
|
||||
return out
|
||||
}
|
150
cmd/itgui/graph.go
Normal file
@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/x/fyne/widget/charts"
|
||||
"go.arsenm.dev/itd/api"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func graphTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
// Get user configuration directory
|
||||
userCfgDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cfgDir := filepath.Join(userCfgDir, "itd")
|
||||
dbPath := filepath.Join(cfgDir, "metrics.db")
|
||||
|
||||
// If stat on database returns error, return nil
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open database
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get heart rate data and create chart
|
||||
heartRateData := getData(db, "bpm", "heartRate")
|
||||
heartRate := newLineChartData(nil, heartRateData)
|
||||
|
||||
// Get step count data and create chart
|
||||
stepCountData := getData(db, "steps", "stepCount")
|
||||
stepCount := newLineChartData(nil, stepCountData)
|
||||
|
||||
// Get battery level data and create chart
|
||||
battLevelData := getData(db, "percent", "battLevel")
|
||||
battLevel := newLineChartData(nil, battLevelData)
|
||||
|
||||
// Get motion data
|
||||
motionData := getMotionData(db)
|
||||
// Create chart for each coordinate
|
||||
xChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorRed), motionData["X"])
|
||||
yChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorGreen), motionData["Y"])
|
||||
zChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorBlue), motionData["Z"])
|
||||
|
||||
// Create new max container with all the charts
|
||||
motion := container.NewMax(xChart, yChart, zChart)
|
||||
|
||||
// Create tabs for charts
|
||||
chartTabs := container.NewAppTabs(
|
||||
container.NewTabItem("Heart Rate", heartRate),
|
||||
container.NewTabItem("Step Count", stepCount),
|
||||
container.NewTabItem("Battery Level", battLevel),
|
||||
container.NewTabItem("Motion", motion),
|
||||
)
|
||||
// Place tabs on left
|
||||
chartTabs.SetTabLocation(container.TabLocationLeading)
|
||||
return chartTabs
|
||||
}
|
||||
|
||||
func newLineChartData(col color.Color, data []float64) *charts.LineChart {
|
||||
// Create new line chart
|
||||
lc := charts.NewLineChart(nil)
|
||||
setOpts(lc, col)
|
||||
// If no data, make the stroke transparent
|
||||
if len(data) == 0 {
|
||||
lc.Options().StrokeColor = color.RGBA{0, 0, 0, 0}
|
||||
}
|
||||
// Set data
|
||||
lc.SetData(data)
|
||||
return lc
|
||||
}
|
||||
|
||||
func setOpts(lc *charts.LineChart, col color.Color) {
|
||||
// Get pointer to options
|
||||
opts := lc.Options()
|
||||
// Set fill color to transparent
|
||||
opts.FillColor = color.RGBA{0, 0, 0, 0}
|
||||
// Set stroke width
|
||||
opts.StrokeWidth = 2
|
||||
// If color provided
|
||||
if col != nil {
|
||||
// Set stroke color
|
||||
opts.StrokeColor = col
|
||||
} else {
|
||||
// Set stroke color to orange primary color
|
||||
opts.StrokeColor = theme.PrimaryColorNamed(theme.ColorOrange)
|
||||
}
|
||||
}
|
||||
|
||||
func getData(db *sql.DB, field, table string) []float64 {
|
||||
// Get data from database
|
||||
rows, err := db.Query("SELECT " + field + " FROM " + table + " ORDER BY time;")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []float64
|
||||
for rows.Next() {
|
||||
var val int64
|
||||
// Scan data into int
|
||||
err := rows.Scan(&val)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to float64 and append to data slice
|
||||
out = append(out, float64(val))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func getMotionData(db *sql.DB) map[string][]float64 {
|
||||
// Get data from database
|
||||
rows, err := db.Query("SELECT X, Y, Z FROM motion ORDER BY time;")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := map[string][]float64{}
|
||||
for rows.Next() {
|
||||
var x, y, z int64
|
||||
// Scan data into ints
|
||||
err := rows.Scan(&x, &y, &z)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert to float64 and append to appropriate slice
|
||||
out["X"] = append(out["X"], float64(x))
|
||||
out["Y"] = append(out["Y"], float64(y))
|
||||
out["Z"] = append(out["Z"], float64(z))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
@ -1,123 +1,86 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func infoTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
infoLayout := container.NewVBox(
|
||||
// Add rectangle for a bit of padding
|
||||
canvas.NewRectangle(color.Transparent),
|
||||
)
|
||||
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
|
||||
// Create label for heart rate
|
||||
heartRateLbl := newText("0 BPM", 24)
|
||||
// Creae container to store heart rate section
|
||||
heartRateSect := container.NewVBox(
|
||||
newText("Heart Rate", 12),
|
||||
heartRateLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(heartRateSect)
|
||||
|
||||
heartRateCh, cancel, err := client.WatchHeartRate()
|
||||
// Create titled text for heart rate
|
||||
heartRateText := newTitledText("Heart Rate", "0 BPM")
|
||||
c.Add(heartRateText)
|
||||
// Watch heart rate
|
||||
heartRateCh, err := client.WatchHeartRate(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting heart rate channel", true, parent)
|
||||
guiErr(err, "Error watching heart rate", true, w)
|
||||
}
|
||||
onClose = append(onClose, cancel)
|
||||
go func() {
|
||||
// For every heart rate sample
|
||||
for heartRate := range heartRateCh {
|
||||
// Change text of heart rate label
|
||||
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate)
|
||||
// Refresh label
|
||||
heartRateLbl.Refresh()
|
||||
// Set body of titled text
|
||||
heartRateText.SetBody(fmt.Sprintf("%d BPM", heartRate))
|
||||
}
|
||||
}()
|
||||
|
||||
// Create label for heart rate
|
||||
stepCountLbl := newText("0 Steps", 24)
|
||||
// Creae container to store heart rate section
|
||||
stepCountSect := container.NewVBox(
|
||||
newText("Step Count", 12),
|
||||
stepCountLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(stepCountSect)
|
||||
|
||||
stepCountCh, cancel, err := client.WatchStepCount()
|
||||
// Create titled text for battery level
|
||||
battLevelText := newTitledText("Battery Level", "0%")
|
||||
c.Add(battLevelText)
|
||||
// Watch battery level
|
||||
battLevelCh, err := client.WatchBatteryLevel(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting step count channel", true, parent)
|
||||
guiErr(err, "Error watching battery level", true, w)
|
||||
}
|
||||
onClose = append(onClose, cancel)
|
||||
go func() {
|
||||
for stepCount := range stepCountCh {
|
||||
// Change text of heart rate label
|
||||
stepCountLbl.Text = fmt.Sprintf("%d Steps", stepCount)
|
||||
// Refresh label
|
||||
stepCountLbl.Refresh()
|
||||
}
|
||||
}()
|
||||
|
||||
// Create label for battery level
|
||||
battLevelLbl := newText("0%", 24)
|
||||
// Create container to store battery level section
|
||||
battLevel := container.NewVBox(
|
||||
newText("Battery Level", 12),
|
||||
battLevelLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(battLevel)
|
||||
|
||||
battLevelCh, cancel, err := client.WatchBatteryLevel()
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting battery level channel", true, parent)
|
||||
}
|
||||
onClose = append(onClose, cancel)
|
||||
go func() {
|
||||
// For every battery level sample
|
||||
for battLevel := range battLevelCh {
|
||||
// Change text of battery level label
|
||||
battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel)
|
||||
// Refresh label
|
||||
battLevelLbl.Refresh()
|
||||
// Set body of titled text
|
||||
battLevelText.SetBody(fmt.Sprintf("%d%%", battLevel))
|
||||
}
|
||||
}()
|
||||
|
||||
fwVerString, err := client.Version()
|
||||
// Create titled text for step count
|
||||
stepCountText := newTitledText("Step Count", "0 Steps")
|
||||
c.Add(stepCountText)
|
||||
// Watch step count
|
||||
stepCountCh, err := client.WatchStepCount(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting firmware string", true, parent)
|
||||
guiErr(err, "Error watching step count", true, w)
|
||||
}
|
||||
go func() {
|
||||
// For every step count sample
|
||||
for stepCount := range stepCountCh {
|
||||
// Set body of titled text
|
||||
stepCountText.SetBody(fmt.Sprintf("%d Steps", stepCount))
|
||||
}
|
||||
}()
|
||||
|
||||
fwVer := container.NewVBox(
|
||||
newText("Firmware Version", 12),
|
||||
newText(fwVerString, 24),
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(fwVer)
|
||||
|
||||
btAddrString, err := client.Address()
|
||||
// Create new titled text for address
|
||||
addressText := newTitledText("Address", "")
|
||||
c.Add(addressText)
|
||||
// Get address
|
||||
address, err := client.Address(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
guiErr(err, "Error getting address", true, w)
|
||||
}
|
||||
// Set body of titled text
|
||||
addressText.SetBody(address)
|
||||
|
||||
btAddr := container.NewVBox(
|
||||
newText("Bluetooth Address", 12),
|
||||
newText(btAddrString, 24),
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
infoLayout.Add(btAddr)
|
||||
// Create new titled text for version
|
||||
versionText := newTitledText("Version", "")
|
||||
c.Add(versionText)
|
||||
// Get version
|
||||
version, err := client.Version(ctx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting version", true, w)
|
||||
}
|
||||
// Set body of titled text
|
||||
versionText.SetBody(version)
|
||||
|
||||
return infoLayout
|
||||
}
|
||||
|
||||
func newText(t string, size float32) *canvas.Text {
|
||||
text := canvas.NewText(t, theme.ForegroundColor())
|
||||
text.TextSize = size
|
||||
return text
|
||||
return container.NewVScroll(c)
|
||||
}
|
||||
|
21
cmd/itgui/loading.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func newLoadingPopUp(w fyne.Window) *widget.PopUp {
|
||||
pb := widget.NewProgressBarInfinite()
|
||||
rect := canvas.NewRectangle(color.Transparent)
|
||||
rect.SetMinSize(fyne.NewSize(200, 0))
|
||||
|
||||
return widget.NewModalPopUp(
|
||||
container.NewMax(rect, pb),
|
||||
w.Canvas(),
|
||||
)
|
||||
}
|
@ -1,43 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
var onClose []func()
|
||||
|
||||
func main() {
|
||||
// Create new app
|
||||
a := app.New()
|
||||
// Create new window with title "itgui"
|
||||
window := a.NewWindow("itgui")
|
||||
window.SetOnClosed(func() {
|
||||
for _, closeFn := range onClose {
|
||||
closeFn()
|
||||
}
|
||||
})
|
||||
w := a.NewWindow("itgui")
|
||||
|
||||
// Create new context for use with the API client
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Connect to ITD API
|
||||
client, err := api.New(api.DefaultAddr)
|
||||
if err != nil {
|
||||
guiErr(err, "Error connecting to itd", true, window)
|
||||
guiErr(err, "Error connecting to ITD", true, w)
|
||||
}
|
||||
onClose = append(onClose, func() {
|
||||
client.Close()
|
||||
})
|
||||
|
||||
// Create new app tabs container
|
||||
// Create channel to signal that the fs tab has been opened
|
||||
fsOpened := make(chan struct{})
|
||||
fsOnce := &sync.Once{}
|
||||
|
||||
// Create app tabs
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Info", infoTab(window, client)),
|
||||
container.NewTabItem("Motion", motionTab(window, client)),
|
||||
container.NewTabItem("Notify", notifyTab(window, client)),
|
||||
container.NewTabItem("Set Time", timeTab(window, client)),
|
||||
container.NewTabItem("Upgrade", upgradeTab(window, client)),
|
||||
container.NewTabItem("Info", infoTab(ctx, client, w)),
|
||||
container.NewTabItem("Motion", motionTab(ctx, client, w)),
|
||||
container.NewTabItem("Notify", notifyTab(ctx, client, w)),
|
||||
container.NewTabItem("FS", fsTab(ctx, client, w, fsOpened)),
|
||||
container.NewTabItem("Time", timeTab(ctx, client, w)),
|
||||
container.NewTabItem("Firmware", firmwareTab(ctx, client, w)),
|
||||
)
|
||||
|
||||
// Set tabs as window content
|
||||
window.SetContent(tabs)
|
||||
// Show window and run app
|
||||
window.ShowAndRun()
|
||||
metricsTab := graphTab(ctx, client, w)
|
||||
if metricsTab != nil {
|
||||
tabs.Append(container.NewTabItem("Metrics", metricsTab))
|
||||
}
|
||||
|
||||
// When a tab is selected
|
||||
tabs.OnSelected = func(ti *container.TabItem) {
|
||||
// If the tab's name is FS
|
||||
if ti.Text == "FS" {
|
||||
// Signal fsOpened only once
|
||||
fsOnce.Do(func() {
|
||||
fsOpened <- struct{}{}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel context on close
|
||||
w.SetOnClosed(cancel)
|
||||
// Set content and show window
|
||||
w.SetContent(tabs)
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
@ -1,105 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strconv"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func motionTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
// Create label for heart rate
|
||||
xCoordLbl := newText("0", 24)
|
||||
// Creae container to store heart rate section
|
||||
xCoordSect := container.NewVBox(
|
||||
newText("X Coordinate", 12),
|
||||
xCoordLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
// Create titledText for each coordinate
|
||||
xText := newTitledText("X Coordinate", "0")
|
||||
yText := newTitledText("Y Coordinate", "0")
|
||||
zText := newTitledText("Z Coordinate", "0")
|
||||
|
||||
// Create label for heart rate
|
||||
yCoordLbl := newText("0", 24)
|
||||
// Creae container to store heart rate section
|
||||
yCoordSect := container.NewVBox(
|
||||
newText("Y Coordinate", 12),
|
||||
yCoordLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
// Create label for heart rate
|
||||
zCoordLbl := newText("0", 24)
|
||||
// Creae container to store heart rate section
|
||||
zCoordSect := container.NewVBox(
|
||||
newText("Z Coordinate", 12),
|
||||
zCoordLbl,
|
||||
canvas.NewLine(theme.ShadowColor()),
|
||||
)
|
||||
var ctxCancel func()
|
||||
|
||||
// Create variable to keep track of whether motion started
|
||||
started := false
|
||||
|
||||
// Create button to stop motion
|
||||
stopBtn := widget.NewButton("Stop", nil)
|
||||
// Create button to start motion
|
||||
startBtn := widget.NewButton("Start", func() {
|
||||
// if motion is started
|
||||
if started {
|
||||
// Do nothing
|
||||
return
|
||||
}
|
||||
// Set motion started
|
||||
started = true
|
||||
// Watch motion values
|
||||
motionCh, cancel, err := client.WatchMotion()
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting heart rate channel", true, parent)
|
||||
}
|
||||
// Create done channel
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case motion := <-motionCh:
|
||||
// Set labels to new values
|
||||
xCoordLbl.Text = strconv.Itoa(int(motion.X))
|
||||
yCoordLbl.Text = strconv.Itoa(int(motion.Y))
|
||||
zCoordLbl.Text = strconv.Itoa(int(motion.Z))
|
||||
// Refresh labels to display new values
|
||||
xCoordLbl.Refresh()
|
||||
yCoordLbl.Refresh()
|
||||
zCoordLbl.Refresh()
|
||||
}
|
||||
// Create start button
|
||||
toggleBtn := widget.NewButton("Start", nil)
|
||||
// Set button's on tapped callback
|
||||
toggleBtn.OnTapped = func() {
|
||||
switch toggleBtn.Text {
|
||||
case "Start":
|
||||
// Create new context for motion
|
||||
motionCtx, cancel := context.WithCancel(ctx)
|
||||
// Set ctxCancel to function so that stop button can run it
|
||||
ctxCancel = cancel
|
||||
// Watch motion
|
||||
motionCh, err := client.WatchMotion(motionCtx)
|
||||
if err != nil {
|
||||
guiErr(err, "Error watching motion", false, w)
|
||||
return
|
||||
}
|
||||
}()
|
||||
// Create stop function
|
||||
stopBtn.OnTapped = func() {
|
||||
done <- struct{}{}
|
||||
started = false
|
||||
cancel()
|
||||
go func() {
|
||||
// For every motion event
|
||||
for motion := range motionCh {
|
||||
// Set coordinates
|
||||
xText.SetBody(fmt.Sprint(motion.X))
|
||||
yText.SetBody(fmt.Sprint(motion.Y))
|
||||
zText.SetBody(fmt.Sprint(motion.Z))
|
||||
}
|
||||
}()
|
||||
// Set button text to "Stop"
|
||||
toggleBtn.SetText("Stop")
|
||||
case "Stop":
|
||||
// Cancel motion context
|
||||
ctxCancel()
|
||||
// Set button text to "Start"
|
||||
toggleBtn.SetText("Start")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
// Run stop button function on close if possible
|
||||
onClose = append(onClose, func() {
|
||||
if stopBtn.OnTapped != nil {
|
||||
stopBtn.OnTapped()
|
||||
}
|
||||
})
|
||||
|
||||
// Return new container containing all elements
|
||||
return container.NewVBox(
|
||||
// Add rectangle for a bit of padding
|
||||
canvas.NewRectangle(color.Transparent),
|
||||
startBtn,
|
||||
stopBtn,
|
||||
xCoordSect,
|
||||
yCoordSect,
|
||||
zCoordSect,
|
||||
)
|
||||
return container.NewVScroll(container.NewVBox(
|
||||
toggleBtn,
|
||||
xText,
|
||||
yText,
|
||||
zText,
|
||||
))
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
@ -8,30 +10,31 @@ import (
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
// Create new entry for notification title
|
||||
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
c.Add(layout.NewSpacer())
|
||||
|
||||
// Create new entry for title
|
||||
titleEntry := widget.NewEntry()
|
||||
titleEntry.SetPlaceHolder("Title")
|
||||
c.Add(titleEntry)
|
||||
|
||||
// Create multiline entry for notification body
|
||||
// Create new multiline entry for body
|
||||
bodyEntry := widget.NewMultiLineEntry()
|
||||
bodyEntry.SetPlaceHolder("Body")
|
||||
c.Add(bodyEntry)
|
||||
|
||||
// Create new button to send notification
|
||||
// Create new send button
|
||||
sendBtn := widget.NewButton("Send", func() {
|
||||
err := client.Notify(titleEntry.Text, bodyEntry.Text)
|
||||
// Send notification
|
||||
err := client.Notify(ctx, titleEntry.Text, bodyEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error sending notification", false, parent)
|
||||
guiErr(err, "Error sending notification", false, w)
|
||||
return
|
||||
}
|
||||
})
|
||||
c.Add(sendBtn)
|
||||
|
||||
// Return new container containing all elements
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
titleEntry,
|
||||
bodyEntry,
|
||||
sendBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
c.Add(layout.NewSpacer())
|
||||
return container.NewVScroll(c)
|
||||
}
|
||||
|
50
cmd/itgui/progress.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type progress struct {
|
||||
lbl *widget.Label
|
||||
pb *widget.ProgressBar
|
||||
*widget.PopUp
|
||||
}
|
||||
|
||||
func newProgress(w fyne.Window) progress {
|
||||
out := progress{}
|
||||
|
||||
// Create label to show how many bytes transfered and center it
|
||||
out.lbl = widget.NewLabel("0 / 0 B")
|
||||
out.lbl.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Create new progress bar
|
||||
out.pb = widget.NewProgressBar()
|
||||
|
||||
// Create new rectangle to set the size of the popup
|
||||
sizeRect := canvas.NewRectangle(color.Transparent)
|
||||
sizeRect.SetMinSize(fyne.NewSize(300, 50))
|
||||
|
||||
// Create vbox for label and progress bar
|
||||
l := container.NewVBox(out.lbl, out.pb)
|
||||
// Create popup
|
||||
out.PopUp = widget.NewModalPopUp(container.NewMax(l, sizeRect), w.Canvas())
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (p progress) SetTotal(v float64) {
|
||||
p.pb.Max = v
|
||||
p.pb.Refresh()
|
||||
p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", p.pb.Value, v))
|
||||
}
|
||||
|
||||
func (p progress) SetValue(v float64) {
|
||||
p.pb.SetValue(v)
|
||||
p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", v, p.pb.Max))
|
||||
}
|
BIN
cmd/itgui/screenshots/firmware.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
cmd/itgui/screenshots/fs.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
cmd/itgui/screenshots/info.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
cmd/itgui/screenshots/metrics.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
cmd/itgui/screenshots/mkdir.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
cmd/itgui/screenshots/motion.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
cmd/itgui/screenshots/notify.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
cmd/itgui/screenshots/progress.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
cmd/itgui/screenshots/time.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
@ -10,51 +11,47 @@ import (
|
||||
"go.arsenm.dev/itd/api"
|
||||
)
|
||||
|
||||
func timeTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
// Create new entry for time string
|
||||
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||
c := container.NewVBox()
|
||||
c.Add(layout.NewSpacer())
|
||||
|
||||
// Create entry for time string
|
||||
timeEntry := widget.NewEntry()
|
||||
// Set text to current time formatter properly
|
||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||
timeEntry.SetPlaceHolder("RFC1123")
|
||||
|
||||
// Create button to set current time
|
||||
currentBtn := widget.NewButton("Set Current", func() {
|
||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||
setTime(client, true)
|
||||
})
|
||||
|
||||
// Create button to set time inside entry
|
||||
timeBtn := widget.NewButton("Set", func() {
|
||||
// Parse time as RFC1123 string
|
||||
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
|
||||
setCurrentBtn := widget.NewButton("Set current time", func() {
|
||||
// Set current time
|
||||
err := client.SetTime(ctx, time.Now())
|
||||
if err != nil {
|
||||
guiErr(err, "Error parsing time string", false, parent)
|
||||
guiErr(err, "Error setting time", false, w)
|
||||
return
|
||||
}
|
||||
// Set time to parsed time
|
||||
setTime(client, false, parsedTime)
|
||||
// Set time entry to current time
|
||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||
})
|
||||
|
||||
// Return new container with all elements centered
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
timeEntry,
|
||||
currentBtn,
|
||||
timeBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
}
|
||||
// Create button to set time from entry
|
||||
setBtn := widget.NewButton("Set", func() {
|
||||
// Parse RFC1123 time string in entry
|
||||
newTime, err := time.Parse(time.RFC1123, timeEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error parsing time string", false, w)
|
||||
return
|
||||
}
|
||||
// Set time from parsed string
|
||||
err = client.SetTime(ctx, newTime)
|
||||
if err != nil {
|
||||
guiErr(err, "Error setting time", false, w)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// setTime sets the first element in the variadic parameter
|
||||
// if current is false, otherwise, it sets the current time.
|
||||
func setTime(client *api.Client, current bool, t ...time.Time) error {
|
||||
var err error
|
||||
if current {
|
||||
err = client.SetTimeNow()
|
||||
} else {
|
||||
err = client.SetTime(t[0])
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
c.Add(timeEntry)
|
||||
c.Add(setBtn)
|
||||
c.Add(setCurrentBtn)
|
||||
|
||||
c.Add(layout.NewSpacer())
|
||||
return c
|
||||
}
|
||||
|
35
cmd/itgui/titledText.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import "fyne.io/fyne/v2/widget"
|
||||
|
||||
type titledText struct {
|
||||
*widget.RichText
|
||||
}
|
||||
|
||||
func newTitledText(title, text string) titledText {
|
||||
titleStyle := widget.RichTextStyleHeading
|
||||
titleStyle.TextStyle.Bold = false
|
||||
return titledText{
|
||||
widget.NewRichText(
|
||||
&widget.TextSegment{
|
||||
Style: widget.RichTextStyleParagraph,
|
||||
Text: title,
|
||||
},
|
||||
&widget.TextSegment{
|
||||
Style: titleStyle,
|
||||
Text: text,
|
||||
},
|
||||
&widget.SeparatorSegment{},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (t titledText) SetTitle(s string) {
|
||||
t.RichText.Segments[0].(*widget.TextSegment).Text = s
|
||||
t.Refresh()
|
||||
}
|
||||
|
||||
func (t titledText) SetBody(s string) {
|
||||
t.RichText.Segments[1].(*widget.TextSegment).Text = s
|
||||
t.Refresh()
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"go.arsenm.dev/itd/api"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
)
|
||||
|
||||
func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
||||
var (
|
||||
archivePath string
|
||||
firmwarePath string
|
||||
initPktPath string
|
||||
)
|
||||
|
||||
var archiveBtn *widget.Button
|
||||
// Create archive selection dialog
|
||||
archiveDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
|
||||
if e != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
uc.Close()
|
||||
archivePath = uc.URI().Path()
|
||||
archiveBtn.SetText(fmt.Sprintf("Select archive (.zip) [%s]", filepath.Base(archivePath)))
|
||||
}, parent)
|
||||
// Limit dialog to .zip files
|
||||
archiveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
|
||||
// Create button to show dialog
|
||||
archiveBtn = widget.NewButton("Select archive (.zip)", archiveDialog.Show)
|
||||
|
||||
var firmwareBtn *widget.Button
|
||||
// Create firmware selection dialog
|
||||
firmwareDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
|
||||
if e != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
uc.Close()
|
||||
firmwarePath = uc.URI().Path()
|
||||
firmwareBtn.SetText(fmt.Sprintf("Select firmware (.bin) [%s]", filepath.Base(firmwarePath)))
|
||||
}, parent)
|
||||
// Limit dialog to .bin files
|
||||
firmwareDialog.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
|
||||
// Create button to show dialog
|
||||
firmwareBtn = widget.NewButton("Select firmware (.bin)", firmwareDialog.Show)
|
||||
|
||||
var initPktBtn *widget.Button
|
||||
// Create init packet selection dialog
|
||||
initPktDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
|
||||
if e != nil || uc == nil {
|
||||
return
|
||||
}
|
||||
uc.Close()
|
||||
initPktPath = uc.URI().Path()
|
||||
initPktBtn.SetText(fmt.Sprintf("Select init packet (.dat) [%s]", filepath.Base(initPktPath)))
|
||||
}, parent)
|
||||
// Limit dialog to .dat files
|
||||
initPktDialog.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
|
||||
// Create button to show dialog
|
||||
initPktBtn = widget.NewButton("Select init packet (.dat)", initPktDialog.Show)
|
||||
|
||||
// Hide init packet and firmware buttons
|
||||
initPktBtn.Hide()
|
||||
firmwareBtn.Hide()
|
||||
|
||||
// Create dropdown to select upgrade type
|
||||
upgradeTypeSelect := widget.NewSelect([]string{
|
||||
"Archive",
|
||||
"Files",
|
||||
}, func(s string) {
|
||||
// Hide all buttons
|
||||
archiveBtn.Hide()
|
||||
initPktBtn.Hide()
|
||||
firmwareBtn.Hide()
|
||||
// Unhide appropriate button(s)
|
||||
switch s {
|
||||
case "Archive":
|
||||
archiveBtn.Show()
|
||||
case "Files":
|
||||
initPktBtn.Show()
|
||||
firmwareBtn.Show()
|
||||
}
|
||||
})
|
||||
// Select first elemetn
|
||||
upgradeTypeSelect.SetSelectedIndex(0)
|
||||
|
||||
// Create new button to start DFU
|
||||
startBtn := widget.NewButton("Start", func() {
|
||||
// If archive path does not exist and both init packet and firmware paths
|
||||
// also do not exist, return error
|
||||
if archivePath == "" && (initPktPath == "" && firmwarePath == "") {
|
||||
guiErr(nil, "Upgrade requires archive or files selected", false, parent)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new label for byte progress
|
||||
progressLbl := widget.NewLabelWithStyle("0 / 0 B", fyne.TextAlignCenter, fyne.TextStyle{})
|
||||
// Create new progress bar
|
||||
progressBar := widget.NewProgressBar()
|
||||
// Create modal dialog containing label and progress bar
|
||||
progressDlg := widget.NewModalPopUp(container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
progressLbl,
|
||||
progressBar,
|
||||
layout.NewSpacer(),
|
||||
), parent.Canvas())
|
||||
// Resize modal to 300x100
|
||||
progressDlg.Resize(fyne.NewSize(300, 100))
|
||||
|
||||
var fwUpgType api.UpgradeType
|
||||
var files []string
|
||||
// Get appropriate upgrade type and file paths
|
||||
switch upgradeTypeSelect.Selected {
|
||||
case "Archive":
|
||||
fwUpgType = types.UpgradeTypeArchive
|
||||
files = append(files, archivePath)
|
||||
case "Files":
|
||||
fwUpgType = types.UpgradeTypeFiles
|
||||
files = append(files, initPktPath, firmwarePath)
|
||||
}
|
||||
|
||||
progress, err := client.FirmwareUpgrade(fwUpgType, files...)
|
||||
if err != nil {
|
||||
guiErr(err, "Error initiating DFU", false, parent)
|
||||
return
|
||||
}
|
||||
|
||||
// Show progress dialog
|
||||
progressDlg.Show()
|
||||
// Hide progress dialog after completion
|
||||
defer progressDlg.Hide()
|
||||
|
||||
for event := range progress {
|
||||
// Set label text to received / total B
|
||||
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
|
||||
// Set progress bar values
|
||||
progressBar.Max = float64(event.Total)
|
||||
progressBar.Value = float64(event.Received)
|
||||
// Refresh progress bar
|
||||
progressBar.Refresh()
|
||||
// If transfer finished, break
|
||||
if event.Sent == event.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Return container containing all elements
|
||||
return container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
upgradeTypeSelect,
|
||||
archiveBtn,
|
||||
firmwareBtn,
|
||||
initPktBtn,
|
||||
startBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
}
|
103
config.go
@ -2,46 +2,105 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf/parsers/toml"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgDir string
|
||||
|
||||
func init() {
|
||||
// Set up logger
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
// Set config settings
|
||||
// Get user's configuration directory
|
||||
userCfgDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfgDir = filepath.Join(userCfgDir, "itd")
|
||||
|
||||
// If config dir is not readable
|
||||
if _, err = os.ReadDir(cfgDir); err != nil {
|
||||
// Create config dir with 700 permissions
|
||||
err = os.MkdirAll(cfgDir, 0700)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get current and old config paths
|
||||
cfgPath := filepath.Join(cfgDir, "itd.toml")
|
||||
oldCfgPath := filepath.Join(userCfgDir, "itd.toml")
|
||||
|
||||
// If old config path exists
|
||||
if _, err = os.Stat(oldCfgPath); err == nil {
|
||||
// Move old config to new path
|
||||
err = os.Rename(oldCfgPath, cfgPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set config defaults
|
||||
setCfgDefaults()
|
||||
viper.AddConfigPath("$HOME/.config")
|
||||
viper.AddConfigPath("/etc")
|
||||
viper.SetConfigName("itd")
|
||||
viper.SetConfigType("toml")
|
||||
viper.WatchConfig()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.SetEnvPrefix("itd")
|
||||
// Ignore error because defaults set
|
||||
viper.ReadInConfig()
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Load config files
|
||||
etcProvider := file.Provider("/etc/itd.toml")
|
||||
cfgProvider := file.Provider(cfgPath)
|
||||
k.Load(etcProvider, toml.Parser())
|
||||
k.Load(cfgProvider, toml.Parser())
|
||||
|
||||
// Watch configs for changes
|
||||
cfgWatch(etcProvider)
|
||||
cfgWatch(cfgProvider)
|
||||
|
||||
// Load envireonment variables
|
||||
k.Load(env.Provider("ITD_", "_", func(s string) string {
|
||||
return strings.ToLower(strings.TrimPrefix(s, "ITD_"))
|
||||
}), nil)
|
||||
}
|
||||
|
||||
func cfgWatch(provider *file.File) {
|
||||
// Watch for changes and reload when detected
|
||||
provider.Watch(func(_ interface{}, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k.Load(provider, toml.Parser())
|
||||
})
|
||||
}
|
||||
|
||||
func setCfgDefaults() {
|
||||
viper.SetDefault("cfg.version", 2)
|
||||
k.Load(confmap.Provider(map[string]interface{}{
|
||||
"bluetooth.adapter": "hci0",
|
||||
|
||||
viper.SetDefault("socket.path", "/tmp/itd/socket")
|
||||
"socket.path": "/tmp/itd/socket",
|
||||
|
||||
viper.SetDefault("conn.reconnect", true)
|
||||
"conn.reconnect": true,
|
||||
|
||||
viper.SetDefault("on.connect.notify", true)
|
||||
"conn.whitelist.enabled": false,
|
||||
"conn.whitelist.devices": []string{},
|
||||
|
||||
viper.SetDefault("on.reconnect.notify", true)
|
||||
viper.SetDefault("on.reconnect.setTime", true)
|
||||
"on.connect.notify": true,
|
||||
|
||||
viper.SetDefault("notifs.ignore.sender", []string{})
|
||||
viper.SetDefault("notifs.ignore.summary", []string{"InfiniTime"})
|
||||
viper.SetDefault("notifs.ignore.body", []string{})
|
||||
"on.reconnect.notify": true,
|
||||
"on.reconnect.setTime": true,
|
||||
|
||||
viper.SetDefault("music.vol.interval", 5)
|
||||
"notifs.translit.use": []string{"eASCII"},
|
||||
"notifs.translit.custom": []string{},
|
||||
|
||||
"notifs.ignore.sender": []string{},
|
||||
"notifs.ignore.summary": []string{"InfiniTime"},
|
||||
"notifs.ignore.body": []string{},
|
||||
|
||||
"music.vol.interval": 5,
|
||||
}, "."), nil)
|
||||
}
|
||||
|
41
dbus.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func newSystemBusConn(ctx context.Context) (*dbus.Conn, error) {
|
||||
// Connect to dbus session bus
|
||||
conn, err := dbus.SystemBusPrivate(dbus.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Auth(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Hello()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func newSessionBusConn(ctx context.Context) (*dbus.Conn, error) {
|
||||
// Connect to dbus session bus
|
||||
conn, err := dbus.SessionBusPrivate(dbus.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Auth(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.Hello()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
96
go.mod
@ -1,33 +1,77 @@
|
||||
module go.arsenm.dev/itd
|
||||
|
||||
go 1.16
|
||||
go 1.17
|
||||
|
||||
replace fyne.io/x/fyne => github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.1.0
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect
|
||||
fyne.io/fyne/v2 v2.1.4
|
||||
fyne.io/x/fyne v0.0.0-20220107050838-c4a1de51d4ce
|
||||
github.com/cheggaaa/pb/v3 v3.0.8
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
|
||||
github.com/godbus/dbus/v5 v5.0.5
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.2
|
||||
github.com/mozillazg/go-pinyin v0.18.0
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/viper v1.9.0
|
||||
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
||||
github.com/yuin/goldmark v1.4.1 // indirect
|
||||
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 // indirect
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
|
||||
github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/knadh/koanf v1.4.0
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mozillazg/go-pinyin v0.19.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
go.arsenm.dev/infinitime v0.0.0-20221020084115-54fdd19bec3c
|
||||
go.arsenm.dev/lrpc v0.0.0-20220513001344-3bcc01fdb6a0
|
||||
golang.org/x/text v0.3.7
|
||||
modernc.org/sqlite v1.17.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/VividCortex/ewma v1.1.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.10.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20211120171407-1837d6608d8c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
||||
github.com/stretchr/testify v1.7.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/yuin/goldmark v1.4.4 // indirect
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||
golang.org/x/tools v0.1.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.36.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.6 // indirect
|
||||
modernc.org/libc v1.16.7 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.1.1 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
)
|
||||
|
326
go.sum
@ -18,11 +18,6 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
||||
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
||||
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
@ -32,7 +27,6 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
@ -43,87 +37,97 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
fyne.io/fyne/v2 v2.1.0 h1:qzdkaXL/UpmMtG4FlsX9xMZ0Q93CRzLxkoiSXyplP/I=
|
||||
fyne.io/fyne/v2 v2.1.0/go.mod h1:c1vwI38Ebd0dAdxVa6H1Pj6/+cK1xtDy61+I31g+s14=
|
||||
fyne.io/fyne/v2 v2.1.4 h1:bt1+28++kAzRzPB0GM2EuSV4cnl8rXNX4cjfd8G06Rc=
|
||||
fyne.io/fyne/v2 v2.1.4/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ=
|
||||
github.com/Andrew-M-C/go.jsonvalue v1.1.2-0.20211223013816-e873b56b4a84/go.mod h1:oTJGG91FhtsxvUFVwHSvr6zuaTcAuroj/ToxfT7Ox8U=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
|
||||
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
|
||||
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
|
||||
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
|
||||
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
|
||||
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA=
|
||||
github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
|
||||
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
|
||||
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
|
||||
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
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/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b h1:M0/hjawi9ur15zpqL/h66ga87jlYA7iAuZ4HC6ak08k=
|
||||
github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259 h1:8q7+xl2D2qHPLTII1t4vSMNP2VKwDcn+Avf2WXvdB1A=
|
||||
github.com/go-gl/gl v0.0.0-20210905235341-f7a045908259/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
|
||||
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 h1:KgfIc81yNEUKNAsF+Mt3C1Cl+iQqKF1r7nWEKzL0c2Y=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
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.5 h1:9Eg0XUhQxtkV8ykTMKtMMYY72g4NgxtRq4jgh4Ih5YM=
|
||||
github.com/godbus/dbus/v5 v5.0.5/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
|
||||
@ -139,7 +143,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@ -157,7 +160,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -171,12 +174,12 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
@ -188,35 +191,38 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
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 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@ -224,79 +230,94 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knadh/koanf v1.4.0 h1:/k0Bh49SqLyLNfte9r6cvuZWrApOQhglOmhIU3L/zDw=
|
||||
github.com/knadh/koanf v1.4.0/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
|
||||
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb h1:+fP6ENsbd+BUOmD/kSjNtrOmi2vgJ/JfWDSWjTKmTVY=
|
||||
github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb/go.mod h1:jBspDudEQ+Rdono8vBGHDtMUPE8ZpB/xq7FUYRqT3CI=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
|
||||
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mozillazg/go-pinyin v0.18.0 h1:hQompXO23/0ohH8YNjvfsAITnCQImCiR/Fny8EhIeW0=
|
||||
github.com/mozillazg/go-pinyin v0.18.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
|
||||
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d h1:EG/xyWjHT19rkUpwsWSkyiCCmyqNwFovr9m10rhyOxU=
|
||||
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
||||
github.com/mozillazg/go-pinyin v0.19.0 h1:p+J8/kjJ558KPvVGYLvqBhxf8jbZA2exSLCs2uUVN8c=
|
||||
github.com/mozillazg/go-pinyin v0.19.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
|
||||
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 h1:kOnq7TfaAO2Vc/MHxPqFIXe00y1qBxJAvhctXdko6vo=
|
||||
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -304,46 +325,46 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
|
||||
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
|
||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE=
|
||||
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/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk=
|
||||
github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4=
|
||||
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
|
||||
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe h1:J5Ga/gb+4/WgJBupg9Fp8F6JQnUT3UF+asoTweLi9Jc=
|
||||
github.com/srwiley/oksvg v0.0.0-20210519022825-9fc0c575d5fe/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
|
||||
github.com/srwiley/oksvg v0.0.0-20211120171407-1837d6608d8c h1:+e9myEHblxwU1r2Jb5PKzepMcsuig7+NUz+K53lBNaQ=
|
||||
github.com/srwiley/oksvg v0.0.0-20211120171407-1837d6608d8c/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
|
||||
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 h1:oDMiXaTMyBEuZMU53atpxqYsSB3U1CHkeAu2zr6wTeY=
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
|
||||
@ -353,22 +374,33 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72 h1:e8kOuL6Jj8ZjJzkGwJ3xqpGG9EhUzfvZk9AlSsm3X1U=
|
||||
go.arsenm.dev/infinitime v0.0.0-20211023042633-53aa6f8a0c72/go.mod h1:gaepaueUz4J5FfxuV19B4w5pi+V3mD0LTef50ryxr/Q=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
|
||||
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
|
||||
go.arsenm.dev/infinitime v0.0.0-20221020084115-54fdd19bec3c h1:1YnQeP7BSjbpF7umNKNee4BU7rad6zcjYMhff2smjrY=
|
||||
go.arsenm.dev/infinitime v0.0.0-20221020084115-54fdd19bec3c/go.mod h1:K3NJ6fyPv5qqHUedB3MccKOE0whJMJZ80l/yTzzTrgc=
|
||||
go.arsenm.dev/lrpc v0.0.0-20220513001344-3bcc01fdb6a0 h1:1K96g1eww+77GeGchwMhd0NTrs7Mk/Hc3M3ItW5NbG4=
|
||||
go.arsenm.dev/lrpc v0.0.0-20220513001344-3bcc01fdb6a0/go.mod h1:goK9z735lfXmqlDxu9qN7FS8t0HJHN3PjyDtCToUY4w=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
@ -379,7 +411,6 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
@ -388,10 +419,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -405,8 +436,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -429,6 +460,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -444,7 +476,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@ -452,6 +483,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
@ -467,9 +499,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0 h1:qOfNqBm5gk93LjGZo1MJaKY6Bph39zOKz1Hz2ogHj1w=
|
||||
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -482,10 +514,6 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -500,9 +528,10 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -511,18 +540,14 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -549,20 +574,20 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/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-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@ -587,7 +612,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@ -620,19 +644,20 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
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-20201124115921-2c860bdd6e78/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-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@ -656,12 +681,6 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
|
||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
||||
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
||||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@ -671,6 +690,7 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -709,21 +729,12 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
|
||||
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
@ -740,13 +751,7 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -759,16 +764,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
|
||||
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@ -784,6 +787,41 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo=
|
||||
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
|
||||
modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA=
|
||||
modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU=
|
||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.17.2 h1:TjmF36Wi5QcPYqRoAacV1cAyJ7xB/CD0ExpVUEMebnw=
|
||||
modernc.org/sqlite v1.17.2/go.mod h1:GOQmuiXd6pTTes1Fi2s9apiCcD/wbKQtBZ0Nw6/etjM=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao=
|
||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
@ -1,58 +0,0 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
ReqTypeHeartRate = iota
|
||||
ReqTypeBattLevel
|
||||
ReqTypeFwVersion
|
||||
ReqTypeFwUpgrade
|
||||
ReqTypeBtAddress
|
||||
ReqTypeNotify
|
||||
ReqTypeSetTime
|
||||
ReqTypeWatchHeartRate
|
||||
ReqTypeWatchBattLevel
|
||||
ReqTypeMotion
|
||||
ReqTypeWatchMotion
|
||||
ReqTypeStepCount
|
||||
ReqTypeWatchStepCount
|
||||
ReqTypeCancel
|
||||
)
|
||||
|
||||
const (
|
||||
UpgradeTypeArchive = iota
|
||||
UpgradeTypeFiles
|
||||
)
|
||||
|
||||
type ReqDataFwUpgrade struct {
|
||||
Type int
|
||||
Files []string
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Type int `json:"type"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Message string `json:"msg,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Error bool `json:"error"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Type int `json:"type"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type ReqDataNotify struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
type DFUProgress struct {
|
||||
Received int64 `mapstructure:"recvd"`
|
||||
Total int64 `mapstructure:"total"`
|
||||
Sent int64 `mapstructure:"sent"`
|
||||
}
|
||||
|
||||
type MotionValues struct {
|
||||
X int16
|
||||
Y int16
|
||||
Z int16
|
||||
}
|
31
itd.toml
@ -1,13 +1,32 @@
|
||||
# This is temporary, it is to show a notice
|
||||
# to people still using the old config
|
||||
cfg.version = 2
|
||||
[bluetooth]
|
||||
adapter = "hci0"
|
||||
|
||||
[socket]
|
||||
path = "/tmp/itd/socket"
|
||||
|
||||
[metrics]
|
||||
enabled = false
|
||||
|
||||
[metrics.heartRate]
|
||||
enabled = true
|
||||
|
||||
[metrics.stepCount]
|
||||
enabled = true
|
||||
|
||||
[metrics.battLevel]
|
||||
enabled = true
|
||||
|
||||
[metrics.motion]
|
||||
# This may lower the battery life of the PineTime
|
||||
enabled = false
|
||||
|
||||
[conn]
|
||||
reconnect = true
|
||||
|
||||
[conn.whitelist]
|
||||
enabled = false
|
||||
devices = []
|
||||
|
||||
[on.connect]
|
||||
notify = true
|
||||
|
||||
@ -26,3 +45,9 @@ cfg.version = 2
|
||||
[music]
|
||||
vol.interval = 5
|
||||
|
||||
[weather]
|
||||
enabled = true
|
||||
location = "Los Angeles, CA"
|
||||
|
||||
[logging]
|
||||
level = "info"
|
133
main.go
@ -19,50 +19,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gen2brain/dlgs"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
)
|
||||
|
||||
var firmwareUpdating = false
|
||||
var k = koanf.New(".")
|
||||
|
||||
//go:embed version.txt
|
||||
var version string
|
||||
|
||||
var (
|
||||
firmwareUpdating = false
|
||||
// The FS must be updated when the watch is reconnected
|
||||
updateFS = false
|
||||
)
|
||||
|
||||
func main() {
|
||||
if viper.GetInt("cfg.version") != 2 {
|
||||
log.Fatal().Msg("Please update your config to the newest format, only v2 configs supported.")
|
||||
showVer := flag.Bool("version", false, "Show version number and exit")
|
||||
flag.Parse()
|
||||
// If version requested, print and exit
|
||||
if *showVer {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
level, err := zerolog.ParseLevel(k.String("logging.level"))
|
||||
if err != nil || level == zerolog.NoLevel {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
|
||||
// Initialize infinitime library
|
||||
infinitime.Init(k.String("bluetooth.adapter"))
|
||||
// Cleanly exit after function
|
||||
defer infinitime.Exit()
|
||||
|
||||
// Create infinitime options struct
|
||||
opts := &infinitime.Options{
|
||||
AttemptReconnect: k.Bool("conn.reconnect"),
|
||||
WhitelistEnabled: k.Bool("conn.whitelist.enabled"),
|
||||
Whitelist: k.Strings("conn.whitelist.devices"),
|
||||
OnReqPasskey: onReqPasskey,
|
||||
Logger: log.Logger,
|
||||
LogLevel: level,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
go func() {
|
||||
<-sigCh
|
||||
cancel()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
signal.Notify(
|
||||
sigCh,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
)
|
||||
|
||||
// Connect to InfiniTime with default options
|
||||
dev, err := infinitime.Connect(&infinitime.Options{
|
||||
AttemptReconnect: viper.GetBool("conn.reconnect"),
|
||||
})
|
||||
dev, err := infinitime.Connect(ctx, opts)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error connecting to InfiniTime")
|
||||
log.Fatal().Err(err).Msg("Error connecting to InfiniTime")
|
||||
}
|
||||
|
||||
// When InfiniTime reconnects
|
||||
dev.OnReconnect(func() {
|
||||
if viper.GetBool("on.reconnect.setTime") {
|
||||
opts.OnReconnect = func() {
|
||||
if k.Bool("on.reconnect.setTime") {
|
||||
// Set time to current time
|
||||
err = dev.SetTime(time.Now())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error setting current time on connected InfiniTime")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If config specifies to notify on reconnect
|
||||
if viper.GetBool("on.reconnect.notify") {
|
||||
if k.Bool("on.reconnect.notify") {
|
||||
// Send notification to InfiniTime
|
||||
err = dev.Notify("itd", "Successfully reconnected")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error sending notification to InfiniTime")
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// FS must be updated on reconnect
|
||||
updateFS = true
|
||||
// Resend weather on reconnect
|
||||
sendWeatherCh <- struct{}{}
|
||||
}
|
||||
|
||||
// Get firmware version
|
||||
ver, err := dev.Version()
|
||||
@ -74,7 +134,7 @@ func main() {
|
||||
log.Info().Str("version", ver).Msg("Connected to InfiniTime")
|
||||
|
||||
// If config specifies to notify on connect
|
||||
if viper.GetBool("on.connect.notify") {
|
||||
if k.Bool("on.connect.notify") {
|
||||
// Send notification to InfiniTime
|
||||
err = dev.Notify("itd", "Successfully connected")
|
||||
if err != nil {
|
||||
@ -95,23 +155,56 @@ func main() {
|
||||
}
|
||||
|
||||
// Start control socket
|
||||
err = initCallNotifs(dev)
|
||||
err = initCallNotifs(ctx, dev)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error starting socket")
|
||||
log.Error().Err(err).Msg("Error initializing call notifications")
|
||||
}
|
||||
|
||||
// Initialize notification relay
|
||||
err = initNotifRelay(dev)
|
||||
err = initNotifRelay(ctx, dev)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error initializing notification relay")
|
||||
}
|
||||
|
||||
// Initializa weather
|
||||
err = initWeather(ctx, dev)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error initializing weather")
|
||||
}
|
||||
|
||||
// Initialize metrics collection
|
||||
err = initMetrics(ctx, dev)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error intializing metrics collection")
|
||||
}
|
||||
|
||||
// Start control socket
|
||||
err = startSocket(dev)
|
||||
err = startSocket(ctx, dev)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error starting socket")
|
||||
}
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
||||
|
||||
func onReqPasskey() (uint32, error) {
|
||||
var out uint32
|
||||
if isatty.IsTerminal(os.Stdin.Fd()) {
|
||||
fmt.Print("Passkey: ")
|
||||
_, err := fmt.Scanln(&out)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
passkey, ok, err := dlgs.Entry("Pairing", "Enter the passkey displayed on your watch.", "")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok {
|
||||
return 0, nil
|
||||
}
|
||||
passkeyInt, err := strconv.Atoi(passkey)
|
||||
return uint32(passkeyInt), err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
131
metrics.go
Normal file
@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.arsenm.dev/infinitime"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func initMetrics(ctx context.Context, dev *infinitime.Device) error {
|
||||
// If metrics disabled, return nil
|
||||
if !k.Bool("metrics.enabled") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open metrics database
|
||||
db, err := sql.Open("sqlite", filepath.Join(cfgDir, "metrics.db"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create heartRate table
|
||||
_, err = db.Exec("CREATE TABLE IF NOT EXISTS heartRate(time INT, bpm INT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create stepCount table
|
||||
_, err = db.Exec("CREATE TABLE IF NOT EXISTS stepCount(time INT, steps INT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create battLevel table
|
||||
_, err = db.Exec("CREATE TABLE IF NOT EXISTS battLevel(time INT, percent INT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create motion table
|
||||
_, err = db.Exec("CREATE TABLE IF NOT EXISTS motion(time INT, X INT, Y INT, Z INT);")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If heart rate metrics enabled in config
|
||||
if k.Bool("metrics.heartRate.enabled") {
|
||||
// Watch heart rate
|
||||
heartRateCh, err := dev.WatchHeartRate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// For every heart rate sample
|
||||
for heartRate := range heartRateCh {
|
||||
// Get current time
|
||||
unixTime := time.Now().UnixNano()
|
||||
// Insert sample and time into database
|
||||
db.Exec("INSERT INTO heartRate VALUES (?, ?);", unixTime, heartRate)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// If step count metrics enabled in config
|
||||
if k.Bool("metrics.stepCount.enabled") {
|
||||
// Watch step count
|
||||
stepCountCh, err := dev.WatchStepCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// For every step count sample
|
||||
for stepCount := range stepCountCh {
|
||||
// Get current time
|
||||
unixTime := time.Now().UnixNano()
|
||||
// Insert sample and time into database
|
||||
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, stepCount)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// If battery level metrics enabled in config
|
||||
if k.Bool("metrics.battLevel.enabled") {
|
||||
// Watch battery level
|
||||
battLevelCh, err := dev.WatchBatteryLevel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// For every battery level sample
|
||||
for battLevel := range battLevelCh {
|
||||
// Get current time
|
||||
unixTime := time.Now().UnixNano()
|
||||
// Insert sample and time into database
|
||||
db.Exec("INSERT INTO battLevel VALUES (?, ?);", unixTime, battLevel)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// If motion metrics enabled in config
|
||||
if k.Bool("metrics.motion.enabled") {
|
||||
// Watch motion values
|
||||
motionCh, err := dev.WatchMotion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// For every motion sample
|
||||
for motionVals := range motionCh {
|
||||
// Get current time
|
||||
unixTime := time.Now().UnixNano()
|
||||
// Insert sample values and time into database
|
||||
db.Exec(
|
||||
"INSERT INTO motion VALUES (?, ?, ?, ?);",
|
||||
unixTime,
|
||||
motionVals.X,
|
||||
motionVals.Y,
|
||||
motionVals.Z,
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
log.Info().Msg("Initialized metrics collection")
|
||||
|
||||
return nil
|
||||
}
|
55
music.go
@ -20,51 +20,32 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/infinitime/pkg/player"
|
||||
"go.arsenm.dev/itd/translit"
|
||||
)
|
||||
|
||||
func initMusicCtrl(dev *infinitime.Device) error {
|
||||
// On player status change, set status
|
||||
err := player.Status(func(newStatus bool) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetStatus(newStatus)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
player.Init()
|
||||
|
||||
// On player title change, set track
|
||||
err = player.Metadata("title", func(newTitle string) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetTrack(newTitle)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
maps := k.Strings("notifs.translit.use")
|
||||
translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom"))
|
||||
|
||||
// On player album change, set album
|
||||
err = player.Metadata("album", func(newAlbum string) {
|
||||
player.OnChange(func(ct player.ChangeType, val string) {
|
||||
newVal := translit.Transliterate(val, maps...)
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetAlbum(newAlbum)
|
||||
switch ct {
|
||||
case player.ChangeTypeStatus:
|
||||
dev.Music.SetStatus(val == "Playing")
|
||||
case player.ChangeTypeTitle:
|
||||
dev.Music.SetTrack(newVal)
|
||||
case player.ChangeTypeAlbum:
|
||||
dev.Music.SetAlbum(newVal)
|
||||
case player.ChangeTypeArtist:
|
||||
dev.Music.SetArtist(newVal)
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// On player artist change, set artist
|
||||
err = player.Metadata("artist", func(newArtist string) {
|
||||
if !firmwareUpdating {
|
||||
dev.Music.SetArtist(newArtist)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for music events
|
||||
musicEvtCh, err := dev.Music.WatchEvents()
|
||||
@ -85,9 +66,9 @@ func initMusicCtrl(dev *infinitime.Device) error {
|
||||
case infinitime.MusicEventPrev:
|
||||
player.Prev()
|
||||
case infinitime.MusicEventVolUp:
|
||||
player.VolUp(viper.GetUint("music.vol.interval"))
|
||||
player.VolUp(uint(k.Int("music.vol.interval")))
|
||||
case infinitime.MusicEventVolDown:
|
||||
player.VolDown(viper.GetUint("music.vol.interval"))
|
||||
player.VolDown(uint(k.Int("music.vol.interval")))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
20
notifs.go
@ -19,18 +19,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/itd/translit"
|
||||
)
|
||||
|
||||
func initNotifRelay(dev *infinitime.Device) error {
|
||||
func initNotifRelay(ctx context.Context, dev *infinitime.Device) error {
|
||||
// Connect to dbus session bus
|
||||
bus, err := dbus.SessionBus()
|
||||
bus, err := newSessionBusConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -41,7 +41,9 @@ func initNotifRelay(dev *infinitime.Device) error {
|
||||
}
|
||||
var flag uint = 0
|
||||
// Becode monitor for notifications
|
||||
call := bus.BusObject().Call("org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag)
|
||||
call := bus.BusObject().CallWithContext(
|
||||
ctx, "org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag,
|
||||
)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@ -72,8 +74,8 @@ func initNotifRelay(dev *infinitime.Device) error {
|
||||
continue
|
||||
}
|
||||
|
||||
maps := viper.GetStringSlice("notifs.translit.use")
|
||||
translit.Transliterators["custom"] = translit.Map(viper.GetStringSlice("notifs.translit.custom"))
|
||||
maps := k.Strings("notifs.translit.use")
|
||||
translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom"))
|
||||
sender = translit.Transliterate(sender, maps...)
|
||||
summary = translit.Transliterate(summary, maps...)
|
||||
body = translit.Transliterate(body, maps...)
|
||||
@ -97,9 +99,9 @@ func initNotifRelay(dev *infinitime.Device) error {
|
||||
|
||||
// ignored checks whether any fields were ignored in the config
|
||||
func ignored(sender, summary, body string) bool {
|
||||
ignoreSender := viper.GetStringSlice("notifs.ignore.sender")
|
||||
ignoreSummary := viper.GetStringSlice("notifs.ignore.summary")
|
||||
ignoreBody := viper.GetStringSlice("notifs.ignore.body")
|
||||
ignoreSender := k.Strings("notifs.ignore.sender")
|
||||
ignoreSummary := k.Strings("notifs.ignore.summary")
|
||||
ignoreBody := k.Strings("notifs.ignore.body")
|
||||
return strSlcContains(ignoreSender, sender) ||
|
||||
strSlcContains(ignoreSummary, summary) ||
|
||||
strSlcContains(ignoreBody, body)
|
||||
|
816
socket.go
@ -19,448 +19,470 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
"go.arsenm.dev/itd/translit"
|
||||
"go.arsenm.dev/infinitime/blefs"
|
||||
"go.arsenm.dev/itd/api"
|
||||
"go.arsenm.dev/lrpc/codec"
|
||||
"go.arsenm.dev/lrpc/server"
|
||||
)
|
||||
|
||||
type DoneMap map[string]chan struct{}
|
||||
var (
|
||||
ErrDFUInvalidFile = errors.New("provided file is invalid for given upgrade type")
|
||||
ErrDFUNotEnoughFiles = errors.New("not enough files provided for given upgrade type")
|
||||
ErrDFUInvalidUpgType = errors.New("invalid upgrade type")
|
||||
)
|
||||
|
||||
func (dm DoneMap) Exists(key string) bool {
|
||||
_, ok := dm[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (dm DoneMap) Done(key string) {
|
||||
ch := dm[key]
|
||||
ch <- struct{}{}
|
||||
}
|
||||
|
||||
func (dm DoneMap) Create(key string) {
|
||||
dm[key] = make(chan struct{}, 1)
|
||||
}
|
||||
|
||||
func (dm DoneMap) Remove(key string) {
|
||||
close(dm[key])
|
||||
delete(dm, key)
|
||||
}
|
||||
|
||||
var done = DoneMap{}
|
||||
|
||||
func startSocket(dev *infinitime.Device) error {
|
||||
func startSocket(ctx context.Context, dev *infinitime.Device) error {
|
||||
// Make socket directory if non-existant
|
||||
err := os.MkdirAll(filepath.Dir(viper.GetString("socket.path")), 0755)
|
||||
err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove old socket if it exists
|
||||
err = os.RemoveAll(viper.GetString("socket.path"))
|
||||
err = os.RemoveAll(k.String("socket.path"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Listen on socket path
|
||||
ln, err := net.Listen("unix", viper.GetString("socket.path"))
|
||||
ln, err := net.Listen("unix", k.String("socket.path"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs, err := dev.FS()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error getting BLE filesystem")
|
||||
}
|
||||
|
||||
srv := server.New()
|
||||
|
||||
itdAPI := &ITD{
|
||||
dev: dev,
|
||||
}
|
||||
err = srv.Register(itdAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fsAPI := &FS{
|
||||
dev: dev,
|
||||
fs: fs,
|
||||
}
|
||||
err = srv.Register(fsAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go srv.Serve(ctx, ln, codec.Default)
|
||||
|
||||
// Log socket start
|
||||
log.Info().Str("path", k.String("socket.path")).Msg("Started control socket")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ITD struct {
|
||||
dev *infinitime.Device
|
||||
}
|
||||
|
||||
func (i *ITD) HeartRate(_ *server.Context) (uint8, error) {
|
||||
return i.dev.HeartRate()
|
||||
}
|
||||
|
||||
func (i *ITD) WatchHeartRate(ctx *server.Context) error {
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
heartRateCh, err := i.dev.WatchHeartRate(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// Accept socket connection
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error accepting connection")
|
||||
}
|
||||
|
||||
// Concurrently handle connection
|
||||
go handleConnection(conn, dev)
|
||||
// For every heart rate value
|
||||
for heartRate := range heartRateCh {
|
||||
ch <- heartRate
|
||||
}
|
||||
}()
|
||||
|
||||
// Log socket start
|
||||
log.Info().Str("path", viper.GetString("socket.path")).Msg("Started control socket")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn, dev *infinitime.Device) {
|
||||
defer conn.Close()
|
||||
func (i *ITD) BatteryLevel(_ *server.Context) (uint8, error) {
|
||||
return i.dev.BatteryLevel()
|
||||
}
|
||||
|
||||
// Create new scanner on connection
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
var req types.Request
|
||||
// Decode scanned message into types.Request
|
||||
err := json.Unmarshal(scanner.Bytes(), &req)
|
||||
func (i *ITD) WatchBatteryLevel(ctx *server.Context) error {
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
battLevelCh, err := i.dev.WatchBatteryLevel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
// For every heart rate value
|
||||
for battLevel := range battLevelCh {
|
||||
ch <- battLevel
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ITD) Motion(_ *server.Context) (infinitime.MotionValues, error) {
|
||||
return i.dev.Motion()
|
||||
}
|
||||
|
||||
func (i *ITD) WatchMotion(ctx *server.Context) error {
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
motionValsCh, err := i.dev.WatchMotion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
// For every heart rate value
|
||||
for motionVals := range motionValsCh {
|
||||
ch <- motionVals
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ITD) StepCount(_ *server.Context) (uint32, error) {
|
||||
return i.dev.StepCount()
|
||||
}
|
||||
|
||||
func (i *ITD) WatchStepCount(ctx *server.Context) error {
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stepCountCh, err := i.dev.WatchStepCount(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
// For every heart rate value
|
||||
for stepCount := range stepCountCh {
|
||||
ch <- stepCount
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ITD) Version(_ *server.Context) (string, error) {
|
||||
return i.dev.Version()
|
||||
}
|
||||
|
||||
func (i *ITD) Address(_ *server.Context) string {
|
||||
return i.dev.Address()
|
||||
}
|
||||
|
||||
func (i *ITD) Notify(_ *server.Context, data api.NotifyData) error {
|
||||
return i.dev.Notify(data.Title, data.Body)
|
||||
}
|
||||
|
||||
func (i *ITD) SetTime(_ *server.Context, t *time.Time) error {
|
||||
return i.dev.SetTime(*t)
|
||||
}
|
||||
|
||||
func (i *ITD) WeatherUpdate(_ *server.Context) {
|
||||
sendWeatherCh <- struct{}{}
|
||||
}
|
||||
|
||||
func (i *ITD) FirmwareUpgrade(ctx *server.Context, reqData api.FwUpgradeData) error {
|
||||
i.dev.DFU.Reset()
|
||||
|
||||
switch reqData.Type {
|
||||
case api.UpgradeTypeArchive:
|
||||
// If less than one file, return error
|
||||
if len(reqData.Files) < 1 {
|
||||
return ErrDFUNotEnoughFiles
|
||||
}
|
||||
// If file is not zip archive, return error
|
||||
if filepath.Ext(reqData.Files[0]) != ".zip" {
|
||||
return ErrDFUInvalidFile
|
||||
}
|
||||
// Load DFU archive
|
||||
err := i.dev.DFU.LoadArchive(reqData.Files[0])
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error decoding JSON input")
|
||||
continue
|
||||
return err
|
||||
}
|
||||
case api.UpgradeTypeFiles:
|
||||
// If less than two files, return error
|
||||
if len(reqData.Files) < 2 {
|
||||
return ErrDFUNotEnoughFiles
|
||||
}
|
||||
// If first file is not init packet, return error
|
||||
if filepath.Ext(reqData.Files[0]) != ".dat" {
|
||||
return ErrDFUInvalidFile
|
||||
}
|
||||
// If second file is not firmware image, return error
|
||||
if filepath.Ext(reqData.Files[1]) != ".bin" {
|
||||
return ErrDFUInvalidFile
|
||||
}
|
||||
// Load individual DFU files
|
||||
err := i.dev.DFU.LoadFiles(reqData.Files[0], reqData.Files[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return ErrDFUInvalidUpgType
|
||||
}
|
||||
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
// For every progress event
|
||||
for event := range i.dev.DFU.Progress() {
|
||||
ch <- event
|
||||
}
|
||||
|
||||
// If firmware is updating, return error
|
||||
if firmwareUpdating {
|
||||
connErr(conn, req.Type, nil, "Firmware update in progress")
|
||||
firmwareUpdating = false
|
||||
// Send zero object to signal completion
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// Set firmwareUpdating
|
||||
firmwareUpdating = true
|
||||
|
||||
go func() {
|
||||
// Start DFU
|
||||
err := i.dev.DFU.Start()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error while upgrading firmware")
|
||||
firmwareUpdating = false
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
switch req.Type {
|
||||
case types.ReqTypeHeartRate:
|
||||
// Get heart rate from watch
|
||||
heartRate, err := dev.HeartRate()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting heart rate")
|
||||
break
|
||||
}
|
||||
// Encode heart rate to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: heartRate,
|
||||
})
|
||||
case types.ReqTypeWatchHeartRate:
|
||||
heartRateCh, cancel, err := dev.WatchHeartRate()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting heart rate channel")
|
||||
break
|
||||
}
|
||||
reqID := uuid.New().String()
|
||||
go func() {
|
||||
done.Create(reqID)
|
||||
// For every heart rate value
|
||||
for heartRate := range heartRateCh {
|
||||
select {
|
||||
case <-done[reqID]:
|
||||
// Stop notifications if done signal received
|
||||
cancel()
|
||||
done.Remove(reqID)
|
||||
return
|
||||
default:
|
||||
// Encode response to connection if no done signal received
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
ID: reqID,
|
||||
Value: heartRate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
case types.ReqTypeBattLevel:
|
||||
// Get battery level from watch
|
||||
battLevel, err := dev.BatteryLevel()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting battery level")
|
||||
break
|
||||
}
|
||||
// Encode battery level to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: battLevel,
|
||||
})
|
||||
case types.ReqTypeWatchBattLevel:
|
||||
battLevelCh, cancel, err := dev.WatchBatteryLevel()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting battery level channel")
|
||||
break
|
||||
}
|
||||
reqID := uuid.New().String()
|
||||
go func() {
|
||||
done.Create(reqID)
|
||||
// For every battery level value
|
||||
for battLevel := range battLevelCh {
|
||||
select {
|
||||
case <-done[reqID]:
|
||||
// Stop notifications if done signal received
|
||||
cancel()
|
||||
done.Remove(reqID)
|
||||
return
|
||||
default:
|
||||
// Encode response to connection if no done signal received
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
ID: reqID,
|
||||
Value: battLevel,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
case types.ReqTypeMotion:
|
||||
// Get battery level from watch
|
||||
motionVals, err := dev.Motion()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting motion values")
|
||||
break
|
||||
}
|
||||
// Encode battery level to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: motionVals,
|
||||
})
|
||||
case types.ReqTypeWatchMotion:
|
||||
motionValCh, cancel, err := dev.WatchMotion()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting heart rate channel")
|
||||
break
|
||||
}
|
||||
reqID := uuid.New().String()
|
||||
go func() {
|
||||
done.Create(reqID)
|
||||
// For every motion event
|
||||
for motionVals := range motionValCh {
|
||||
select {
|
||||
case <-done[reqID]:
|
||||
// Stop notifications if done signal received
|
||||
cancel()
|
||||
done.Remove(reqID)
|
||||
return nil
|
||||
}
|
||||
|
||||
return
|
||||
default:
|
||||
// Encode response to connection if no done signal received
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
ID: reqID,
|
||||
Value: motionVals,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
case types.ReqTypeStepCount:
|
||||
// Get battery level from watch
|
||||
stepCount, err := dev.StepCount()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting step count")
|
||||
break
|
||||
}
|
||||
// Encode battery level to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: stepCount,
|
||||
})
|
||||
case types.ReqTypeWatchStepCount:
|
||||
stepCountCh, cancel, err := dev.WatchStepCount()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting heart rate channel")
|
||||
break
|
||||
}
|
||||
reqID := uuid.New().String()
|
||||
go func() {
|
||||
done.Create(reqID)
|
||||
// For every step count value
|
||||
for stepCount := range stepCountCh {
|
||||
select {
|
||||
case <-done[reqID]:
|
||||
// Stop notifications if done signal received
|
||||
cancel()
|
||||
done.Remove(reqID)
|
||||
return
|
||||
default:
|
||||
// Encode response to connection if no done signal received
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
ID: reqID,
|
||||
Value: stepCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
case types.ReqTypeFwVersion:
|
||||
// Get firmware version from watch
|
||||
version, err := dev.Version()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error getting firmware version")
|
||||
break
|
||||
}
|
||||
// Encode version to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: version,
|
||||
})
|
||||
case types.ReqTypeBtAddress:
|
||||
// Encode bluetooth address to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: dev.Address(),
|
||||
})
|
||||
case types.ReqTypeNotify:
|
||||
// If no data, return error
|
||||
if req.Data == nil {
|
||||
connErr(conn, req.Type, nil, "Data required for notify request")
|
||||
break
|
||||
}
|
||||
var reqData types.ReqDataNotify
|
||||
// Decode data map to notify request data
|
||||
err = mapstructure.Decode(req.Data, &reqData)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error decoding request data")
|
||||
break
|
||||
}
|
||||
maps := viper.GetStringSlice("notifs.translit.use")
|
||||
translit.Transliterators["custom"] = translit.Map(viper.GetStringSlice("notifs.translit.custom"))
|
||||
title := translit.Transliterate(reqData.Title, maps...)
|
||||
body := translit.Transliterate(reqData.Body, maps...)
|
||||
// Send notification to watch
|
||||
err = dev.Notify(title, body)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error sending notification")
|
||||
break
|
||||
}
|
||||
// Encode empty types.Response to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
case types.ReqTypeSetTime:
|
||||
// If no data, return error
|
||||
if req.Data == nil {
|
||||
connErr(conn, req.Type, nil, "Data required for settime request")
|
||||
break
|
||||
}
|
||||
// Get string from data or return error
|
||||
reqTimeStr, ok := req.Data.(string)
|
||||
if !ok {
|
||||
connErr(conn, req.Type, nil, "Data for settime request must be RFC3339 formatted time string")
|
||||
break
|
||||
}
|
||||
type FS struct {
|
||||
dev *infinitime.Device
|
||||
fs *blefs.FS
|
||||
}
|
||||
|
||||
var reqTime time.Time
|
||||
if reqTimeStr == "now" {
|
||||
reqTime = time.Now()
|
||||
} else {
|
||||
// Parse time as RFC3339/ISO8601
|
||||
reqTime, err = time.Parse(time.RFC3339, reqTimeStr)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Invalid time format. Time string must be formatted as ISO8601 or the word `now`")
|
||||
break
|
||||
}
|
||||
}
|
||||
// Set time on watch
|
||||
err = dev.SetTime(reqTime)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error setting device time")
|
||||
break
|
||||
}
|
||||
// Encode empty types.Response to connection
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
case types.ReqTypeFwUpgrade:
|
||||
// If no data, return error
|
||||
if req.Data == nil {
|
||||
connErr(conn, req.Type, nil, "Data required for firmware upgrade request")
|
||||
break
|
||||
}
|
||||
var reqData types.ReqDataFwUpgrade
|
||||
// Decode data map to firmware upgrade request data
|
||||
err = mapstructure.Decode(req.Data, &reqData)
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error decoding request data")
|
||||
break
|
||||
}
|
||||
// Reset DFU to prepare for next update
|
||||
dev.DFU.Reset()
|
||||
switch reqData.Type {
|
||||
case types.UpgradeTypeArchive:
|
||||
// If less than one file, return error
|
||||
if len(reqData.Files) < 1 {
|
||||
connErr(conn, req.Type, nil, "Archive upgrade requires one file with .zip extension")
|
||||
break
|
||||
}
|
||||
// If file is not zip archive, return error
|
||||
if filepath.Ext(reqData.Files[0]) != ".zip" {
|
||||
connErr(conn, req.Type, nil, "Archive upgrade file must be a zip archive")
|
||||
break
|
||||
}
|
||||
// Load DFU archive
|
||||
err := dev.DFU.LoadArchive(reqData.Files[0])
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error loading archive file")
|
||||
break
|
||||
}
|
||||
case types.UpgradeTypeFiles:
|
||||
// If less than two files, return error
|
||||
if len(reqData.Files) < 2 {
|
||||
connErr(conn, req.Type, nil, "Files upgrade requires two files. First with .dat and second with .bin extension.")
|
||||
break
|
||||
}
|
||||
// If first file is not init packet, return error
|
||||
if filepath.Ext(reqData.Files[0]) != ".dat" {
|
||||
connErr(conn, req.Type, nil, "First file must be a .dat file")
|
||||
break
|
||||
}
|
||||
// If second file is not firmware image, return error
|
||||
if filepath.Ext(reqData.Files[1]) != ".bin" {
|
||||
connErr(conn, req.Type, nil, "Second file must be a .bin file")
|
||||
break
|
||||
}
|
||||
// Load individual DFU files
|
||||
err := dev.DFU.LoadFiles(reqData.Files[0], reqData.Files[1])
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error loading firmware files")
|
||||
break
|
||||
}
|
||||
}
|
||||
func (fs *FS) RemoveAll(_ *server.Context, paths []string) error {
|
||||
fs.updateFS()
|
||||
for _, path := range paths {
|
||||
err := fs.fs.RemoveAll(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Get progress
|
||||
progress := dev.DFU.Progress()
|
||||
// For every progress event
|
||||
for event := range progress {
|
||||
// Encode event on connection
|
||||
json.NewEncoder(conn).Encode(types.Response{
|
||||
Type: req.Type,
|
||||
Value: event,
|
||||
})
|
||||
}
|
||||
firmwareUpdating = false
|
||||
}()
|
||||
func (fs *FS) Remove(_ *server.Context, paths []string) error {
|
||||
fs.updateFS()
|
||||
for _, path := range paths {
|
||||
err := fs.fs.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set firmwareUpdating
|
||||
firmwareUpdating = true
|
||||
// Start DFU
|
||||
err = dev.DFU.Start()
|
||||
if err != nil {
|
||||
connErr(conn, req.Type, err, "Error performing upgrade")
|
||||
firmwareUpdating = false
|
||||
break
|
||||
func (fs *FS) Rename(_ *server.Context, paths [2]string) error {
|
||||
fs.updateFS()
|
||||
return fs.fs.Rename(paths[0], paths[1])
|
||||
}
|
||||
|
||||
func (fs *FS) MkdirAll(_ *server.Context, paths []string) error {
|
||||
fs.updateFS()
|
||||
for _, path := range paths {
|
||||
err := fs.fs.MkdirAll(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FS) Mkdir(_ *server.Context, paths []string) error {
|
||||
fs.updateFS()
|
||||
for _, path := range paths {
|
||||
err := fs.fs.Mkdir(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FS) ReadDir(_ *server.Context, dir string) ([]api.FileInfo, error) {
|
||||
fs.updateFS()
|
||||
|
||||
entries, err := fs.fs.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var fileInfo []api.FileInfo
|
||||
for _, entry := range entries {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileInfo = append(fileInfo, api.FileInfo{
|
||||
Name: info.Name(),
|
||||
Size: info.Size(),
|
||||
IsDir: info.IsDir(),
|
||||
})
|
||||
}
|
||||
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
func (fs *FS) Upload(ctx *server.Context, paths [2]string) error {
|
||||
fs.updateFS()
|
||||
|
||||
localFile, err := os.Open(paths[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localInfo, err := localFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remoteFile, err := fs.fs.Create(paths[0], uint32(localInfo.Size()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// For every progress event
|
||||
for sent := range remoteFile.Progress() {
|
||||
ch <- api.FSTransferProgress{
|
||||
Total: remoteFile.Size(),
|
||||
Sent: sent,
|
||||
}
|
||||
firmwareUpdating = false
|
||||
case types.ReqTypeCancel:
|
||||
if req.Data == nil {
|
||||
connErr(conn, req.Type, nil, "No data provided. Cancel request requires request ID string as data.")
|
||||
continue
|
||||
}
|
||||
|
||||
// Send zero object to signal completion
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
io.Copy(remoteFile, localFile)
|
||||
localFile.Close()
|
||||
remoteFile.Close()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FS) Download(ctx *server.Context, paths [2]string) error {
|
||||
fs.updateFS()
|
||||
|
||||
localFile, err := os.Create(paths[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remoteFile, err := fs.fs.Open(paths[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// For every progress event
|
||||
for sent := range remoteFile.Progress() {
|
||||
ch <- api.FSTransferProgress{
|
||||
Total: remoteFile.Size(),
|
||||
Sent: sent,
|
||||
}
|
||||
reqID, ok := req.Data.(string)
|
||||
if !ok {
|
||||
connErr(conn, req.Type, nil, "Invalid data. Cancel request required request ID string as data.")
|
||||
}
|
||||
// Stop notifications
|
||||
done.Done(reqID)
|
||||
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
|
||||
default:
|
||||
connErr(conn, req.Type, nil, fmt.Sprintf("Unknown request type %d", req.Type))
|
||||
}
|
||||
|
||||
// Send zero object to signal completion
|
||||
close(ch)
|
||||
localFile.Close()
|
||||
remoteFile.Close()
|
||||
}()
|
||||
|
||||
go io.Copy(localFile, remoteFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FS) LoadResources(ctx *server.Context, path string) error {
|
||||
resFl, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progCh, err := infinitime.LoadResources(resFl, fs.fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := ctx.MakeChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for evt := range progCh {
|
||||
ch <- evt
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FS) updateFS() {
|
||||
if fs.fs == nil || updateFS {
|
||||
// Get new FS
|
||||
newFS, err := fs.dev.FS()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error updating BLE filesystem")
|
||||
} else {
|
||||
// Set FS pointer to new FS
|
||||
fs.fs = newFS
|
||||
// Reset updateFS
|
||||
updateFS = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func connErr(conn net.Conn, resType int, err error, msg string) {
|
||||
var res types.Response
|
||||
// If error exists, add to types.Response, otherwise don't
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(msg)
|
||||
res = types.Response{Message: fmt.Sprintf("%s: %s", msg, err)}
|
||||
} else {
|
||||
log.Error().Msg(msg)
|
||||
res = types.Response{Message: msg, Type: resType}
|
||||
}
|
||||
res.Error = true
|
||||
|
||||
// Encode error to connection
|
||||
json.NewEncoder(conn).Encode(res)
|
||||
}
|
||||
|
@ -301,7 +301,7 @@ var Transliterators = map[string]Transliterator{
|
||||
"Ð", "D",
|
||||
"ð", "d",
|
||||
},
|
||||
"Czeck": Map{
|
||||
"Czech": Map{
|
||||
"ř", "r",
|
||||
"ě", "e",
|
||||
"ý", "y",
|
||||
@ -327,11 +327,40 @@ var Transliterators = map[string]Transliterator{
|
||||
"ÿ", "y",
|
||||
"ç", "c",
|
||||
},
|
||||
"Romanian": Map{
|
||||
"ă", "a",
|
||||
"Ă", "A",
|
||||
"â", "a",
|
||||
"Â", "A",
|
||||
"î", "i",
|
||||
"Î", "I",
|
||||
"ș", "s",
|
||||
"Ș", "S",
|
||||
"ț", "t",
|
||||
"Ț", "T",
|
||||
"ş", "s",
|
||||
"Ş", "S",
|
||||
"ţ", "t",
|
||||
"Ţ", "T",
|
||||
"„", "\"",
|
||||
"”", "\"",
|
||||
},
|
||||
"Emoji": Map{
|
||||
"😂", ":')",
|
||||
"😂", "XD",
|
||||
"🤣", "XD",
|
||||
"😊", ":)",
|
||||
"😃", ":)",
|
||||
"☺️", ":)",
|
||||
"😌", ":)",
|
||||
"😃", ":D",
|
||||
"😁", ":D",
|
||||
"😋", ":P",
|
||||
"😛", ":P",
|
||||
"😜", ";P",
|
||||
"🙃", "(:",
|
||||
"😎", "8)",
|
||||
"😶", ":#",
|
||||
"😩", "-_-",
|
||||
"😕", ":(",
|
||||
"😏", ":‑J",
|
||||
"💜", "<3",
|
||||
"💖", "<3",
|
||||
@ -343,12 +372,37 @@ var Transliterators = map[string]Transliterator{
|
||||
"💓", "<3",
|
||||
"💚", "<3",
|
||||
"💙", "<3",
|
||||
"💟", "<3",
|
||||
"❣️", "<3!",
|
||||
"💔", "</3",
|
||||
"😱", "D:",
|
||||
"😮", ":O",
|
||||
"😝", ":P",
|
||||
"😍", ":x",
|
||||
"😢", ":(",
|
||||
"😯", ":O",
|
||||
"😝", "xP",
|
||||
"🤔", "',:-|",
|
||||
"😔", ":|",
|
||||
"😍", ":*",
|
||||
"😘", ":*",
|
||||
"😚", ":*",
|
||||
"😙", ":*",
|
||||
"👍", ":thumbsup:",
|
||||
"👌", ":ok_hand:",
|
||||
"🤞", ":crossed_fingers:",
|
||||
"✌️", ":victory_hand:",
|
||||
"🌄", ":sunrise_over_mountains:",
|
||||
"🌞", ":sun_with_face:",
|
||||
"🤗", ":hugging_face:",
|
||||
"🌻", ":sunflower:",
|
||||
"🥱", ":yawning_face:",
|
||||
"🙄", ":face_with_rolling_eyes:",
|
||||
"🔫", ":gun:",
|
||||
"🥔", ":potato:",
|
||||
"😬", ":E",
|
||||
"✨", "***",
|
||||
"🌌", "***",
|
||||
"💀", "8-X",
|
||||
"😅", "':D",
|
||||
"😢", ":'(",
|
||||
"💯", ":100:",
|
||||
"🔥", ":fire:",
|
||||
"😉", ";)",
|
||||
|
283
weather.go
Normal file
@ -0,0 +1,283 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/infinitime/weather"
|
||||
)
|
||||
|
||||
// METResponse represents a response from
|
||||
// the MET Norway API
|
||||
type METResponse struct {
|
||||
Properties struct {
|
||||
Timeseries []struct {
|
||||
Time time.Time
|
||||
Data METData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// METData represents data in a METResponse
|
||||
type METData struct {
|
||||
Instant struct {
|
||||
Details struct {
|
||||
AirPressure float32 `json:"air_pressure_at_sea_level"`
|
||||
AirTemperature float32 `json:"air_temperature"`
|
||||
DewPoint float32 `json:"dew_point_temperature"`
|
||||
CloudAreaFraction float32 `json:"cloud_area_fraction"`
|
||||
FogAreaFraction float32 `json:"fog_area_fraction"`
|
||||
RelativeHumidity float32 `json:"relative_humidity"`
|
||||
UVIndex float32 `json:"ultraviolet_index_clear_sky"`
|
||||
WindDirection float32 `json:"wind_from_direction"`
|
||||
WindSpeed float32 `json:"wind_speed"`
|
||||
}
|
||||
}
|
||||
NextHour struct {
|
||||
Summary struct {
|
||||
SymbolCode string `json:"symbol_code"`
|
||||
}
|
||||
Details struct {
|
||||
PrecipitationAmount float32 `json:"precipitation_amount"`
|
||||
}
|
||||
} `json:"next_1_hours"`
|
||||
}
|
||||
|
||||
// OSMData represents lat/long data from
|
||||
// OpenStreetMap Nominatim
|
||||
type OSMData []struct {
|
||||
Lat string `json:"lat"`
|
||||
Lon string `json:"lon"`
|
||||
}
|
||||
|
||||
var sendWeatherCh = make(chan struct{}, 1)
|
||||
|
||||
func initWeather(ctx context.Context, dev *infinitime.Device) error {
|
||||
if !k.Bool("weather.enabled") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get location based on string in config
|
||||
lat, lon, err := getLocation(ctx, k.String("weather.location"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timer := time.NewTimer(time.Hour)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// Attempt to get weather
|
||||
data, err := getWeather(ctx, lat, lon)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Error getting weather data")
|
||||
// Wait 15 minutes before retrying
|
||||
time.Sleep(15 * time.Minute)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get current data
|
||||
current := data.Properties.Timeseries[0]
|
||||
currentData := current.Data.Instant.Details
|
||||
|
||||
// Add temperature event
|
||||
err = dev.AddWeatherEvent(weather.TemperatureEvent{
|
||||
TimelineHeader: weather.NewHeader(
|
||||
weather.EventTypeTemperature,
|
||||
time.Hour,
|
||||
),
|
||||
Temperature: int16(round(currentData.AirTemperature * 100)),
|
||||
DewPoint: int16(round(currentData.DewPoint)),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error adding temperature event")
|
||||
}
|
||||
|
||||
// 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().Err(err).Msg("Error adding precipitation event")
|
||||
}
|
||||
|
||||
// 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().Err(err).Msg("Error adding wind event")
|
||||
}
|
||||
|
||||
// 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().Err(err).Msg("Error adding clouds event")
|
||||
}
|
||||
|
||||
// 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().Err(err).Msg("Error adding humidity event")
|
||||
}
|
||||
|
||||
// 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().Err(err).Msg("Error adding pressure event")
|
||||
}
|
||||
|
||||
// Reset timer to 1 hour
|
||||
timer.Stop()
|
||||
timer.Reset(time.Hour)
|
||||
|
||||
// Wait for timer to fire or manual update signal
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-sendWeatherCh:
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLocation returns the latitude and longitude
|
||||
// given a location
|
||||
func getLocation(ctx context.Context, loc string) (lat, lon float64, err error) {
|
||||
// Create request URL and perform GET request
|
||||
reqURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search.php?q=%s&format=jsonv2", url.QueryEscape(loc))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Decode JSON from response into OSMData
|
||||
data := OSMData{}
|
||||
err = json.NewDecoder(res.Body).Decode(&data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// If no data points
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get first data point
|
||||
out := data[0]
|
||||
|
||||
// Attempt to parse latitude
|
||||
lat, err = strconv.ParseFloat(out.Lat, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Attempt to parse longitude
|
||||
lon, err = strconv.ParseFloat(out.Lon, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getWeather gets weather data given a latitude and longitude
|
||||
func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
|
||||
// Create new GET request
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodGet,
|
||||
fmt.Sprintf(
|
||||
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f",
|
||||
lat,
|
||||
lon,
|
||||
),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set identifying user agent as per NMI requirements
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("ITD/%s gitea.arsenm.dev/Arsen6331/itd", version))
|
||||
|
||||
// Perform request
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode JSON from response to METResponse struct
|
||||
out := &METResponse{}
|
||||
err = json.NewDecoder(res.Body).Decode(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseSymbol determines what type of precipitation a symbol code
|
||||
// codes for.
|
||||
func parseSymbol(symCode string) weather.PrecipitationType {
|
||||
switch {
|
||||
case strings.Contains(symCode, "lightrain"):
|
||||
return weather.PrecipitationTypeRain
|
||||
case strings.Contains(symCode, "rain"):
|
||||
return weather.PrecipitationTypeRain
|
||||
case strings.Contains(symCode, "snow"):
|
||||
return weather.PrecipitationTypeSnow
|
||||
case strings.Contains(symCode, "sleet"):
|
||||
return weather.PrecipitationTypeSleet
|
||||
case strings.Contains(symCode, "snow"):
|
||||
return weather.PrecipitationTypeSnow
|
||||
default:
|
||||
return weather.PrecipitationTypeNone
|
||||
}
|
||||
}
|
||||
|
||||
// round rounds 32-bit floats to 32-bit integers
|
||||
func round(f float32) int32 {
|
||||
return int32(math.Round(float64(f)))
|
||||
}
|