Compare commits

...

130 Commits

Author SHA1 Message Date
03f3968fe1 Update infinitime library 2022-10-20 01:42:23 -07:00
dea92c6404 Update infinitime library 2022-10-17 12:50:51 -07:00
006f245c10 Add warning if current InfiniTime doesn't support BLE FS (#29) 2022-10-17 12:40:51 -07:00
d232340edd Update infinitime library 2022-10-17 12:24:17 -07:00
c6458720e9 Handle error events in itctl res load command (#29) 2022-10-17 12:23:06 -07:00
1e072a3540 Merge pull request 'Add resource loading to ITD' (#28) from resource-loading into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/28
2022-10-16 20:17:49 +00:00
f639fef992 Add resource loading as part of DFU 2022-10-16 13:17:12 -07:00
2d0db1dcf1 Close channel once resource uploading complete 2022-10-16 12:42:59 -07:00
4efa4380c4 Add -r for rm and -p for mkdir 2022-09-03 16:28:25 -07:00
fca64afbf3 Remove example comments from goreleaser config 2022-09-01 15:09:49 -07:00
cf24c5ace8 Fix file extension of Alpine example 2022-09-01 15:02:11 -07:00
a25b2e3e62 Add --allow-untrusted to Alpine example 2022-09-01 15:00:03 -07:00
2d0b64d92f Add installation instructions for major distros 2022-09-01 14:58:33 -07:00
5efafe9be7 Remove download binary badge and add new AUR badge 2022-09-01 14:33:23 -07:00
271510d528 Allow automatic release to the AUR 2022-09-01 11:50:41 -07:00
4d72a063b2 Update CI badge 2022-09-01 03:14:05 -07:00
643245f16c Add GoReleaser config 2022-09-01 01:52:46 -07:00
6f87980d4b Add resource loading to itctl 2022-08-30 13:01:36 -07:00
851f1975d6 Add LoadResources() to API 2022-08-30 12:13:22 -07:00
5e66fe82ac Improve itgui compilation documentation (Fixes #24) 2022-08-29 13:28:52 -07:00
645541e079 Update infinitime library and bluetooth library (Fixes #22) 2022-08-19 14:05:30 -07:00
1012be6e5b Revert #22 bandaid fix 2022-08-19 14:04:08 -07:00
5973290d6c Temporary bandaid fix for #22 2022-08-14 00:05:56 -07:00
19bacf29b2 Fix comment above goroutine code 2022-07-31 02:40:46 -07:00
a78650e526 Fix bug where itctl doesn't exit on SIGINT/SIGTERM 2022-07-31 02:22:33 -07:00
71e9caf0bc Fix bug where help command doesn't show flags/subcommands 2022-07-31 02:15:42 -07:00
1f5a6365bc Remove GOFLAGS from Makefile as the go tool already looks at that variable 2022-07-22 10:36:19 -07:00
958f2af516 Propagate context to lrpc 2022-05-12 17:14:34 -07:00
60f1eedc9a Create and propagate contexts wherever possible 2022-05-11 13:24:12 -07:00
c05147518d Add metrics screenshot 2022-05-11 12:10:50 -07:00
422f844943 Add metrics graphs to itgui 2022-05-10 23:37:58 -07:00
66618e5bf0 Add metrics collection via sqlite 2022-05-10 18:03:37 -07:00
0c2e57ced0 Update lrpc 2022-05-10 02:12:52 -07:00
6d9f6fc6e6 Update Go compiler requirement 2022-05-09 21:46:03 -07:00
0cbd6a48ae Allow API client to be made from connection 2022-05-07 21:23:42 -07:00
b614138f6b Update lrpc library 2022-05-07 15:12:29 -07:00
3a0491f069 Add new itgui screenshots 2022-05-05 14:05:58 -07:00
093a5632c7 Rewrite itgui and add new screenshots 2022-05-05 14:00:49 -07:00
91662e6f38 Update infinitime library 2022-05-05 12:41:51 -07:00
931966bf1e Update lrpc for data race fixes 2022-05-04 16:16:28 -07:00
ed01700e26 Update lrpc 2022-05-03 18:55:37 -07:00
e9269e8eb8 Update infinitime library 2022-05-02 20:21:47 -07:00
52b85ab361 Allow changing bluetooth adapter ID 2022-05-02 20:17:38 -07:00
bc45943bdc Update lrpc 2022-05-02 16:28:25 -07:00
6933f45683 Fix lrpc response line number in README 2022-05-01 23:06:27 -07:00
0f22d67395 Update lrpc 2022-05-01 21:39:58 -07:00
6da03181a9 Remove version.txt on clean 2022-05-01 21:22:15 -07:00
44a25625da Only update version if version.txt does not exist 2022-05-01 21:21:22 -07:00
14a38351e4 Update README to reflect recent changes 2022-05-01 21:16:47 -07:00
4c27f424b2 Remove debug print 2022-05-01 20:56:14 -07:00
86fbef2e8a Remove the no-longer useful none type alias 2022-05-01 20:51:13 -07:00
7b8658e072 Remove version.txt 2022-05-01 20:49:42 -07:00
73c46cfa66 Remove replace directive and fix firmware upgrade error 2022-05-01 20:40:30 -07:00
1e0f1c5b76 Fix bug where itctl could not be killed 2022-05-01 20:32:59 -07:00
78b5ca1de8 Add context support and update lrpc 2022-05-01 15:22:28 -07:00
b0c4574481 Remove now unnecessary DoneMap 2022-05-01 14:00:31 -07:00
01975f207c Upgrade lrpc version 2022-05-01 13:59:40 -07:00
428e7967c1 Use default codec 2022-05-01 11:41:16 -07:00
56dbf0540e Switch to lrpc and use context to handle signals 2022-05-01 11:36:28 -07:00
240e7a5ee4 Use rpcxlite 2022-04-30 03:25:27 -07:00
625805fe96 Add comments 2022-04-24 00:58:39 -07:00
4b6f7d408e Support bidirectional requests over gateway 2022-04-24 00:54:04 -07:00
9034ef7c6b Add debug logs 2022-04-23 20:20:13 -07:00
9939f724c4 Re-add watch commands to itctl 2022-04-23 18:46:49 -07:00
8dce33f7b1 Enable RPCX gateway 2022-04-23 11:29:16 -07:00
563009c44d Merge branch 'master' of ssh://192.168.100.62:2222/Arsen6331/itd 2022-04-22 19:22:32 -07:00
d4a8a9f8c9 Improve error handling 2022-04-22 18:43:13 -07:00
7fd9af3288 Remove old code comment 2022-04-22 17:19:23 -07:00
4508559bfd Update module go version to 1.17 2022-04-22 17:15:41 -07:00
0cdf8a4bed Switch from custom socket API to rpcx 2022-04-22 17:12:30 -07:00
2af6c1887f Fix typo in code (Czeck -> Czech) 2022-04-16 10:15:55 -07:00
3a3f95acdf Fix typo (Czeck -> Czech) 2022-04-16 10:14:18 -07:00
d318c584da Use new changes in infinitime library to stop removing InfiniTime devices (Fixes #10) 2022-04-16 04:28:53 -07:00
c8c617c10a Fix itctl panic when itd is not running (Fixes #14) 2022-04-02 15:20:31 -07:00
365414f951 Merge pull request 'emoji translation: Add my frequently received emojis' (#15) from earboxer/itd:common-emojis into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/15
2022-03-25 17:22:02 -07:00
9b04d06560 emoji translation: Add my frequently received emojis
mapped to a common ASCII emoticon, or to the shortcode
2022-03-21 11:58:13 -04:00
23e9195e70 Remove debug code 2022-03-15 19:25:37 -07:00
cd68fbd7f3 Update 'cmd/itctl/main.go' 2022-03-15 16:16:44 -07:00
205a041758 Remove exit error handler because it causes duplicated help text 2022-03-15 16:06:05 -07:00
62597f70ee Transliterate song metadata (Fixes #13) 2022-03-11 13:14:23 -08:00
32bb141244 Merge pull request 'Romanian transliterate' (#12) from eugenr/itd:romanian into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/12
2022-03-11 10:04:26 -08:00
f28c68438a Add Romanian to README.md 2022-03-11 04:17:12 -08:00
aa90e9eb26 Romanian translit 2022-03-11 04:15:10 -08:00
553709ce8d Make sure fs is only updated if dev.FS() succeeds (#11) 2022-03-08 08:32:31 -08:00
2ded0d36b1 Update infinitime library for #9 fix 2022-03-04 12:05:58 -08:00
a885eacc70 Rewrite itctl to use urfave/cli instead of spf13/cobra 2022-02-24 21:26:40 -08:00
9e63401db3 Add update weather command to itctl 2022-02-23 21:22:03 -08:00
2f14e70721 Add default version.txt file 2022-02-22 08:44:50 -08:00
614d14e399 Add version flag 2022-02-22 08:43:29 -08:00
c08ddfd810 Add enable switch for weather to config 2022-02-22 08:33:27 -08:00
4bdb82b1bc Add error logging for weather 2022-02-21 16:27:04 -08:00
b4d302caf6 Implement weather via MET Norway 2022-02-21 16:18:52 -08:00
4b2694ee0d Switch from viper to koanf 2022-02-21 11:20:02 -08:00
4c36144b0b Update version of infinitime library for rewritten connection code 2022-02-21 02:47:48 -08:00
e88dea40fb Reorganize and clean code 2021-12-17 00:31:05 -08:00
23b9cfe8a3 Update Infinitime library to use custom agent 2021-12-16 21:32:06 -08:00
518fe74e96 Propagate FS errors on read/write and close files when finished writing 2021-12-13 09:58:34 -08:00
27aabdceba Make paths absolute for firmware upgrades 2021-12-12 17:46:50 -08:00
c019d7523b Implement file transfer progress 2021-12-12 17:08:48 -08:00
03c3c6b22f Remove debug code 2021-12-11 22:23:01 -08:00
69d1027f01 Create absolute directories for ITD to read/write 2021-12-11 22:13:21 -08:00
873df67d1f Directly read/write files from ITD 2021-12-11 22:11:01 -08:00
a9ef386883 Fix comments in filesystem commands 2021-11-27 00:11:37 -08:00
24cfda82d7 Fix and add error messages to fs operations 2021-11-27 00:03:13 -08:00
655af5c446 Ensure that the FS works after a reconnect 2021-11-25 20:35:03 -08:00
b363a20a9d Remove replace directive 2021-11-25 19:49:07 -08:00
f5d326124d Add missing responses to some FS operations 2021-11-25 19:46:04 -08:00
cb8fb2c0bc Add newline for read file command if output is stdout 2021-11-25 19:44:43 -08:00
38119435f1 Get BLE FS once rather than on every connection 2021-11-25 19:41:44 -08:00
034a69c12f Allow multiple call notification responses 2021-11-25 12:41:36 -08:00
70006a3d7b Remove playerctl from depencency list 2021-11-24 16:46:57 -08:00
8aada58d64 Update music control implementation 2021-11-24 16:44:36 -08:00
5d231207cd Remove useless function call 2021-11-24 13:07:48 -08:00
584d9426e6 Make sure modemmanager exists for call notifications 2021-11-24 13:04:20 -08:00
7a772a5458 Add comments 2021-11-24 12:00:44 -08:00
079c733b60 Use clearer variable names 2021-11-24 11:54:16 -08:00
e24a8e9088 Use new helper functions 2021-11-24 11:52:52 -08:00
0b5d777077 Switch calls to use dbus library and add helpers for private connections 2021-11-24 11:36:36 -08:00
75327286ef Switch to private bus connection 2021-11-23 22:03:41 -08:00
099b0cd849 Add filesystem to itctl 2021-11-23 14:14:45 -08:00
c9c00e0072 Allow multiple paths in mkdir and remove 2021-11-23 13:35:18 -08:00
b2ffb2062a Fix write file in api package 2021-11-23 11:19:21 -08:00
2e8c825fff Add BLE FS to API package 2021-11-23 11:12:16 -08:00
7b870950d1 Implement BLE FS 2021-11-22 22:04:09 -08:00
3a877c41a4 Update infinitime library to fix compatibility with BlueZ 5.62 2021-11-22 01:18:40 -08:00
04fb390bee Add reminder to validate firmware to itctl and itgui 2021-11-06 19:06:17 -07:00
50b17d3266 Update default values to reflect new config fields 2021-11-01 11:28:55 -07:00
763d408405 Upgrade infinitime library version 2021-11-01 11:21:37 -07:00
fbb7cd9bc1 Remove config version field 2021-10-27 08:34:10 -07:00
f1b7f70313 Add whitelist support 2021-10-27 07:27:12 -07:00
81 changed files with 4028 additions and 2868 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
/itctl
/itd
/itgui
/version.txt
dist/

108
.goreleaser.yaml Normal file
View 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

View File

@ -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
View File

@ -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, &copy; [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
View 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()
}

View File

@ -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
View 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
View 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
View 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
}

View File

@ -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
}

View File

@ -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
View 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
View 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,
)
}

View File

@ -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
View 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
View 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,
)
}

View File

@ -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
View 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
View File

@ -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
View 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
}

View File

@ -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)
}

View File

@ -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"))
}

View File

@ -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
View 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
View 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
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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"))
}

View File

@ -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")
}

View File

@ -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
View 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
}

View File

@ -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
View 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
}

View File

@ -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
View 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)
}
}

View File

@ -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)
}

View File

@ -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
View 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
View 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
}
}
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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"))
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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
View 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
View 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
View 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
}

View File

@ -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
View 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(),
)
}

View File

@ -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()
}

View File

@ -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,
))
}

View File

@ -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
View 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))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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
View 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()
}

View File

@ -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
View File

@ -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
View 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
View File

@ -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
View File

@ -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=

View File

@ -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
}

View File

@ -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
View File

@ -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
View 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
}

View File

@ -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")))
}
}
}()

View File

@ -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
View File

@ -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)
}

View File

@ -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
View 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)))
}