Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e072a3540 | |||
| f639fef992 | |||
| 2d0db1dcf1 | |||
| 4efa4380c4 | |||
| fca64afbf3 | |||
| cf24c5ace8 | |||
| a25b2e3e62 | |||
| 2d0b64d92f | |||
| 5efafe9be7 | |||
| 271510d528 | |||
| 4d72a063b2 | |||
| 643245f16c | |||
| 6f87980d4b | |||
| 851f1975d6 | |||
| 5e66fe82ac | |||
| 645541e079 | |||
| 1012be6e5b | |||
| 5973290d6c | |||
| 19bacf29b2 | |||
| a78650e526 | |||
| 71e9caf0bc | |||
| 1f5a6365bc | |||
| 958f2af516 | |||
| 60f1eedc9a | |||
| c05147518d | |||
| 422f844943 | |||
| 66618e5bf0 | |||
| 0c2e57ced0 | |||
| 6d9f6fc6e6 | |||
| 0cbd6a48ae | |||
| b614138f6b | |||
| 3a0491f069 | |||
| 093a5632c7 | |||
| 91662e6f38 | |||
| 931966bf1e | |||
| ed01700e26 | |||
| e9269e8eb8 | |||
| 52b85ab361 | |||
| bc45943bdc | |||
| 6933f45683 | |||
| 0f22d67395 | |||
| 6da03181a9 | |||
| 44a25625da | |||
| 14a38351e4 | |||
| 4c27f424b2 | |||
| 86fbef2e8a | |||
| 7b8658e072 | |||
| 73c46cfa66 | |||
| 1e0f1c5b76 | |||
| 78b5ca1de8 | |||
| b0c4574481 | |||
| 01975f207c | |||
| 428e7967c1 | |||
| 56dbf0540e | |||
| 240e7a5ee4 |
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
/itd
|
/itd
|
||||||
/itgui
|
/itgui
|
||||||
/version.txt
|
/version.txt
|
||||||
|
dist/
|
||||||
|
|||||||
108
.goreleaser.yaml
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
builds:
|
||||||
|
- id: itd
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
binary: itd
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- 386
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
- id: itctl
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
main: ./cmd/itctl
|
||||||
|
binary: itctl
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- 386
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
archives:
|
||||||
|
- replacements:
|
||||||
|
386: i386
|
||||||
|
amd64: x86_64
|
||||||
|
arm64: aarch64
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- itd.toml
|
||||||
|
- itd.service
|
||||||
|
nfpms:
|
||||||
|
- id: itd
|
||||||
|
file_name_template: '{{.PackageName}}-{{.Version}}-{{.Os}}-{{.Arch}}'
|
||||||
|
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||||
|
replacements:
|
||||||
|
386: i386
|
||||||
|
amd64: x86_64
|
||||||
|
arm64: aarch64
|
||||||
|
homepage: 'https://gitea.arsenm.dev/Arsen6331/itd'
|
||||||
|
maintainer: 'Arsen Musyaelyan <arsen@arsenm.dev>'
|
||||||
|
license: GPLv3
|
||||||
|
formats:
|
||||||
|
- apk
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
dependencies:
|
||||||
|
- dbus
|
||||||
|
- bluez
|
||||||
|
- pulseaudio-utils
|
||||||
|
contents:
|
||||||
|
- src: itd.toml
|
||||||
|
dst: /etc/itd.toml
|
||||||
|
type: "config|noreplace"
|
||||||
|
- src: itd.service
|
||||||
|
dst: /usr/lib/systemd/user/itd.service
|
||||||
|
aurs:
|
||||||
|
- name: itd-bin
|
||||||
|
homepage: 'https://gitea.arsenm.dev/Arsen6331/itd'
|
||||||
|
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
|
||||||
|
maintainers:
|
||||||
|
- 'Arsen Musyaelyan <arsen@arsenm.dev>'
|
||||||
|
license: GPLv3
|
||||||
|
private_key: '{{ .Env.AUR_KEY }}'
|
||||||
|
git_url: 'ssh://aur@aur.archlinux.org/itd-bin.git'
|
||||||
|
provides:
|
||||||
|
- itd
|
||||||
|
- itctl
|
||||||
|
conflicts:
|
||||||
|
- itd
|
||||||
|
- itctl
|
||||||
|
depends:
|
||||||
|
- dbus
|
||||||
|
- bluez
|
||||||
|
- libpulse
|
||||||
|
package: |-
|
||||||
|
# binaries
|
||||||
|
install -Dm755 "./itd" "${pkgdir}/usr/bin/itd"
|
||||||
|
install -Dm755 "./itctl" "${pkgdir}/usr/bin/itctl"
|
||||||
|
|
||||||
|
# service
|
||||||
|
install -Dm644 "./itd.service" ${pkgdir}/usr/lib/systemd/user/itd.service
|
||||||
|
|
||||||
|
# config
|
||||||
|
install -Dm644 "./itd.toml" ${pkgdir}/etc/itd.toml
|
||||||
|
|
||||||
|
# license
|
||||||
|
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/itd/LICENSE"
|
||||||
|
release:
|
||||||
|
gitea:
|
||||||
|
owner: Arsen6331
|
||||||
|
name: itd
|
||||||
|
gitea_urls:
|
||||||
|
api: 'https://gitea.arsenm.dev/api/v1/'
|
||||||
|
download: 'https://gitea.arsenm.dev'
|
||||||
|
skip_tls_verify: false
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
12
Makefile
@@ -3,14 +3,14 @@ BIN_PREFIX = $(DESTDIR)$(PREFIX)/bin
|
|||||||
SERVICE_PREFIX = $(DESTDIR)$(PREFIX)/lib/systemd/user
|
SERVICE_PREFIX = $(DESTDIR)$(PREFIX)/lib/systemd/user
|
||||||
CFG_PREFIX = $(DESTDIR)/etc
|
CFG_PREFIX = $(DESTDIR)/etc
|
||||||
|
|
||||||
all: version
|
all: version.txt
|
||||||
go build $(GOFLAGS)
|
go build
|
||||||
go build ./cmd/itctl $(GOFLAGS)
|
go build ./cmd/itctl
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f itctl
|
rm -f itctl
|
||||||
rm -f itd
|
rm -f itd
|
||||||
printf "unknown" > version.txt
|
rm -f version.txt
|
||||||
|
|
||||||
install:
|
install:
|
||||||
install -Dm755 ./itd $(BIN_PREFIX)/itd
|
install -Dm755 ./itd $(BIN_PREFIX)/itd
|
||||||
@@ -24,7 +24,7 @@ uninstall:
|
|||||||
rm $(SERVICE_PREFIX)/itd.service
|
rm $(SERVICE_PREFIX)/itd.service
|
||||||
rm $(CFG_PREFIX)/itd.toml
|
rm $(CFG_PREFIX)/itd.toml
|
||||||
|
|
||||||
version:
|
version.txt:
|
||||||
printf "r%s.%s" "$(shell git rev-list --count HEAD)" "$(shell git rev-parse --short HEAD)" > 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 version
|
.PHONY: all clean install uninstall
|
||||||
89
README.md
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
`itd` is a daemon that uses my infinitime [library](https://go.arsenm.dev/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io).
|
`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).
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/moussaelianarsen/itd)
|
[](https://ci.appveyor.com/project/moussaelianarsen/itd-7t6ko)
|
||||||
[](https://minio.arsenm.dev/minio/itd/)
|
[](https://aur.archlinux.org/packages/itd-git/)
|
||||||
[](https://aur.archlinux.org/packages/itd-git/)
|
[](https://aur.archlinux.org/packages/itd-bin/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,6 +19,38 @@
|
|||||||
- Set current time
|
- Set current time
|
||||||
- Control socket
|
- Control socket
|
||||||
- Firmware upgrades
|
- 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.
|
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
|
```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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -105,48 +135,47 @@ Use "itctl [command] --help" for more information about a command.
|
|||||||
|
|
||||||
### `itgui`
|
### `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
|
```shell
|
||||||
go build ./cmd/itgui
|
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
|
#### Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
---
|

|
||||||
|
|
||||||
#### Interactive mode
|

|
||||||
|
|
||||||
Running `itctl` by itself will open interactive mode. It's essentially a shell where you can enter commands. For example:
|

|
||||||
|
|
||||||
```
|

|
||||||
$ 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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Installation
|
### 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
|
To install, run
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
106
api/api.go
@@ -1,117 +1,37 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/smallnest/rpcx/client"
|
"go.arsenm.dev/lrpc/client"
|
||||||
"github.com/smallnest/rpcx/protocol"
|
"go.arsenm.dev/lrpc/codec"
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
|
||||||
"go.arsenm.dev/infinitime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultAddr = "/tmp/itd/socket"
|
const DefaultAddr = "/tmp/itd/socket"
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
itdClient client.XClient
|
client *client.Client
|
||||||
itdCh chan *protocol.Message
|
|
||||||
fsClient client.XClient
|
|
||||||
fsCh chan *protocol.Message
|
|
||||||
srvVals map[string]chan interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(sockPath string) (*Client, error) {
|
func New(sockPath string) (*Client, error) {
|
||||||
d, err := client.NewPeer2PeerDiscovery("unix@"+sockPath, "")
|
conn, err := net.Dial("unix", sockPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
out := &Client{}
|
out := &Client{
|
||||||
|
client: client.New(conn, codec.Default),
|
||||||
out.itdCh = make(chan *protocol.Message, 5)
|
}
|
||||||
out.itdClient = client.NewBidirectionalXClient(
|
|
||||||
"ITD",
|
|
||||||
client.Failtry,
|
|
||||||
client.RandomSelect,
|
|
||||||
d,
|
|
||||||
client.DefaultOption,
|
|
||||||
out.itdCh,
|
|
||||||
)
|
|
||||||
|
|
||||||
out.fsCh = make(chan *protocol.Message, 5)
|
|
||||||
out.fsClient = client.NewBidirectionalXClient(
|
|
||||||
"FS",
|
|
||||||
client.Failtry,
|
|
||||||
client.RandomSelect,
|
|
||||||
d,
|
|
||||||
client.DefaultOption,
|
|
||||||
out.fsCh,
|
|
||||||
)
|
|
||||||
|
|
||||||
out.srvVals = map[string]chan interface{}{}
|
|
||||||
|
|
||||||
go out.handleMessages(out.itdCh)
|
|
||||||
go out.handleMessages(out.fsCh)
|
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) handleMessages(msgCh chan *protocol.Message) {
|
func NewFromConn(conn io.ReadWriteCloser) *Client {
|
||||||
for msg := range msgCh {
|
return &Client{
|
||||||
_, ok := c.srvVals[msg.ServicePath]
|
client: client.New(conn, codec.Default),
|
||||||
if !ok {
|
|
||||||
c.srvVals[msg.ServicePath] = make(chan interface{}, 5)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Printf("%+v\n", msg)
|
|
||||||
|
|
||||||
ch := c.srvVals[msg.ServicePath]
|
|
||||||
|
|
||||||
switch msg.ServiceMethod {
|
|
||||||
case "FSProgress":
|
|
||||||
var progress FSTransferProgress
|
|
||||||
msgpack.Unmarshal(msg.Payload, &progress)
|
|
||||||
ch <- progress
|
|
||||||
case "DFUProgress":
|
|
||||||
var progress infinitime.DFUProgress
|
|
||||||
msgpack.Unmarshal(msg.Payload, &progress)
|
|
||||||
ch <- progress
|
|
||||||
case "MotionSample":
|
|
||||||
var motionVals infinitime.MotionValues
|
|
||||||
msgpack.Unmarshal(msg.Payload, &motionVals)
|
|
||||||
ch <- motionVals
|
|
||||||
case "Done":
|
|
||||||
close(c.srvVals[msg.ServicePath])
|
|
||||||
delete(c.srvVals, msg.ServicePath)
|
|
||||||
default:
|
|
||||||
var value interface{}
|
|
||||||
msgpack.Unmarshal(msg.Payload, &value)
|
|
||||||
ch <- value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) done(id string) error {
|
|
||||||
return c.itdClient.Call(
|
|
||||||
context.Background(),
|
|
||||||
"Done",
|
|
||||||
id,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
err := c.itdClient.Close()
|
return c.client.Close()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.fsClient.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
close(c.itdCh)
|
|
||||||
close(c.fsCh)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,39 +2,25 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) FirmwareUpgrade(upgType UpgradeType, files ...string) (chan infinitime.DFUProgress, error) {
|
func (c *Client) FirmwareUpgrade(ctx context.Context, upgType UpgradeType, files ...string) (chan infinitime.DFUProgress, error) {
|
||||||
var id string
|
progressCh := make(chan infinitime.DFUProgress, 5)
|
||||||
err := c.itdClient.Call(
|
err := c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"FirmwareUpgrade",
|
"FirmwareUpgrade",
|
||||||
FwUpgradeData{
|
FwUpgradeData{
|
||||||
Type: upgType,
|
Type: upgType,
|
||||||
Files: files,
|
Files: files,
|
||||||
},
|
},
|
||||||
&id,
|
progressCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
progressCh := make(chan infinitime.DFUProgress, 5)
|
|
||||||
go func() {
|
|
||||||
srvValCh, ok := c.srvVals[id]
|
|
||||||
for !ok {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
for val := range srvValCh {
|
|
||||||
progressCh <- val.(infinitime.DFUProgress)
|
|
||||||
}
|
|
||||||
close(progressCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return progressCh, nil
|
return progressCh, nil
|
||||||
}
|
}
|
||||||
|
|||||||
105
api/fs.go
@@ -1,40 +1,61 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import "context"
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) Remove(paths ...string) error {
|
func (c *Client) RemoveAll(ctx context.Context, paths ...string) error {
|
||||||
return c.fsClient.Call(
|
return c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"FS",
|
||||||
|
"RemoveAll",
|
||||||
|
paths,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Remove(ctx context.Context, paths ...string) error {
|
||||||
|
return c.client.Call(
|
||||||
|
ctx,
|
||||||
|
"FS",
|
||||||
"Remove",
|
"Remove",
|
||||||
paths,
|
paths,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Rename(old, new string) error {
|
func (c *Client) Rename(ctx context.Context, old, new string) error {
|
||||||
return c.fsClient.Call(
|
return c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
"Remove",
|
"FS",
|
||||||
|
"Rename",
|
||||||
[2]string{old, new},
|
[2]string{old, new},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Mkdir(paths ...string) error {
|
func (c *Client) MkdirAll(ctx context.Context, paths ...string) error {
|
||||||
return c.fsClient.Call(
|
return c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"FS",
|
||||||
|
"MkdirAll",
|
||||||
|
paths,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Mkdir(ctx context.Context, paths ...string) error {
|
||||||
|
return c.client.Call(
|
||||||
|
ctx,
|
||||||
|
"FS",
|
||||||
"Mkdir",
|
"Mkdir",
|
||||||
paths,
|
paths,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ReadDir(dir string) (out []FileInfo, err error) {
|
func (c *Client) ReadDir(ctx context.Context, dir string) (out []FileInfo, err error) {
|
||||||
err = c.fsClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"FS",
|
||||||
"ReadDir",
|
"ReadDir",
|
||||||
dir,
|
dir,
|
||||||
&out,
|
&out,
|
||||||
@@ -42,60 +63,34 @@ func (c *Client) ReadDir(dir string) (out []FileInfo, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Upload(dst, src string) (chan FSTransferProgress, error) {
|
func (c *Client) Upload(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
|
||||||
var id string
|
progressCh := make(chan FSTransferProgress, 5)
|
||||||
err := c.fsClient.Call(
|
err := c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"FS",
|
||||||
"Upload",
|
"Upload",
|
||||||
[2]string{dst, src},
|
[2]string{dst, src},
|
||||||
&id,
|
progressCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
progressCh := make(chan FSTransferProgress, 5)
|
|
||||||
go func() {
|
|
||||||
srvValCh, ok := c.srvVals[id]
|
|
||||||
for !ok {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
for val := range srvValCh {
|
|
||||||
progressCh <- val.(FSTransferProgress)
|
|
||||||
}
|
|
||||||
close(progressCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return progressCh, nil
|
return progressCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Download(dst, src string) (chan FSTransferProgress, error) {
|
func (c *Client) Download(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
|
||||||
var id string
|
progressCh := make(chan FSTransferProgress, 5)
|
||||||
err := c.fsClient.Call(
|
err := c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"FS",
|
||||||
"Download",
|
"Download",
|
||||||
[2]string{dst, src},
|
[2]string{dst, src},
|
||||||
&id,
|
progressCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
progressCh := make(chan FSTransferProgress, 5)
|
|
||||||
go func() {
|
|
||||||
srvValCh, ok := c.srvVals[id]
|
|
||||||
for !ok {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
for val := range srvValCh {
|
|
||||||
progressCh <- val.(FSTransferProgress)
|
|
||||||
}
|
|
||||||
close(progressCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return progressCh, nil
|
return progressCh, nil
|
||||||
}
|
}
|
||||||
|
|||||||
42
api/get.go
@@ -6,9 +6,10 @@ import (
|
|||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) HeartRate() (out uint8, err error) {
|
func (c *Client) HeartRate(ctx context.Context) (out uint8, err error) {
|
||||||
err = c.itdClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"HeartRate",
|
"HeartRate",
|
||||||
nil,
|
nil,
|
||||||
&out,
|
&out,
|
||||||
@@ -16,9 +17,10 @@ func (c *Client) HeartRate() (out uint8, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) BatteryLevel() (out uint8, err error) {
|
func (c *Client) BatteryLevel(ctx context.Context) (out uint8, err error) {
|
||||||
err = c.itdClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"BatteryLevel",
|
"BatteryLevel",
|
||||||
nil,
|
nil,
|
||||||
&out,
|
&out,
|
||||||
@@ -26,9 +28,10 @@ func (c *Client) BatteryLevel() (out uint8, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Motion() (out infinitime.MotionValues, err error) {
|
func (c *Client) Motion(ctx context.Context) (out infinitime.MotionValues, err error) {
|
||||||
err = c.itdClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"Motion",
|
"Motion",
|
||||||
nil,
|
nil,
|
||||||
&out,
|
&out,
|
||||||
@@ -36,9 +39,10 @@ func (c *Client) Motion() (out infinitime.MotionValues, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) StepCount() (out uint32, err error) {
|
func (c *Client) StepCount(ctx context.Context) (out uint32, err error) {
|
||||||
err = c.itdClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"StepCount",
|
"StepCount",
|
||||||
nil,
|
nil,
|
||||||
&out,
|
&out,
|
||||||
@@ -46,9 +50,10 @@ func (c *Client) StepCount() (out uint32, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Version() (out string, err error) {
|
func (c *Client) Version(ctx context.Context) (out string, err error) {
|
||||||
err = c.itdClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"Version",
|
"Version",
|
||||||
nil,
|
nil,
|
||||||
&out,
|
&out,
|
||||||
@@ -56,9 +61,10 @@ func (c *Client) Version() (out string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Address() (out string, err error) {
|
func (c *Client) Address(ctx context.Context) (out string, err error) {
|
||||||
err = c.itdClient.Call(
|
err = c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"Address",
|
"Address",
|
||||||
nil,
|
nil,
|
||||||
&out,
|
&out,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import "context"
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) Notify(title, body string) error {
|
func (c *Client) Notify(ctx context.Context, title, body string) error {
|
||||||
return c.itdClient.Call(
|
return c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"Notify",
|
"Notify",
|
||||||
NotifyData{
|
NotifyData{
|
||||||
Title: title,
|
Title: title,
|
||||||
|
|||||||
26
api/resources.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.arsenm.dev/infinitime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadResources loads resources onto the watch from the given
|
||||||
|
// file path to the resources zip
|
||||||
|
func (c *Client) LoadResources(ctx context.Context, path string) (<-chan infinitime.ResourceLoadProgress, error) {
|
||||||
|
progCh := make(chan infinitime.ResourceLoadProgress)
|
||||||
|
|
||||||
|
err := c.client.Call(
|
||||||
|
ctx,
|
||||||
|
"FS",
|
||||||
|
"LoadResources",
|
||||||
|
path,
|
||||||
|
progCh,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return progCh, nil
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) SetTime(t time.Time) error {
|
func (c *Client) SetTime(ctx context.Context, t time.Time) error {
|
||||||
return c.itdClient.Call(
|
return c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"SetTime",
|
"SetTime",
|
||||||
t,
|
t,
|
||||||
nil,
|
nil,
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package api
|
|||||||
|
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
func (c *Client) WeatherUpdate() error {
|
func (c *Client) WeatherUpdate(ctx context.Context) error {
|
||||||
return c.itdClient.Call(
|
return c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"WeatherUpdate",
|
"WeatherUpdate",
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
|||||||
139
api/watch.go
@@ -2,143 +2,70 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) {
|
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
|
||||||
var id string
|
outCh := make(chan uint8, 2)
|
||||||
err := c.itdClient.Call(
|
err := c.client.Call(
|
||||||
context.Background(),
|
ctx,
|
||||||
|
"ITD",
|
||||||
"WatchHeartRate",
|
"WatchHeartRate",
|
||||||
nil,
|
nil,
|
||||||
&id,
|
outCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return outCh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
|
||||||
outCh := make(chan uint8, 2)
|
outCh := make(chan uint8, 2)
|
||||||
go func() {
|
err := c.client.Call(
|
||||||
srvValCh, ok := c.srvVals[id]
|
ctx,
|
||||||
for !ok {
|
"ITD",
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
for val := range srvValCh {
|
|
||||||
outCh <- val.(uint8)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
doneFn := func() {
|
|
||||||
c.done(id)
|
|
||||||
close(c.srvVals[id])
|
|
||||||
delete(c.srvVals, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outCh, doneFn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
|
|
||||||
var id string
|
|
||||||
err := c.itdClient.Call(
|
|
||||||
context.Background(),
|
|
||||||
"WatchBatteryLevel",
|
"WatchBatteryLevel",
|
||||||
nil,
|
nil,
|
||||||
&id,
|
outCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
outCh := make(chan uint8, 2)
|
return outCh, nil
|
||||||
go func() {
|
|
||||||
srvValCh, ok := c.srvVals[id]
|
|
||||||
for !ok {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for val := range srvValCh {
|
func (c *Client) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
|
||||||
outCh <- val.(uint8)
|
outCh := make(chan uint32, 2)
|
||||||
}
|
err := c.client.Call(
|
||||||
}()
|
ctx,
|
||||||
|
"ITD",
|
||||||
doneFn := func() {
|
|
||||||
c.done(id)
|
|
||||||
close(c.srvVals[id])
|
|
||||||
delete(c.srvVals, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outCh, doneFn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) WatchStepCount() (<-chan uint32, func(), error) {
|
|
||||||
var id string
|
|
||||||
err := c.itdClient.Call(
|
|
||||||
context.Background(),
|
|
||||||
"WatchStepCount",
|
"WatchStepCount",
|
||||||
nil,
|
nil,
|
||||||
&id,
|
outCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
outCh := make(chan uint32, 2)
|
return outCh, nil
|
||||||
go func() {
|
|
||||||
srvValCh, ok := c.srvVals[id]
|
|
||||||
for !ok {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for val := range srvValCh {
|
func (c *Client) WatchMotion(ctx context.Context) (<-chan infinitime.MotionValues, error) {
|
||||||
outCh <- val.(uint32)
|
outCh := make(chan infinitime.MotionValues, 2)
|
||||||
}
|
err := c.client.Call(
|
||||||
}()
|
ctx,
|
||||||
|
"ITD",
|
||||||
doneFn := func() {
|
|
||||||
c.done(id)
|
|
||||||
close(c.srvVals[id])
|
|
||||||
delete(c.srvVals, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outCh, doneFn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) {
|
|
||||||
var id string
|
|
||||||
err := c.itdClient.Call(
|
|
||||||
context.Background(),
|
|
||||||
"WatchMotion",
|
"WatchMotion",
|
||||||
nil,
|
nil,
|
||||||
&id,
|
outCh,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
outCh := make(chan infinitime.MotionValues, 2)
|
return outCh, nil
|
||||||
go func() {
|
|
||||||
srvValCh, ok := c.srvVals[id]
|
|
||||||
for !ok {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
srvValCh, ok = c.srvVals[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
for val := range srvValCh {
|
|
||||||
outCh <- val.(infinitime.MotionValues)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
doneFn := func() {
|
|
||||||
c.done(id)
|
|
||||||
close(c.srvVals[id])
|
|
||||||
delete(c.srvVals, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outCh, doneFn, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
calls.go
@@ -1,22 +1,23 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gitea.arsenm.dev/Arsen6331/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initCallNotifs(dev *infinitime.Device) error {
|
func initCallNotifs(ctx context.Context, dev *infinitime.Device) error {
|
||||||
// Connect to system bus. This connection is for method calls.
|
// Connect to system bus. This connection is for method calls.
|
||||||
conn, err := newSystemBusConn()
|
conn, err := newSystemBusConn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if modem manager interface exists
|
// Check if modem manager interface exists
|
||||||
exists, err := modemManagerExists(conn)
|
exists, err := modemManagerExists(ctx, conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ func initCallNotifs(dev *infinitime.Device) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect to system bus. This connection is for monitoring.
|
// Connect to system bus. This connection is for monitoring.
|
||||||
monitorConn, err := newSystemBusConn()
|
monitorConn, err := newSystemBusConn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -78,13 +79,13 @@ func initCallNotifs(dev *infinitime.Device) error {
|
|||||||
switch res {
|
switch res {
|
||||||
case infinitime.CallStatusAccepted:
|
case infinitime.CallStatusAccepted:
|
||||||
// Attempt to accept call
|
// Attempt to accept call
|
||||||
err = acceptCall(conn, callObj)
|
err = acceptCall(ctx, conn, callObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Error accepting call")
|
log.Warn().Err(err).Msg("Error accepting call")
|
||||||
}
|
}
|
||||||
case infinitime.CallStatusDeclined:
|
case infinitime.CallStatusDeclined:
|
||||||
// Attempt to decline call
|
// Attempt to decline call
|
||||||
err = declineCall(conn, callObj)
|
err = declineCall(ctx, conn, callObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Error declining call")
|
log.Warn().Err(err).Msg("Error declining call")
|
||||||
}
|
}
|
||||||
@@ -101,9 +102,11 @@ func initCallNotifs(dev *infinitime.Device) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func modemManagerExists(conn *dbus.Conn) (bool, error) {
|
func modemManagerExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
|
||||||
var names []string
|
var names []string
|
||||||
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
|
err := conn.BusObject().CallWithContext(
|
||||||
|
ctx, "org.freedesktop.DBus.ListNames", 0,
|
||||||
|
).Store(&names)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -122,9 +125,11 @@ func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getPhoneNum accepts a call using a DBus connection
|
// getPhoneNum accepts a call using a DBus connection
|
||||||
func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||||
// Call Accept() method on DBus object
|
// Call Accept() method on DBus object
|
||||||
call := callObj.Call("org.freedesktop.ModemManager1.Call.Accept", 0)
|
call := callObj.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.ModemManager1.Call.Accept", 0,
|
||||||
|
)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return call.Err
|
return call.Err
|
||||||
}
|
}
|
||||||
@@ -132,9 +137,11 @@ func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getPhoneNum declines a call using a DBus connection
|
// getPhoneNum declines a call using a DBus connection
|
||||||
func declineCall(conn *dbus.Conn, callObj dbus.BusObject) error {
|
func declineCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
|
||||||
// Call Hangup() method on DBus object
|
// Call Hangup() method on DBus object
|
||||||
call := callObj.Call("org.freedesktop.ModemManager1.Call.Hangup", 0)
|
call := callObj.CallWithContext(
|
||||||
|
ctx, "org.freedesktop.ModemManager1.Call.Hangup", 0,
|
||||||
|
)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return call.Err
|
return call.Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func fwUpgrade(c *cli.Context) error {
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
var upgType api.UpgradeType
|
var upgType api.UpgradeType
|
||||||
@@ -28,7 +41,7 @@ func fwUpgrade(c *cli.Context) error {
|
|||||||
return cli.Exit("Upgrade command requires either archive or init packet and firmware.", 1)
|
return cli.Exit("Upgrade command requires either archive or init packet and firmware.", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
progress, err := client.FirmwareUpgrade(upgType, abs(files)...)
|
progress, err := client.FirmwareUpgrade(c.Context, upgType, abs(files)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -58,7 +71,7 @@ func fwUpgrade(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fwVersion(c *cli.Context) error {
|
func fwVersion(c *cli.Context) error {
|
||||||
version, err := client.Version()
|
version, err := client.Version(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func fsList(c *cli.Context) error {
|
|||||||
dirPath = c.Args().Get(0)
|
dirPath = c.Args().Get(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
listing, err := client.ReadDir(dirPath)
|
listing, err := client.ReadDir(c.Context, dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,12 @@ func fsMkdir(c *cli.Context) error {
|
|||||||
return cli.Exit("Command mkdir requires one or more arguments", 1)
|
return cli.Exit("Command mkdir requires one or more arguments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.Mkdir(c.Args().Slice()...)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -47,7 +52,7 @@ func fsMove(c *cli.Context) error {
|
|||||||
return cli.Exit("Command move requires two arguments", 1)
|
return cli.Exit("Command move requires two arguments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.Rename(c.Args().Get(0), c.Args().Get(1))
|
err := client.Rename(c.Context, c.Args().Get(0), c.Args().Get(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -76,7 +81,7 @@ func fsRead(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress, err := client.Download(path, c.Args().Get(0))
|
progress, err := client.Download(c.Context, path, c.Args().Get(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -109,7 +114,12 @@ func fsRemove(c *cli.Context) error {
|
|||||||
return cli.Exit("Command remove requires one or more arguments", 1)
|
return cli.Exit("Command remove requires one or more arguments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.Remove(c.Args().Slice()...)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -144,7 +154,7 @@ func fsWrite(c *cli.Context) error {
|
|||||||
defer os.Remove(path)
|
defer os.Remove(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
progress, err := client.Upload(c.Args().Get(1), path)
|
progress, err := client.Upload(c.Context, c.Args().Get(1), path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func getAddress(c *cli.Context) error {
|
func getAddress(c *cli.Context) error {
|
||||||
address, err := client.Address()
|
address, err := client.Address(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ func getAddress(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getBattery(c *cli.Context) error {
|
func getBattery(c *cli.Context) error {
|
||||||
battLevel, err := client.BatteryLevel()
|
battLevel, err := client.BatteryLevel(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ func getBattery(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getHeart(c *cli.Context) error {
|
func getHeart(c *cli.Context) error {
|
||||||
heartRate, err := client.HeartRate()
|
heartRate, err := client.HeartRate(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func getHeart(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getMotion(c *cli.Context) error {
|
func getMotion(c *cli.Context) error {
|
||||||
motionVals, err := client.Motion()
|
motionVals, err := client.Motion(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func getMotion(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSteps(c *cli.Context) error {
|
func getSteps(c *cli.Context) error {
|
||||||
stepCount, err := client.StepCount()
|
stepCount, err := client.StepCount(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -16,8 +18,24 @@ var client *api.Client
|
|||||||
func main() {
|
func main() {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
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{
|
app := cli.App{
|
||||||
Name: "itctl",
|
Name: "itctl",
|
||||||
|
HideHelpCommand: true,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "socket-path",
|
Name: "socket-path",
|
||||||
@@ -27,6 +45,25 @@ func main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Commands: []*cli.Command{
|
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",
|
Name: "filesystem",
|
||||||
Aliases: []string{"fs"},
|
Aliases: []string{"fs"},
|
||||||
@@ -40,6 +77,13 @@ func main() {
|
|||||||
Action: fsList,
|
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",
|
Name: "mkdir",
|
||||||
ArgsUsage: "<paths...>",
|
ArgsUsage: "<paths...>",
|
||||||
Usage: "Create new directories",
|
Usage: "Create new directories",
|
||||||
@@ -60,6 +104,13 @@ func main() {
|
|||||||
Action: fsRead,
|
Action: fsRead,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "recursive",
|
||||||
|
Aliases: []string{"r", "R"},
|
||||||
|
Usage: "Remove directories and their contents recursively",
|
||||||
|
},
|
||||||
|
},
|
||||||
Name: "remove",
|
Name: "remove",
|
||||||
ArgsUsage: "<paths...>",
|
ArgsUsage: "<paths...>",
|
||||||
Aliases: []string{"rm"},
|
Aliases: []string{"rm"},
|
||||||
@@ -92,6 +143,11 @@ func main() {
|
|||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Usage: "Path to firmware image (.bin file)",
|
Usage: "Path to firmware image (.bin file)",
|
||||||
},
|
},
|
||||||
|
&cli.PathFlag{
|
||||||
|
Name: "resources",
|
||||||
|
Aliases: []string{"r"},
|
||||||
|
Usage: "Path to resources file (.zip file)",
|
||||||
|
},
|
||||||
&cli.PathFlag{
|
&cli.PathFlag{
|
||||||
Name: "archive",
|
Name: "archive",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
@@ -221,11 +277,13 @@ func main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Before: func(c *cli.Context) error {
|
Before: func(c *cli.Context) error {
|
||||||
|
if !isHelpCmd() {
|
||||||
newClient, err := api.New(c.String("socket-path"))
|
newClient, err := api.New(c.String("socket-path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client = newClient
|
client = newClient
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
After: func(*cli.Context) error {
|
After: func(*cli.Context) error {
|
||||||
@@ -236,22 +294,26 @@ func main() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Run(os.Args)
|
err := app.RunContext(ctx, os.Args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Error while running app")
|
log.Fatal().Err(err).Msg("Error while running app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func catchSignal(fn func()) {
|
func helpCmd(c *cli.Context) error {
|
||||||
sigCh := make(chan os.Signal, 1)
|
cmdArgs := append([]string{os.Args[0]}, c.Args().Slice()...)
|
||||||
signal.Notify(
|
cmdArgs = append(cmdArgs, "-h")
|
||||||
sigCh,
|
return c.App.RunContext(c.Context, cmdArgs)
|
||||||
syscall.SIGINT,
|
}
|
||||||
syscall.SIGTERM,
|
|
||||||
)
|
func isHelpCmd() bool {
|
||||||
go func() {
|
if len(os.Args) == 1 {
|
||||||
<-sigCh
|
return true
|
||||||
fn()
|
}
|
||||||
os.Exit(0)
|
for _, arg := range os.Args {
|
||||||
}()
|
if arg == "-h" || arg == "help" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func notify(c *cli.Context) error {
|
|||||||
return cli.Exit("Command notify requires two arguments", 1)
|
return cli.Exit("Command notify requires two arguments", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.Notify(c.Args().Get(0), c.Args().Get(1))
|
err := client.Notify(c.Context, c.Args().Get(0), c.Args().Get(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
53
cmd/itctl/resources.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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.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
|
||||||
|
}
|
||||||
@@ -13,12 +13,12 @@ func setTime(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.Args().Get(0) == "now" {
|
if c.Args().Get(0) == "now" {
|
||||||
return client.SetTime(time.Now())
|
return client.SetTime(c.Context, time.Now())
|
||||||
} else {
|
} else {
|
||||||
parsed, err := time.Parse(time.RFC3339, c.Args().Get(0))
|
parsed, err := time.Parse(time.RFC3339, c.Args().Get(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return client.SetTime(parsed)
|
return client.SetTime(c.Context, parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package main
|
|||||||
import "github.com/urfave/cli/v2"
|
import "github.com/urfave/cli/v2"
|
||||||
|
|
||||||
func updateWeather(c *cli.Context) error {
|
func updateWeather(c *cli.Context) error {
|
||||||
return client.WeatherUpdate()
|
return client.WeatherUpdate(c.Context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func watchHeart(c *cli.Context) error {
|
func watchHeart(c *cli.Context) error {
|
||||||
heartCh, cancel, err := client.WatchHeartRate()
|
heartCh, err := client.WatchHeartRate(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
catchSignal(cancel)
|
for {
|
||||||
|
select {
|
||||||
for heartRate := range heartCh {
|
case heartRate := <-heartCh:
|
||||||
if c.Bool("json") {
|
if c.Bool("json") {
|
||||||
json.NewEncoder(os.Stdout).Encode(
|
json.NewEncoder(os.Stdout).Encode(
|
||||||
map[string]uint8{"heartRate": heartRate},
|
map[string]uint8{"heartRate": heartRate},
|
||||||
@@ -26,20 +26,21 @@ func watchHeart(c *cli.Context) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println(heartRate, "BPM")
|
fmt.Println(heartRate, "BPM")
|
||||||
}
|
}
|
||||||
}
|
case <-c.Done():
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func watchBattLevel(c *cli.Context) error {
|
func watchBattLevel(c *cli.Context) error {
|
||||||
battLevelCh, cancel, err := client.WatchBatteryLevel()
|
battLevelCh, err := client.WatchBatteryLevel(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
catchSignal(cancel)
|
for {
|
||||||
|
select {
|
||||||
for battLevel := range battLevelCh {
|
case battLevel := <-battLevelCh:
|
||||||
if c.Bool("json") {
|
if c.Bool("json") {
|
||||||
json.NewEncoder(os.Stdout).Encode(
|
json.NewEncoder(os.Stdout).Encode(
|
||||||
map[string]uint8{"battLevel": battLevel},
|
map[string]uint8{"battLevel": battLevel},
|
||||||
@@ -49,20 +50,21 @@ func watchBattLevel(c *cli.Context) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Printf("%d%%\n", battLevel)
|
fmt.Printf("%d%%\n", battLevel)
|
||||||
}
|
}
|
||||||
}
|
case <-c.Done():
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func watchStepCount(c *cli.Context) error {
|
func watchStepCount(c *cli.Context) error {
|
||||||
stepCountCh, cancel, err := client.WatchStepCount()
|
stepCountCh, err := client.WatchStepCount(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
catchSignal(cancel)
|
for {
|
||||||
|
select {
|
||||||
for stepCount := range stepCountCh {
|
case stepCount := <-stepCountCh:
|
||||||
if c.Bool("json") {
|
if c.Bool("json") {
|
||||||
json.NewEncoder(os.Stdout).Encode(
|
json.NewEncoder(os.Stdout).Encode(
|
||||||
map[string]uint32{"stepCount": stepCount},
|
map[string]uint32{"stepCount": stepCount},
|
||||||
@@ -72,20 +74,21 @@ func watchStepCount(c *cli.Context) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println(stepCount, "Steps")
|
fmt.Println(stepCount, "Steps")
|
||||||
}
|
}
|
||||||
}
|
case <-c.Done():
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func watchMotion(c *cli.Context) error {
|
func watchMotion(c *cli.Context) error {
|
||||||
motionCh, cancel, err := client.WatchMotion()
|
motionCh, err := client.WatchMotion(c.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
catchSignal(cancel)
|
for {
|
||||||
|
select {
|
||||||
for motionVals := range motionCh {
|
case motionVals := <-motionCh:
|
||||||
if c.Bool("json") {
|
if c.Bool("json") {
|
||||||
json.NewEncoder(os.Stdout).Encode(motionVals)
|
json.NewEncoder(os.Stdout).Encode(motionVals)
|
||||||
} else if c.Bool("shell") {
|
} else if c.Bool("shell") {
|
||||||
@@ -98,7 +101,8 @@ func watchMotion(c *cli.Context) error {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println(motionVals)
|
fmt.Println(motionVals)
|
||||||
}
|
}
|
||||||
}
|
case <-c.Done():
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,10 +28,15 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Create new label containing error text
|
// 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
|
// Create new dropdown containing error label
|
||||||
content.Add(widget.NewAccordion(
|
content.Add(widget.NewAccordion(
|
||||||
widget.NewAccordionItem("More Details", errLbl),
|
widget.NewAccordionItem("More Details", errEntry),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if fatal {
|
if fatal {
|
||||||
@@ -49,5 +54,4 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
|||||||
// Show error dialog
|
// Show error dialog
|
||||||
dialog.NewCustom("Error", "Ok", content, parent).Show()
|
dialog.NewCustom("Error", "Ok", content, parent).Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
163
cmd/itgui/firmware.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/storage"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"go.arsenm.dev/itd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
// Create select to chose between archive and files upgrade
|
||||||
|
typeSelect := widget.NewSelect([]string{"Archive", "Files"}, nil)
|
||||||
|
typeSelect.PlaceHolder = "Upgrade Type"
|
||||||
|
|
||||||
|
// Create map to store files
|
||||||
|
files := map[string]string{}
|
||||||
|
|
||||||
|
// Create and disable start button
|
||||||
|
startBtn := widget.NewButton("Start", nil)
|
||||||
|
startBtn.Disable()
|
||||||
|
|
||||||
|
// Create new file open dialog for archive
|
||||||
|
archiveDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || uc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer uc.Close()
|
||||||
|
// Set archive path in map
|
||||||
|
files[".zip"] = uc.URI().Path()
|
||||||
|
// Enable start button
|
||||||
|
startBtn.Enable()
|
||||||
|
}, w)
|
||||||
|
// Only allow .zip files
|
||||||
|
archiveDlg.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
|
||||||
|
// Create button to show dialog
|
||||||
|
archiveBtn := widget.NewButton("Select Archive (.zip)", archiveDlg.Show)
|
||||||
|
|
||||||
|
// Create new file open dialog for firmware image
|
||||||
|
imageDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || uc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer uc.Close()
|
||||||
|
|
||||||
|
// Set firmware image path in map
|
||||||
|
files[".bin"] = uc.URI().Path()
|
||||||
|
|
||||||
|
// If the init packet was already selected
|
||||||
|
_, datOk := files[".dat"]
|
||||||
|
if datOk {
|
||||||
|
// Enable start button
|
||||||
|
startBtn.Enable()
|
||||||
|
}
|
||||||
|
}, w)
|
||||||
|
// Only allow .bin files
|
||||||
|
imageDlg.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
|
||||||
|
// Create button to show dialog
|
||||||
|
imageBtn := widget.NewButton("Select Firmware (.bin)", imageDlg.Show)
|
||||||
|
|
||||||
|
// Create new file open dialog for init packet
|
||||||
|
initDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || uc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer uc.Close()
|
||||||
|
|
||||||
|
// Set init packet path in map
|
||||||
|
files[".dat"] = uc.URI().Path()
|
||||||
|
|
||||||
|
// If the firmware image was already selected
|
||||||
|
_, binOk := files[".bin"]
|
||||||
|
if binOk {
|
||||||
|
// Enable start button
|
||||||
|
startBtn.Enable()
|
||||||
|
}
|
||||||
|
}, w)
|
||||||
|
// Only allow .dat files
|
||||||
|
initDlg.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
|
||||||
|
// Create button to show dialog
|
||||||
|
initBtn := widget.NewButton("Select Init Packet (.dat)", initDlg.Show)
|
||||||
|
|
||||||
|
var upgType api.UpgradeType = 255
|
||||||
|
// When upgrade type changes
|
||||||
|
typeSelect.OnChanged = func(s string) {
|
||||||
|
// Delete all files from map
|
||||||
|
delete(files, ".bin")
|
||||||
|
delete(files, ".dat")
|
||||||
|
delete(files, ".zip")
|
||||||
|
// Hide all dialog buttons
|
||||||
|
imageBtn.Hide()
|
||||||
|
initBtn.Hide()
|
||||||
|
archiveBtn.Hide()
|
||||||
|
// Disable start button
|
||||||
|
startBtn.Disable()
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case "Files":
|
||||||
|
// Set file upgrade type
|
||||||
|
upgType = api.UpgradeTypeFiles
|
||||||
|
// Show firmware image and init packet buttons
|
||||||
|
imageBtn.Show()
|
||||||
|
initBtn.Show()
|
||||||
|
case "Archive":
|
||||||
|
// Set archive upgrade type
|
||||||
|
upgType = api.UpgradeTypeArchive
|
||||||
|
// Show archive button
|
||||||
|
archiveBtn.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Select archive by default
|
||||||
|
typeSelect.SetSelectedIndex(0)
|
||||||
|
|
||||||
|
// When start button pressed
|
||||||
|
startBtn.OnTapped = func() {
|
||||||
|
var args []string
|
||||||
|
// Append the appropriate files for upgrade type
|
||||||
|
switch upgType {
|
||||||
|
case api.UpgradeTypeArchive:
|
||||||
|
args = append(args, files[".zip"])
|
||||||
|
case api.UpgradeTypeFiles:
|
||||||
|
args = append(args, files[".dat"], files[".bin"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// If args are nil (invalid upgrade type)
|
||||||
|
if args == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new progress dialog
|
||||||
|
progress := newProgress(w)
|
||||||
|
// Start firmware upgrade
|
||||||
|
progressCh, err := client.FirmwareUpgrade(ctx, upgType, args...)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error performing firmware upgrade", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Show progress dialog
|
||||||
|
progress.Show()
|
||||||
|
// For every progress event
|
||||||
|
for progressEvt := range progressCh {
|
||||||
|
// Set progress bar values
|
||||||
|
progress.SetTotal(float64(progressEvt.Total))
|
||||||
|
progress.SetValue(float64(progressEvt.Sent))
|
||||||
|
}
|
||||||
|
// Hide progress dialog
|
||||||
|
progress.Hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.NewVBox(
|
||||||
|
layout.NewSpacer(),
|
||||||
|
typeSelect,
|
||||||
|
archiveBtn,
|
||||||
|
imageBtn,
|
||||||
|
initBtn,
|
||||||
|
startBtn,
|
||||||
|
layout.NewSpacer(),
|
||||||
|
)
|
||||||
|
}
|
||||||
360
cmd/itgui/fs.go
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/data/binding"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"go.arsenm.dev/itd/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fsTab(ctx context.Context, client *api.Client, w fyne.Window, opened chan struct{}) fyne.CanvasObject {
|
||||||
|
c := container.NewVBox()
|
||||||
|
|
||||||
|
// Create new binding to store current directory
|
||||||
|
cwdData := binding.NewString()
|
||||||
|
cwdData.Set("/")
|
||||||
|
|
||||||
|
// Create new list binding to store fs listing entries
|
||||||
|
lsData := binding.NewUntypedList()
|
||||||
|
|
||||||
|
// This goroutine waits until the fs tab is opened to
|
||||||
|
// request the listing from the watch
|
||||||
|
go func() {
|
||||||
|
// Wait for opened signal
|
||||||
|
<-opened
|
||||||
|
|
||||||
|
// Show loading pop up
|
||||||
|
loading := newLoadingPopUp(w)
|
||||||
|
loading.Show()
|
||||||
|
|
||||||
|
// Read root directory
|
||||||
|
ls, err := client.ReadDir(ctx, "/")
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error reading directory", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set ls binding
|
||||||
|
lsData.Set(lsToAny(ls))
|
||||||
|
|
||||||
|
// Hide loading pop up
|
||||||
|
loading.Hide()
|
||||||
|
}()
|
||||||
|
|
||||||
|
toolbar := widget.NewToolbar(
|
||||||
|
widget.NewToolbarAction(
|
||||||
|
theme.ViewRefreshIcon(),
|
||||||
|
func() {
|
||||||
|
refresh(ctx, cwdData, lsData, client, w, c)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
widget.NewToolbarAction(
|
||||||
|
theme.UploadIcon(),
|
||||||
|
func() {
|
||||||
|
// Create open dialog for file that will be uploaded
|
||||||
|
dlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || uc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get filepath and close
|
||||||
|
localPath := uc.URI().Path()
|
||||||
|
uc.Close()
|
||||||
|
|
||||||
|
// Create new entry to store filepath
|
||||||
|
filenameEntry := widget.NewEntry()
|
||||||
|
// Set entry text to the file name of the selected file
|
||||||
|
filenameEntry.SetText(filepath.Base(localPath))
|
||||||
|
// Create new dialog asking for the filename of the file to be stored on the watch
|
||||||
|
uploadDlg := dialog.NewForm("Upload", "Upload", "Cancel", []*widget.FormItem{
|
||||||
|
widget.NewFormItem("Filename", filenameEntry),
|
||||||
|
}, func(ok bool) {
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Get remote path by joining current directory with filename
|
||||||
|
remotePath := filepath.Join(cwd, filenameEntry.Text)
|
||||||
|
|
||||||
|
// Create new progress dialog
|
||||||
|
progressDlg := newProgress(w)
|
||||||
|
progressDlg.Show()
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
progressCh, err := client.Upload(ctx, remotePath, localPath)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error uploading file", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for progressEvt := range progressCh {
|
||||||
|
progressDlg.SetTotal(float64(progressEvt.Total))
|
||||||
|
progressDlg.SetValue(float64(progressEvt.Sent))
|
||||||
|
if progressEvt.Sent == progressEvt.Total {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close progress dialog
|
||||||
|
progressDlg.Hide()
|
||||||
|
|
||||||
|
// Add file to listing (avoids full refresh)
|
||||||
|
lsData.Append(api.FileInfo{
|
||||||
|
IsDir: false,
|
||||||
|
Name: filepath.Base(remotePath),
|
||||||
|
})
|
||||||
|
}, w)
|
||||||
|
uploadDlg.Show()
|
||||||
|
}, w)
|
||||||
|
dlg.Show()
|
||||||
|
|
||||||
|
},
|
||||||
|
),
|
||||||
|
widget.NewToolbarAction(
|
||||||
|
theme.FolderNewIcon(),
|
||||||
|
func() {
|
||||||
|
// Create new entry for filename
|
||||||
|
filenameEntry := widget.NewEntry()
|
||||||
|
// Create new dialog to ask for the filename
|
||||||
|
mkdirDialog := dialog.NewForm("Make Directory", "Create", "Cancel", []*widget.FormItem{
|
||||||
|
widget.NewFormItem("Filename", filenameEntry),
|
||||||
|
}, func(ok bool) {
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Get remote path by joining current directory and filename
|
||||||
|
remotePath := filepath.Join(cwd, filenameEntry.Text)
|
||||||
|
|
||||||
|
// Make directory
|
||||||
|
err := client.Mkdir(ctx, remotePath)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error creating directory", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add directory to listing (avoids full refresh)
|
||||||
|
lsData.Append(api.FileInfo{
|
||||||
|
IsDir: true,
|
||||||
|
Name: filepath.Base(remotePath),
|
||||||
|
})
|
||||||
|
}, w)
|
||||||
|
mkdirDialog.Show()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add listener to listing data to create the new items on the GUI
|
||||||
|
// whenever the listing changes
|
||||||
|
lsData.AddListener(binding.NewDataListener(func() {
|
||||||
|
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
|
||||||
|
c.Refresh()
|
||||||
|
}))
|
||||||
|
|
||||||
|
return container.NewBorder(
|
||||||
|
nil,
|
||||||
|
toolbar,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
container.NewVScroll(c),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeItems creates GUI objects from listing data
|
||||||
|
func makeItems(
|
||||||
|
ctx context.Context,
|
||||||
|
client *api.Client,
|
||||||
|
lsData binding.UntypedList,
|
||||||
|
cwdData binding.String,
|
||||||
|
w fyne.Window,
|
||||||
|
c *fyne.Container,
|
||||||
|
) []fyne.CanvasObject {
|
||||||
|
// Get listing data
|
||||||
|
ls, _ := lsData.Get()
|
||||||
|
|
||||||
|
// Create output slice with dame length as listing
|
||||||
|
out := make([]fyne.CanvasObject, len(ls))
|
||||||
|
for index, val := range ls {
|
||||||
|
// Assert value as file info
|
||||||
|
item := val.(api.FileInfo)
|
||||||
|
|
||||||
|
var icon fyne.Resource
|
||||||
|
// Decide which icon to use
|
||||||
|
if item.IsDir {
|
||||||
|
if item.Name == ".." {
|
||||||
|
icon = theme.NavigateBackIcon()
|
||||||
|
} else {
|
||||||
|
icon = theme.FolderIcon()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
icon = theme.FileIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new button with the decided icon and the item name
|
||||||
|
btn := widget.NewButtonWithIcon(item.Name, icon, nil)
|
||||||
|
// Align left
|
||||||
|
btn.Alignment = widget.ButtonAlignLeading
|
||||||
|
// Decide which callback function to use
|
||||||
|
if item.IsDir {
|
||||||
|
btn.OnTapped = func() {
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Join current directory with item name
|
||||||
|
cwd = filepath.Join(cwd, item.Name)
|
||||||
|
// Set new current directory
|
||||||
|
cwdData.Set(cwd)
|
||||||
|
// Refresh GUI to display new directory
|
||||||
|
refresh(ctx, cwdData, lsData, client, w, c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.OnTapped = func() {
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Join current directory with item name
|
||||||
|
remotePath := filepath.Join(cwd, item.Name)
|
||||||
|
// Create new save dialog
|
||||||
|
dlg := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
|
||||||
|
if err != nil || uc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get path of selected file
|
||||||
|
localPath := uc.URI().Path()
|
||||||
|
// Close WriteCloser (it's not needed)
|
||||||
|
uc.Close()
|
||||||
|
|
||||||
|
// Create new progress dialog
|
||||||
|
progressDlg := newProgress(w)
|
||||||
|
progressDlg.Show()
|
||||||
|
|
||||||
|
// Download file
|
||||||
|
progressCh, err := client.Download(ctx, localPath, remotePath)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error downloading file", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For every progress event
|
||||||
|
for progressEvt := range progressCh {
|
||||||
|
progressDlg.SetTotal(float64(progressEvt.Total))
|
||||||
|
progressDlg.SetValue(float64(progressEvt.Sent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close progress dialog
|
||||||
|
progressDlg.Hide()
|
||||||
|
}, w)
|
||||||
|
// Set filename to the item name
|
||||||
|
dlg.SetFileName(item.Name)
|
||||||
|
dlg.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Name == ".." {
|
||||||
|
out[index] = btn
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBtn := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
|
||||||
|
moveEntry := widget.NewEntry()
|
||||||
|
dlg := dialog.NewForm("Move", "Move", "Cancel", []*widget.FormItem{
|
||||||
|
widget.NewFormItem("New Path", moveEntry),
|
||||||
|
}, func(ok bool) {
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Join current directory with item name
|
||||||
|
oldPath := filepath.Join(cwd, item.Name)
|
||||||
|
|
||||||
|
// Rename file
|
||||||
|
err := client.Rename(ctx, oldPath, moveEntry.Text)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error renaming file", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh GUI
|
||||||
|
refresh(ctx, cwdData, lsData, client, w, c)
|
||||||
|
}, w)
|
||||||
|
dlg.Show()
|
||||||
|
})
|
||||||
|
|
||||||
|
removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Join current directory with item name
|
||||||
|
path := filepath.Join(cwd, item.Name)
|
||||||
|
|
||||||
|
// Remove file
|
||||||
|
err := client.Remove(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error removing file", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh GUI
|
||||||
|
refresh(ctx, cwdData, lsData, client, w, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add button to GUI component list
|
||||||
|
out[index] = container.NewBorder(
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
container.NewHBox(moveBtn, removeBtn),
|
||||||
|
btn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh(
|
||||||
|
ctx context.Context,
|
||||||
|
cwdData binding.String,
|
||||||
|
lsData binding.UntypedList,
|
||||||
|
client *api.Client,
|
||||||
|
w fyne.Window,
|
||||||
|
c *fyne.Container,
|
||||||
|
) {
|
||||||
|
// Create and show new loading pop up
|
||||||
|
loading := newLoadingPopUp(w)
|
||||||
|
loading.Show()
|
||||||
|
// Close pop up at the end of the function
|
||||||
|
defer loading.Hide()
|
||||||
|
|
||||||
|
// Get current directory
|
||||||
|
cwd, _ := cwdData.Get()
|
||||||
|
// Read directory
|
||||||
|
ls, err := client.ReadDir(ctx, cwd)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error reading directory", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set new listing data
|
||||||
|
lsData.Set(lsToAny(ls))
|
||||||
|
// Create new GUI objects
|
||||||
|
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
|
||||||
|
// Refresh GUI
|
||||||
|
c.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func lsToAny(ls []api.FileInfo) []interface{} {
|
||||||
|
out := make([]interface{}, len(ls)-1)
|
||||||
|
for i, e := range ls {
|
||||||
|
// Skip first element as it is always "."
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[i-1] = e
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
150
cmd/itgui/graph.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"image/color"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
"fyne.io/x/fyne/widget/charts"
|
||||||
|
"go.arsenm.dev/itd/api"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func graphTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
|
// Get user configuration directory
|
||||||
|
userCfgDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cfgDir := filepath.Join(userCfgDir, "itd")
|
||||||
|
dbPath := filepath.Join(cfgDir, "metrics.db")
|
||||||
|
|
||||||
|
// If stat on database returns error, return nil
|
||||||
|
if _, err := os.Stat(dbPath); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database
|
||||||
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get heart rate data and create chart
|
||||||
|
heartRateData := getData(db, "bpm", "heartRate")
|
||||||
|
heartRate := newLineChartData(nil, heartRateData)
|
||||||
|
|
||||||
|
// Get step count data and create chart
|
||||||
|
stepCountData := getData(db, "steps", "stepCount")
|
||||||
|
stepCount := newLineChartData(nil, stepCountData)
|
||||||
|
|
||||||
|
// Get battery level data and create chart
|
||||||
|
battLevelData := getData(db, "percent", "battLevel")
|
||||||
|
battLevel := newLineChartData(nil, battLevelData)
|
||||||
|
|
||||||
|
// Get motion data
|
||||||
|
motionData := getMotionData(db)
|
||||||
|
// Create chart for each coordinate
|
||||||
|
xChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorRed), motionData["X"])
|
||||||
|
yChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorGreen), motionData["Y"])
|
||||||
|
zChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorBlue), motionData["Z"])
|
||||||
|
|
||||||
|
// Create new max container with all the charts
|
||||||
|
motion := container.NewMax(xChart, yChart, zChart)
|
||||||
|
|
||||||
|
// Create tabs for charts
|
||||||
|
chartTabs := container.NewAppTabs(
|
||||||
|
container.NewTabItem("Heart Rate", heartRate),
|
||||||
|
container.NewTabItem("Step Count", stepCount),
|
||||||
|
container.NewTabItem("Battery Level", battLevel),
|
||||||
|
container.NewTabItem("Motion", motion),
|
||||||
|
)
|
||||||
|
// Place tabs on left
|
||||||
|
chartTabs.SetTabLocation(container.TabLocationLeading)
|
||||||
|
return chartTabs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLineChartData(col color.Color, data []float64) *charts.LineChart {
|
||||||
|
// Create new line chart
|
||||||
|
lc := charts.NewLineChart(nil)
|
||||||
|
setOpts(lc, col)
|
||||||
|
// If no data, make the stroke transparent
|
||||||
|
if len(data) == 0 {
|
||||||
|
lc.Options().StrokeColor = color.RGBA{0, 0, 0, 0}
|
||||||
|
}
|
||||||
|
// Set data
|
||||||
|
lc.SetData(data)
|
||||||
|
return lc
|
||||||
|
}
|
||||||
|
|
||||||
|
func setOpts(lc *charts.LineChart, col color.Color) {
|
||||||
|
// Get pointer to options
|
||||||
|
opts := lc.Options()
|
||||||
|
// Set fill color to transparent
|
||||||
|
opts.FillColor = color.RGBA{0, 0, 0, 0}
|
||||||
|
// Set stroke width
|
||||||
|
opts.StrokeWidth = 2
|
||||||
|
// If color provided
|
||||||
|
if col != nil {
|
||||||
|
// Set stroke color
|
||||||
|
opts.StrokeColor = col
|
||||||
|
} else {
|
||||||
|
// Set stroke color to orange primary color
|
||||||
|
opts.StrokeColor = theme.PrimaryColorNamed(theme.ColorOrange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getData(db *sql.DB, field, table string) []float64 {
|
||||||
|
// Get data from database
|
||||||
|
rows, err := db.Query("SELECT " + field + " FROM " + table + " ORDER BY time;")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []float64
|
||||||
|
for rows.Next() {
|
||||||
|
var val int64
|
||||||
|
// Scan data into int
|
||||||
|
err := rows.Scan(&val)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to float64 and append to data slice
|
||||||
|
out = append(out, float64(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMotionData(db *sql.DB) map[string][]float64 {
|
||||||
|
// Get data from database
|
||||||
|
rows, err := db.Query("SELECT X, Y, Z FROM motion ORDER BY time;")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := map[string][]float64{}
|
||||||
|
for rows.Next() {
|
||||||
|
var x, y, z int64
|
||||||
|
// Scan data into ints
|
||||||
|
err := rows.Scan(&x, &y, &z)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to float64 and append to appropriate slice
|
||||||
|
out["X"] = append(out["X"], float64(x))
|
||||||
|
out["Y"] = append(out["Y"], float64(y))
|
||||||
|
out["Z"] = append(out["Z"], float64(z))
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -1,123 +1,86 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"go.arsenm.dev/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func infoTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
infoLayout := container.NewVBox(
|
c := container.NewVBox()
|
||||||
// Add rectangle for a bit of padding
|
|
||||||
canvas.NewRectangle(color.Transparent),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create label for heart rate
|
// Create titled text for heart rate
|
||||||
heartRateLbl := newText("0 BPM", 24)
|
heartRateText := newTitledText("Heart Rate", "0 BPM")
|
||||||
// Creae container to store heart rate section
|
c.Add(heartRateText)
|
||||||
heartRateSect := container.NewVBox(
|
// Watch heart rate
|
||||||
newText("Heart Rate", 12),
|
heartRateCh, err := client.WatchHeartRate(ctx)
|
||||||
heartRateLbl,
|
|
||||||
canvas.NewLine(theme.ShadowColor()),
|
|
||||||
)
|
|
||||||
infoLayout.Add(heartRateSect)
|
|
||||||
|
|
||||||
heartRateCh, cancel, err := client.WatchHeartRate()
|
|
||||||
if err != nil {
|
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() {
|
go func() {
|
||||||
|
// For every heart rate sample
|
||||||
for heartRate := range heartRateCh {
|
for heartRate := range heartRateCh {
|
||||||
// Change text of heart rate label
|
// Set body of titled text
|
||||||
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate)
|
heartRateText.SetBody(fmt.Sprintf("%d BPM", heartRate))
|
||||||
// Refresh label
|
|
||||||
heartRateLbl.Refresh()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Create label for heart rate
|
// Create titled text for battery level
|
||||||
stepCountLbl := newText("0 Steps", 24)
|
battLevelText := newTitledText("Battery Level", "0%")
|
||||||
// Creae container to store heart rate section
|
c.Add(battLevelText)
|
||||||
stepCountSect := container.NewVBox(
|
// Watch battery level
|
||||||
newText("Step Count", 12),
|
battLevelCh, err := client.WatchBatteryLevel(ctx)
|
||||||
stepCountLbl,
|
|
||||||
canvas.NewLine(theme.ShadowColor()),
|
|
||||||
)
|
|
||||||
infoLayout.Add(stepCountSect)
|
|
||||||
|
|
||||||
stepCountCh, cancel, err := client.WatchStepCount()
|
|
||||||
if err != nil {
|
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() {
|
go func() {
|
||||||
|
// For every battery level sample
|
||||||
for battLevel := range battLevelCh {
|
for battLevel := range battLevelCh {
|
||||||
// Change text of battery level label
|
// Set body of titled text
|
||||||
battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel)
|
battLevelText.SetBody(fmt.Sprintf("%d%%", battLevel))
|
||||||
// Refresh label
|
|
||||||
battLevelLbl.Refresh()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
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 {
|
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(
|
// Create new titled text for address
|
||||||
newText("Firmware Version", 12),
|
addressText := newTitledText("Address", "")
|
||||||
newText(fwVerString, 24),
|
c.Add(addressText)
|
||||||
canvas.NewLine(theme.ShadowColor()),
|
// Get address
|
||||||
)
|
address, err := client.Address(ctx)
|
||||||
infoLayout.Add(fwVer)
|
|
||||||
|
|
||||||
btAddrString, err := client.Address()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
guiErr(err, "Error getting address", true, w)
|
||||||
}
|
}
|
||||||
|
// Set body of titled text
|
||||||
|
addressText.SetBody(address)
|
||||||
|
|
||||||
btAddr := container.NewVBox(
|
// Create new titled text for version
|
||||||
newText("Bluetooth Address", 12),
|
versionText := newTitledText("Version", "")
|
||||||
newText(btAddrString, 24),
|
c.Add(versionText)
|
||||||
canvas.NewLine(theme.ShadowColor()),
|
// Get version
|
||||||
)
|
version, err := client.Version(ctx)
|
||||||
infoLayout.Add(btAddr)
|
if err != nil {
|
||||||
|
guiErr(err, "Error getting version", true, w)
|
||||||
return infoLayout
|
|
||||||
}
|
}
|
||||||
|
// Set body of titled text
|
||||||
|
versionText.SetBody(version)
|
||||||
|
|
||||||
func newText(t string, size float32) *canvas.Text {
|
return container.NewVScroll(c)
|
||||||
text := canvas.NewText(t, theme.ForegroundColor())
|
|
||||||
text.TextSize = size
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
cmd/itgui/loading.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newLoadingPopUp(w fyne.Window) *widget.PopUp {
|
||||||
|
pb := widget.NewProgressBarInfinite()
|
||||||
|
rect := canvas.NewRectangle(color.Transparent)
|
||||||
|
rect.SetMinSize(fyne.NewSize(200, 0))
|
||||||
|
|
||||||
|
return widget.NewModalPopUp(
|
||||||
|
container.NewMax(rect, pb),
|
||||||
|
w.Canvas(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,43 +1,60 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"go.arsenm.dev/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var onClose []func()
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create new app
|
|
||||||
a := app.New()
|
a := app.New()
|
||||||
// Create new window with title "itgui"
|
w := a.NewWindow("itgui")
|
||||||
window := a.NewWindow("itgui")
|
|
||||||
window.SetOnClosed(func() {
|
|
||||||
for _, closeFn := range onClose {
|
|
||||||
closeFn()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// 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)
|
client, err := api.New(api.DefaultAddr)
|
||||||
if err != nil {
|
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(
|
tabs := container.NewAppTabs(
|
||||||
container.NewTabItem("Info", infoTab(window, client)),
|
container.NewTabItem("Info", infoTab(ctx, client, w)),
|
||||||
container.NewTabItem("Motion", motionTab(window, client)),
|
container.NewTabItem("Motion", motionTab(ctx, client, w)),
|
||||||
container.NewTabItem("Notify", notifyTab(window, client)),
|
container.NewTabItem("Notify", notifyTab(ctx, client, w)),
|
||||||
container.NewTabItem("Set Time", timeTab(window, client)),
|
container.NewTabItem("FS", fsTab(ctx, client, w, fsOpened)),
|
||||||
container.NewTabItem("Upgrade", upgradeTab(window, client)),
|
container.NewTabItem("Time", timeTab(ctx, client, w)),
|
||||||
|
container.NewTabItem("Firmware", firmwareTab(ctx, client, w)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set tabs as window content
|
metricsTab := graphTab(ctx, client, w)
|
||||||
window.SetContent(tabs)
|
if metricsTab != nil {
|
||||||
// Show window and run app
|
tabs.Append(container.NewTabItem("Metrics", metricsTab))
|
||||||
window.ShowAndRun()
|
}
|
||||||
|
|
||||||
|
// When a tab is selected
|
||||||
|
tabs.OnSelected = func(ti *container.TabItem) {
|
||||||
|
// If the tab's name is FS
|
||||||
|
if ti.Text == "FS" {
|
||||||
|
// Signal fsOpened only once
|
||||||
|
fsOnce.Do(func() {
|
||||||
|
fsOpened <- struct{}{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel context on close
|
||||||
|
w.SetOnClosed(cancel)
|
||||||
|
// Set content and show window
|
||||||
|
w.SetContent(tabs)
|
||||||
|
w.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,62 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"context"
|
||||||
"strconv"
|
"fmt"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
"go.arsenm.dev/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func motionTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
// Create label for heart rate
|
// Create titledText for each coordinate
|
||||||
xCoordLbl := newText("0", 24)
|
xText := newTitledText("X Coordinate", "0")
|
||||||
// Creae container to store heart rate section
|
yText := newTitledText("Y Coordinate", "0")
|
||||||
xCoordSect := container.NewVBox(
|
zText := newTitledText("Z Coordinate", "0")
|
||||||
newText("X Coordinate", 12),
|
|
||||||
xCoordLbl,
|
|
||||||
canvas.NewLine(theme.ShadowColor()),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create label for heart rate
|
var ctxCancel func()
|
||||||
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()),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create variable to keep track of whether motion started
|
// Create start button
|
||||||
started := false
|
toggleBtn := widget.NewButton("Start", nil)
|
||||||
|
// Set button's on tapped callback
|
||||||
// Create button to stop motion
|
toggleBtn.OnTapped = func() {
|
||||||
stopBtn := widget.NewButton("Stop", nil)
|
switch toggleBtn.Text {
|
||||||
// Create button to start motion
|
case "Start":
|
||||||
startBtn := widget.NewButton("Start", func() {
|
// Create new context for motion
|
||||||
// if motion is started
|
motionCtx, cancel := context.WithCancel(ctx)
|
||||||
if started {
|
// Set ctxCancel to function so that stop button can run it
|
||||||
// Do nothing
|
ctxCancel = cancel
|
||||||
return
|
// Watch motion
|
||||||
}
|
motionCh, err := client.WatchMotion(motionCtx)
|
||||||
// Set motion started
|
|
||||||
started = true
|
|
||||||
// Watch motion values
|
|
||||||
motionCh, cancel, err := client.WatchMotion()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
guiErr(err, "Error getting heart rate channel", true, parent)
|
guiErr(err, "Error watching motion", false, w)
|
||||||
}
|
|
||||||
// Create done channel
|
|
||||||
done := make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return
|
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()
|
|
||||||
}
|
}
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// Create stop function
|
// Set button text to "Stop"
|
||||||
stopBtn.OnTapped = func() {
|
toggleBtn.SetText("Stop")
|
||||||
done <- struct{}{}
|
case "Stop":
|
||||||
started = false
|
// Cancel motion context
|
||||||
cancel()
|
ctxCancel()
|
||||||
|
// Set button text to "Start"
|
||||||
|
toggleBtn.SetText("Start")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
return container.NewVScroll(container.NewVBox(
|
||||||
// Run stop button function on close if possible
|
toggleBtn,
|
||||||
onClose = append(onClose, func() {
|
xText,
|
||||||
if stopBtn.OnTapped != nil {
|
yText,
|
||||||
stopBtn.OnTapped()
|
zText,
|
||||||
}
|
))
|
||||||
})
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
@@ -8,30 +10,31 @@ import (
|
|||||||
"go.arsenm.dev/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
// Create new entry for notification title
|
c := container.NewVBox()
|
||||||
|
c.Add(layout.NewSpacer())
|
||||||
|
|
||||||
|
// Create new entry for title
|
||||||
titleEntry := widget.NewEntry()
|
titleEntry := widget.NewEntry()
|
||||||
titleEntry.SetPlaceHolder("Title")
|
titleEntry.SetPlaceHolder("Title")
|
||||||
|
c.Add(titleEntry)
|
||||||
|
|
||||||
// Create multiline entry for notification body
|
// Create new multiline entry for body
|
||||||
bodyEntry := widget.NewMultiLineEntry()
|
bodyEntry := widget.NewMultiLineEntry()
|
||||||
bodyEntry.SetPlaceHolder("Body")
|
bodyEntry.SetPlaceHolder("Body")
|
||||||
|
c.Add(bodyEntry)
|
||||||
|
|
||||||
// Create new button to send notification
|
// Create new send button
|
||||||
sendBtn := widget.NewButton("Send", func() {
|
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 {
|
if err != nil {
|
||||||
guiErr(err, "Error sending notification", false, parent)
|
guiErr(err, "Error sending notification", false, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
c.Add(sendBtn)
|
||||||
|
|
||||||
// Return new container containing all elements
|
c.Add(layout.NewSpacer())
|
||||||
return container.NewVBox(
|
return container.NewVScroll(c)
|
||||||
layout.NewSpacer(),
|
|
||||||
titleEntry,
|
|
||||||
bodyEntry,
|
|
||||||
sendBtn,
|
|
||||||
layout.NewSpacer(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
cmd/itgui/progress.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
type progress struct {
|
||||||
|
lbl *widget.Label
|
||||||
|
pb *widget.ProgressBar
|
||||||
|
*widget.PopUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgress(w fyne.Window) progress {
|
||||||
|
out := progress{}
|
||||||
|
|
||||||
|
// Create label to show how many bytes transfered and center it
|
||||||
|
out.lbl = widget.NewLabel("0 / 0 B")
|
||||||
|
out.lbl.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
// Create new progress bar
|
||||||
|
out.pb = widget.NewProgressBar()
|
||||||
|
|
||||||
|
// Create new rectangle to set the size of the popup
|
||||||
|
sizeRect := canvas.NewRectangle(color.Transparent)
|
||||||
|
sizeRect.SetMinSize(fyne.NewSize(300, 50))
|
||||||
|
|
||||||
|
// Create vbox for label and progress bar
|
||||||
|
l := container.NewVBox(out.lbl, out.pb)
|
||||||
|
// Create popup
|
||||||
|
out.PopUp = widget.NewModalPopUp(container.NewMax(l, sizeRect), w.Canvas())
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p progress) SetTotal(v float64) {
|
||||||
|
p.pb.Max = v
|
||||||
|
p.pb.Refresh()
|
||||||
|
p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", p.pb.Value, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p progress) SetValue(v float64) {
|
||||||
|
p.pb.SetValue(v)
|
||||||
|
p.lbl.SetText(fmt.Sprintf("%.0f / %.0f B", v, p.pb.Max))
|
||||||
|
}
|
||||||
BIN
cmd/itgui/screenshots/firmware.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
cmd/itgui/screenshots/fs.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
cmd/itgui/screenshots/info.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
cmd/itgui/screenshots/metrics.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
cmd/itgui/screenshots/mkdir.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
cmd/itgui/screenshots/motion.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
cmd/itgui/screenshots/notify.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
cmd/itgui/screenshots/progress.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
cmd/itgui/screenshots/time.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
@@ -10,51 +11,47 @@ import (
|
|||||||
"go.arsenm.dev/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func timeTab(parent fyne.Window, client *api.Client) *fyne.Container {
|
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
|
||||||
// Create new entry for time string
|
c := container.NewVBox()
|
||||||
|
c.Add(layout.NewSpacer())
|
||||||
|
|
||||||
|
// Create entry for time string
|
||||||
timeEntry := widget.NewEntry()
|
timeEntry := widget.NewEntry()
|
||||||
// Set text to current time formatter properly
|
|
||||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||||
|
timeEntry.SetPlaceHolder("RFC1123")
|
||||||
|
|
||||||
// Create button to set current time
|
// Create button to set current time
|
||||||
currentBtn := widget.NewButton("Set Current", func() {
|
setCurrentBtn := widget.NewButton("Set current time", func() {
|
||||||
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
// Set current time
|
||||||
setTime(client, true)
|
err := client.SetTime(ctx, time.Now())
|
||||||
})
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
guiErr(err, "Error parsing time string", false, parent)
|
guiErr(err, "Error setting time", false, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Set time to parsed time
|
// Set time entry to current time
|
||||||
setTime(client, false, parsedTime)
|
timeEntry.SetText(time.Now().Format(time.RFC1123))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return new container with all elements centered
|
// Create button to set time from entry
|
||||||
return container.NewVBox(
|
setBtn := widget.NewButton("Set", func() {
|
||||||
layout.NewSpacer(),
|
// Parse RFC1123 time string in entry
|
||||||
timeEntry,
|
newTime, err := time.Parse(time.RFC1123, timeEntry.Text)
|
||||||
currentBtn,
|
|
||||||
timeBtn,
|
|
||||||
layout.NewSpacer(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.SetTime(time.Now())
|
|
||||||
} else {
|
|
||||||
err = client.SetTime(t[0])
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
guiErr(err, "Error parsing time string", false, w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return nil
|
// Set time from parsed string
|
||||||
|
err = client.SetTime(ctx, newTime)
|
||||||
|
if err != nil {
|
||||||
|
guiErr(err, "Error setting time", false, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Add(timeEntry)
|
||||||
|
c.Add(setBtn)
|
||||||
|
c.Add(setCurrentBtn)
|
||||||
|
|
||||||
|
c.Add(layout.NewSpacer())
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
35
cmd/itgui/titledText.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
type titledText struct {
|
||||||
|
*widget.RichText
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTitledText(title, text string) titledText {
|
||||||
|
titleStyle := widget.RichTextStyleHeading
|
||||||
|
titleStyle.TextStyle.Bold = false
|
||||||
|
return titledText{
|
||||||
|
widget.NewRichText(
|
||||||
|
&widget.TextSegment{
|
||||||
|
Style: widget.RichTextStyleParagraph,
|
||||||
|
Text: title,
|
||||||
|
},
|
||||||
|
&widget.TextSegment{
|
||||||
|
Style: titleStyle,
|
||||||
|
Text: text,
|
||||||
|
},
|
||||||
|
&widget.SeparatorSegment{},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t titledText) SetTitle(s string) {
|
||||||
|
t.RichText.Segments[0].(*widget.TextSegment).Text = s
|
||||||
|
t.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t titledText) SetBody(s string) {
|
||||||
|
t.RichText.Segments[1].(*widget.TextSegment).Text = s
|
||||||
|
t.Refresh()
|
||||||
|
}
|
||||||
@@ -1,180 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = api.UpgradeTypeArchive
|
|
||||||
files = append(files, archivePath)
|
|
||||||
case "Files":
|
|
||||||
fwUpgType = api.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()
|
|
||||||
|
|
||||||
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 int64(event.Sent) == event.Total {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide progress dialog after completion
|
|
||||||
progressDlg.Hide()
|
|
||||||
|
|
||||||
// Reset screen to default
|
|
||||||
upgradeTypeSelect.SetSelectedIndex(0)
|
|
||||||
firmwareBtn.SetText("Select firmware (.bin)")
|
|
||||||
initPktBtn.SetText("Select init packet (.dat)")
|
|
||||||
archiveBtn.SetText("Select archive (.zip)")
|
|
||||||
firmwarePath = ""
|
|
||||||
initPktPath = ""
|
|
||||||
archivePath = ""
|
|
||||||
|
|
||||||
dialog.NewInformation(
|
|
||||||
"Upgrade Complete",
|
|
||||||
"The firmware was transferred successfully.\nRemember to validate the firmware in InfiniTime settings.",
|
|
||||||
parent,
|
|
||||||
).Show()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return container containing all elements
|
|
||||||
return container.NewVBox(
|
|
||||||
layout.NewSpacer(),
|
|
||||||
upgradeTypeSelect,
|
|
||||||
archiveBtn,
|
|
||||||
firmwareBtn,
|
|
||||||
initPktBtn,
|
|
||||||
startBtn,
|
|
||||||
layout.NewSpacer(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
31
config.go
@@ -13,22 +13,47 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cfgDir string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Set up logger
|
// Set up logger
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
|
|
||||||
// Get user's configuration directory
|
// Get user's configuration directory
|
||||||
cfgDir, err := os.UserConfigDir()
|
userCfgDir, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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
|
// Set config defaults
|
||||||
setCfgDefaults()
|
setCfgDefaults()
|
||||||
|
|
||||||
// Load config files
|
// Load config files
|
||||||
etcProvider := file.Provider("/etc/itd.toml")
|
etcProvider := file.Provider("/etc/itd.toml")
|
||||||
cfgProvider := file.Provider(filepath.Join(cfgDir, "itd.toml"))
|
cfgProvider := file.Provider(cfgPath)
|
||||||
k.Load(etcProvider, toml.Parser())
|
k.Load(etcProvider, toml.Parser())
|
||||||
k.Load(cfgProvider, toml.Parser())
|
k.Load(cfgProvider, toml.Parser())
|
||||||
|
|
||||||
@@ -55,6 +80,8 @@ func cfgWatch(provider *file.File) {
|
|||||||
|
|
||||||
func setCfgDefaults() {
|
func setCfgDefaults() {
|
||||||
k.Load(confmap.Provider(map[string]interface{}{
|
k.Load(confmap.Provider(map[string]interface{}{
|
||||||
|
"bluetooth.adapter": "hci0",
|
||||||
|
|
||||||
"socket.path": "/tmp/itd/socket",
|
"socket.path": "/tmp/itd/socket",
|
||||||
|
|
||||||
"conn.reconnect": true,
|
"conn.reconnect": true,
|
||||||
|
|||||||
14
dbus.go
@@ -1,10 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/godbus/dbus/v5"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
func newSystemBusConn() (*dbus.Conn, error) {
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSystemBusConn(ctx context.Context) (*dbus.Conn, error) {
|
||||||
// Connect to dbus session bus
|
// Connect to dbus session bus
|
||||||
conn, err := dbus.SystemBusPrivate()
|
conn, err := dbus.SystemBusPrivate(dbus.WithContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -19,9 +23,9 @@ func newSystemBusConn() (*dbus.Conn, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSessionBusConn() (*dbus.Conn, error) {
|
func newSessionBusConn(ctx context.Context) (*dbus.Conn, error) {
|
||||||
// Connect to dbus session bus
|
// Connect to dbus session bus
|
||||||
conn, err := dbus.SessionBusPrivate()
|
conn, err := dbus.SessionBusPrivate(dbus.WithContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
111
go.mod
@@ -1,122 +1,77 @@
|
|||||||
module gitea.arsenm.dev/cpyarger/itd
|
module go.arsenm.dev/itd
|
||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
|
replace fyne.io/x/fyne => github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.1.2
|
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/cheggaaa/pb/v3 v3.0.8
|
||||||
github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b
|
github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b
|
||||||
github.com/godbus/dbus/v5 v5.0.6
|
github.com/godbus/dbus/v5 v5.0.6
|
||||||
github.com/google/uuid v1.3.0
|
|
||||||
github.com/knadh/koanf v1.4.0
|
github.com/knadh/koanf v1.4.0
|
||||||
github.com/mattn/go-isatty v0.0.14
|
github.com/mattn/go-isatty v0.0.14
|
||||||
github.com/mozillazg/go-pinyin v0.19.0
|
github.com/mozillazg/go-pinyin v0.19.0
|
||||||
github.com/rs/zerolog v1.26.1
|
github.com/rs/zerolog v1.26.1
|
||||||
github.com/smallnest/rpcx v1.7.4
|
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
github.com/vmihailenco/msgpack/v5 v5.3.5
|
go.arsenm.dev/infinitime v0.0.0-20221016193942-01970b2bb770
|
||||||
gitea.arsenm.dev/Arsen6331/infinitime v0.0.0-20220424030849-6c3f1b14c948
|
go.arsenm.dev/lrpc v0.0.0-20220513001344-3bcc01fdb6a0
|
||||||
golang.org/x/text v0.3.7
|
golang.org/x/text v0.3.7
|
||||||
|
modernc.org/sqlite v1.17.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
github.com/VividCortex/ewma v1.1.1 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
|
||||||
github.com/apache/thrift v0.16.0 // indirect
|
|
||||||
github.com/armon/go-metrics v0.3.10 // indirect
|
|
||||||
github.com/cenk/backoff v2.2.1+incompatible // indirect
|
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
|
||||||
github.com/cheekybits/genny v1.0.0 // indirect
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 // indirect
|
github.com/fatih/color v1.10.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/edwingeng/doublejump v0.0.0-20210724020454-c82f1bcb3280 // indirect
|
|
||||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
|
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
|
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.4.0 // 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/gl v0.0.0-20211210172815-726fda9656d6 // indirect
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 // indirect
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534 // indirect
|
|
||||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
|
||||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
|
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
|
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/hashicorp/consul/api v1.12.0 // indirect
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
|
||||||
github.com/hashicorp/go-hclog v1.2.0 // indirect
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
|
||||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
|
||||||
github.com/hashicorp/serf v0.9.7 // indirect
|
|
||||||
github.com/juju/ratelimit v1.0.1 // indirect
|
|
||||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
|
||||||
github.com/kavu/go_reuseport v1.5.0 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
|
||||||
github.com/klauspost/reedsolomon v1.9.16 // indirect
|
|
||||||
github.com/lucas-clemente/quic-go v0.27.0 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/miekg/dns v1.1.48 // indirect
|
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a // indirect
|
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 // indirect
|
||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
|
||||||
github.com/philhofer/fwd v1.1.1 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/rpcxio/libkv v0.5.1-0.20210420120011-1fceaedca8a5 // indirect
|
|
||||||
github.com/rs/cors v1.8.2 // indirect
|
|
||||||
github.com/rubyist/circuitbreaker v2.2.1+incompatible // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||||
github.com/samuel/go-zookeeper v0.0.0-20201211165307-7117e9ea2414 // indirect
|
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
github.com/smallnest/quick v0.0.0-20220103065406-780def6371e6 // indirect
|
|
||||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
|
||||||
github.com/srwiley/oksvg v0.0.0-20211120171407-1837d6608d8c // indirect
|
github.com/srwiley/oksvg v0.0.0-20211120171407-1837d6608d8c // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
|
||||||
github.com/stretchr/testify v1.7.1 // indirect
|
github.com/stretchr/testify v1.7.1 // indirect
|
||||||
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||||
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
|
|
||||||
github.com/tinylib/msgp v1.1.6 // indirect
|
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fastrand v1.1.0 // indirect
|
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xtaci/kcp-go v5.4.20+incompatible // indirect
|
|
||||||
github.com/yuin/goldmark v1.4.4 // indirect
|
github.com/yuin/goldmark v1.4.4 // indirect
|
||||||
go.opentelemetry.io/otel v1.6.3 // indirect
|
|
||||||
go.opentelemetry.io/otel/trace v1.6.3 // indirect
|
|
||||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect
|
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
golang.org/x/mod v0.4.2 // indirect
|
||||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||||
golang.org/x/tools v0.1.10 // indirect
|
golang.org/x/tools v0.1.7 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // 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
|
||||||
)
|
)
|
||||||
|
|||||||
19
itd.toml
@@ -1,6 +1,25 @@
|
|||||||
|
[bluetooth]
|
||||||
|
adapter = "hci0"
|
||||||
|
|
||||||
[socket]
|
[socket]
|
||||||
path = "/tmp/itd/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]
|
[conn]
|
||||||
reconnect = true
|
reconnect = true
|
||||||
|
|
||||||
|
|||||||
40
main.go
@@ -19,11 +19,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gen2brain/dlgs"
|
"github.com/gen2brain/dlgs"
|
||||||
@@ -31,7 +34,7 @@ import (
|
|||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gitea.arsenm.dev/cpyarger/itd"
|
"go.arsenm.dev/infinitime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var k = koanf.New(".")
|
var k = koanf.New(".")
|
||||||
@@ -60,7 +63,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize infinitime library
|
// Initialize infinitime library
|
||||||
infinitime.Init()
|
infinitime.Init(k.String("bluetooth.adapter"))
|
||||||
// Cleanly exit after function
|
// Cleanly exit after function
|
||||||
defer infinitime.Exit()
|
defer infinitime.Exit()
|
||||||
|
|
||||||
@@ -74,8 +77,24 @@ func main() {
|
|||||||
LogLevel: level,
|
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
|
// Connect to InfiniTime with default options
|
||||||
dev, err := infinitime.Connect(opts)
|
dev, err := infinitime.Connect(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Error connecting to InfiniTime")
|
log.Fatal().Err(err).Msg("Error connecting to InfiniTime")
|
||||||
}
|
}
|
||||||
@@ -136,29 +155,34 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start control socket
|
// Start control socket
|
||||||
err = initCallNotifs(dev)
|
err = initCallNotifs(ctx, dev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error initializing call notifications")
|
log.Error().Err(err).Msg("Error initializing call notifications")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize notification relay
|
// Initialize notification relay
|
||||||
err = initNotifRelay(dev)
|
err = initNotifRelay(ctx, dev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error initializing notification relay")
|
log.Error().Err(err).Msg("Error initializing notification relay")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initializa weather
|
// Initializa weather
|
||||||
err = initWeather(dev)
|
err = initWeather(ctx, dev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error initializing weather")
|
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
|
// Start control socket
|
||||||
err = startSocket(dev)
|
err = startSocket(ctx, dev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error starting socket")
|
log.Error().Err(err).Msg("Error starting socket")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block forever
|
// Block forever
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|||||||
131
metrics.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"go.arsenm.dev/infinitime"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initMetrics(ctx context.Context, dev *infinitime.Device) error {
|
||||||
|
// If metrics disabled, return nil
|
||||||
|
if !k.Bool("metrics.enabled") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open metrics database
|
||||||
|
db, err := sql.Open("sqlite", filepath.Join(cfgDir, "metrics.db"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create heartRate table
|
||||||
|
_, err = db.Exec("CREATE TABLE IF NOT EXISTS heartRate(time INT, bpm INT);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create stepCount table
|
||||||
|
_, err = db.Exec("CREATE TABLE IF NOT EXISTS stepCount(time INT, steps INT);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create battLevel table
|
||||||
|
_, err = db.Exec("CREATE TABLE IF NOT EXISTS battLevel(time INT, percent INT);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create motion table
|
||||||
|
_, err = db.Exec("CREATE TABLE IF NOT EXISTS motion(time INT, X INT, Y INT, Z INT);")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If heart rate metrics enabled in config
|
||||||
|
if k.Bool("metrics.heartRate.enabled") {
|
||||||
|
// Watch heart rate
|
||||||
|
heartRateCh, err := dev.WatchHeartRate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// For every heart rate sample
|
||||||
|
for heartRate := range heartRateCh {
|
||||||
|
// Get current time
|
||||||
|
unixTime := time.Now().UnixNano()
|
||||||
|
// Insert sample and time into database
|
||||||
|
db.Exec("INSERT INTO heartRate VALUES (?, ?);", unixTime, heartRate)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If step count metrics enabled in config
|
||||||
|
if k.Bool("metrics.stepCount.enabled") {
|
||||||
|
// Watch step count
|
||||||
|
stepCountCh, err := dev.WatchStepCount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// For every step count sample
|
||||||
|
for stepCount := range stepCountCh {
|
||||||
|
// Get current time
|
||||||
|
unixTime := time.Now().UnixNano()
|
||||||
|
// Insert sample and time into database
|
||||||
|
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, stepCount)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If battery level metrics enabled in config
|
||||||
|
if k.Bool("metrics.battLevel.enabled") {
|
||||||
|
// Watch battery level
|
||||||
|
battLevelCh, err := dev.WatchBatteryLevel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// For every battery level sample
|
||||||
|
for battLevel := range battLevelCh {
|
||||||
|
// Get current time
|
||||||
|
unixTime := time.Now().UnixNano()
|
||||||
|
// Insert sample and time into database
|
||||||
|
db.Exec("INSERT INTO battLevel VALUES (?, ?);", unixTime, battLevel)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If motion metrics enabled in config
|
||||||
|
if k.Bool("metrics.motion.enabled") {
|
||||||
|
// Watch motion values
|
||||||
|
motionCh, err := dev.WatchMotion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// For every motion sample
|
||||||
|
for motionVals := range motionCh {
|
||||||
|
// Get current time
|
||||||
|
unixTime := time.Now().UnixNano()
|
||||||
|
// Insert sample values and time into database
|
||||||
|
db.Exec(
|
||||||
|
"INSERT INTO motion VALUES (?, ?, ?, ?);",
|
||||||
|
unixTime,
|
||||||
|
motionVals.X,
|
||||||
|
motionVals.Y,
|
||||||
|
motionVals.Z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msg("Initialized metrics collection")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -27,9 +28,9 @@ import (
|
|||||||
"go.arsenm.dev/itd/translit"
|
"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
|
// Connect to dbus session bus
|
||||||
bus, err := newSessionBusConn()
|
bus, err := newSessionBusConn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,9 @@ func initNotifRelay(dev *infinitime.Device) error {
|
|||||||
}
|
}
|
||||||
var flag uint = 0
|
var flag uint = 0
|
||||||
// Becode monitor for notifications
|
// 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 {
|
if call.Err != nil {
|
||||||
return call.Err
|
return call.Err
|
||||||
}
|
}
|
||||||
|
|||||||
476
socket.go
@@ -19,64 +19,29 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/smallnest/rpcx/server"
|
|
||||||
"github.com/smallnest/rpcx/share"
|
|
||||||
"github.com/vmihailenco/msgpack/v5"
|
|
||||||
"go.arsenm.dev/infinitime"
|
"go.arsenm.dev/infinitime"
|
||||||
"go.arsenm.dev/infinitime/blefs"
|
"go.arsenm.dev/infinitime/blefs"
|
||||||
"go.arsenm.dev/itd/api"
|
"go.arsenm.dev/itd/api"
|
||||||
|
"go.arsenm.dev/lrpc/codec"
|
||||||
|
"go.arsenm.dev/lrpc/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This type signifies an unneeded value.
|
|
||||||
// A struct{} is used as it takes no space in memory.
|
|
||||||
// This exists for readability purposes
|
|
||||||
type none = struct{}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrDFUInvalidFile = errors.New("provided file is invalid for given upgrade type")
|
ErrDFUInvalidFile = errors.New("provided file is invalid for given upgrade type")
|
||||||
ErrDFUNotEnoughFiles = errors.New("not enough files provided for given upgrade type")
|
ErrDFUNotEnoughFiles = errors.New("not enough files provided for given upgrade type")
|
||||||
ErrDFUInvalidUpgType = errors.New("invalid upgrade type")
|
ErrDFUInvalidUpgType = errors.New("invalid upgrade type")
|
||||||
ErrRPCXNoReturnURL = errors.New("bidirectional requests over gateway require a returnURL field in the metadata")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DoneMap map[string]chan struct{}
|
func startSocket(ctx context.Context, dev *infinitime.Device) error {
|
||||||
|
|
||||||
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 {
|
|
||||||
// Make socket directory if non-existant
|
// Make socket directory if non-existant
|
||||||
err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0755)
|
err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,13 +65,12 @@ func startSocket(dev *infinitime.Device) error {
|
|||||||
log.Warn().Err(err).Msg("Error getting BLE filesystem")
|
log.Warn().Err(err).Msg("Error getting BLE filesystem")
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := server.NewServer()
|
srv := server.New()
|
||||||
|
|
||||||
itdAPI := &ITD{
|
itdAPI := &ITD{
|
||||||
dev: dev,
|
dev: dev,
|
||||||
srv: srv,
|
|
||||||
}
|
}
|
||||||
err = srv.Register(itdAPI, "")
|
err = srv.Register(itdAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -114,14 +78,13 @@ func startSocket(dev *infinitime.Device) error {
|
|||||||
fsAPI := &FS{
|
fsAPI := &FS{
|
||||||
dev: dev,
|
dev: dev,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
srv: srv,
|
|
||||||
}
|
}
|
||||||
err = srv.Register(fsAPI, "")
|
err = srv.Register(fsAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go srv.ServeListener("tcp", ln)
|
go srv.Serve(ctx, ln, codec.Default)
|
||||||
|
|
||||||
// Log socket start
|
// Log socket start
|
||||||
log.Info().Str("path", k.String("socket.path")).Msg("Started control socket")
|
log.Info().Str("path", k.String("socket.path")).Msg("Started control socket")
|
||||||
@@ -131,223 +94,129 @@ func startSocket(dev *infinitime.Device) error {
|
|||||||
|
|
||||||
type ITD struct {
|
type ITD struct {
|
||||||
dev *infinitime.Device
|
dev *infinitime.Device
|
||||||
srv *server.Server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) HeartRate(_ context.Context, _ none, out *uint8) error {
|
func (i *ITD) HeartRate(_ *server.Context) (uint8, error) {
|
||||||
heartRate, err := i.dev.HeartRate()
|
return i.dev.HeartRate()
|
||||||
*out = heartRate
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) WatchHeartRate(ctx context.Context, _ none, out *string) error {
|
func (i *ITD) WatchHeartRate(ctx *server.Context) error {
|
||||||
// Get client message sender
|
ch, err := ctx.MakeChannel()
|
||||||
msgSender, ok := getMsgSender(ctx, i.srv)
|
if err != nil {
|
||||||
// If user is using gateway, the client connection will not be available
|
return err
|
||||||
if !ok {
|
}
|
||||||
return ErrRPCXNoReturnURL
|
|
||||||
}
|
heartRateCh, err := i.dev.WatchHeartRate(ctx)
|
||||||
|
|
||||||
heartRateCh, cancel, err := i.dev.WatchHeartRate()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
go func() {
|
go func() {
|
||||||
done.Create(id)
|
|
||||||
// For every heart rate value
|
// For every heart rate value
|
||||||
for heartRate := range heartRateCh {
|
for heartRate := range heartRateCh {
|
||||||
select {
|
ch <- heartRate
|
||||||
case <-done[id]:
|
|
||||||
// Stop notifications if done signal received
|
|
||||||
cancel()
|
|
||||||
done.Remove(id)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
data, err := msgpack.Marshal(heartRate)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding heart rate")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send response to connection if no done signal received
|
|
||||||
msgSender.SendMessage(id, "HeartRateSample", nil, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
*out = id
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) BatteryLevel(_ context.Context, _ none, out *uint8) error {
|
func (i *ITD) BatteryLevel(_ *server.Context) (uint8, error) {
|
||||||
battLevel, err := i.dev.BatteryLevel()
|
return i.dev.BatteryLevel()
|
||||||
*out = battLevel
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) WatchBatteryLevel(ctx context.Context, _ none, out *string) error {
|
func (i *ITD) WatchBatteryLevel(ctx *server.Context) error {
|
||||||
// Get client message sender
|
ch, err := ctx.MakeChannel()
|
||||||
msgSender, ok := getMsgSender(ctx, i.srv)
|
if err != nil {
|
||||||
// If user is using gateway, the client connection will not be available
|
return err
|
||||||
if !ok {
|
}
|
||||||
return ErrRPCXNoReturnURL
|
|
||||||
}
|
battLevelCh, err := i.dev.WatchBatteryLevel(ctx)
|
||||||
|
|
||||||
battLevelCh, cancel, err := i.dev.WatchBatteryLevel()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
go func() {
|
go func() {
|
||||||
done.Create(id)
|
|
||||||
// For every heart rate value
|
// For every heart rate value
|
||||||
for battLevel := range battLevelCh {
|
for battLevel := range battLevelCh {
|
||||||
select {
|
ch <- battLevel
|
||||||
case <-done[id]:
|
|
||||||
// Stop notifications if done signal received
|
|
||||||
cancel()
|
|
||||||
done.Remove(id)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
data, err := msgpack.Marshal(battLevel)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding battery level")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send response to connection if no done signal received
|
|
||||||
msgSender.SendMessage(id, "BatteryLevelSample", nil, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
*out = id
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Motion(_ context.Context, _ none, out *infinitime.MotionValues) error {
|
func (i *ITD) Motion(_ *server.Context) (infinitime.MotionValues, error) {
|
||||||
motionVals, err := i.dev.Motion()
|
return i.dev.Motion()
|
||||||
*out = motionVals
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) WatchMotion(ctx context.Context, _ none, out *string) error {
|
func (i *ITD) WatchMotion(ctx *server.Context) error {
|
||||||
// Get client message sender
|
ch, err := ctx.MakeChannel()
|
||||||
msgSender, ok := getMsgSender(ctx, i.srv)
|
if err != nil {
|
||||||
// If user is using gateway, the client connection will not be available
|
return err
|
||||||
if !ok {
|
}
|
||||||
return ErrRPCXNoReturnURL
|
|
||||||
}
|
motionValsCh, err := i.dev.WatchMotion(ctx)
|
||||||
|
|
||||||
motionValsCh, cancel, err := i.dev.WatchMotion()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
go func() {
|
go func() {
|
||||||
done.Create(id)
|
|
||||||
// For every heart rate value
|
// For every heart rate value
|
||||||
for motionVals := range motionValsCh {
|
for motionVals := range motionValsCh {
|
||||||
select {
|
ch <- motionVals
|
||||||
case <-done[id]:
|
|
||||||
// Stop notifications if done signal received
|
|
||||||
cancel()
|
|
||||||
done.Remove(id)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
data, err := msgpack.Marshal(motionVals)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding motion values")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send response to connection if no done signal received
|
|
||||||
msgSender.SendMessage(id, "MotionSample", nil, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
*out = id
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) StepCount(_ context.Context, _ none, out *uint32) error {
|
func (i *ITD) StepCount(_ *server.Context) (uint32, error) {
|
||||||
stepCount, err := i.dev.StepCount()
|
return i.dev.StepCount()
|
||||||
*out = stepCount
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) WatchStepCount(ctx context.Context, _ none, out *string) error {
|
func (i *ITD) WatchStepCount(ctx *server.Context) error {
|
||||||
// Get client message sender
|
ch, err := ctx.MakeChannel()
|
||||||
msgSender, ok := getMsgSender(ctx, i.srv)
|
if err != nil {
|
||||||
// If user is using gateway, the client connection will not be available
|
return err
|
||||||
if !ok {
|
}
|
||||||
return ErrRPCXNoReturnURL
|
|
||||||
}
|
stepCountCh, err := i.dev.WatchStepCount(ctx)
|
||||||
|
|
||||||
stepCountCh, cancel, err := i.dev.WatchStepCount()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
go func() {
|
go func() {
|
||||||
done.Create(id)
|
|
||||||
// For every heart rate value
|
// For every heart rate value
|
||||||
for stepCount := range stepCountCh {
|
for stepCount := range stepCountCh {
|
||||||
select {
|
ch <- stepCount
|
||||||
case <-done[id]:
|
|
||||||
// Stop notifications if done signal received
|
|
||||||
cancel()
|
|
||||||
done.Remove(id)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
data, err := msgpack.Marshal(stepCount)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding step count")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send response to connection if no done signal received
|
|
||||||
msgSender.SendMessage(id, "StepCountSample", nil, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
*out = id
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Version(_ context.Context, _ none, out *string) error {
|
func (i *ITD) Version(_ *server.Context) (string, error) {
|
||||||
version, err := i.dev.Version()
|
return i.dev.Version()
|
||||||
*out = version
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Address(_ context.Context, _ none, out *string) error {
|
func (i *ITD) Address(_ *server.Context) string {
|
||||||
addr := i.dev.Address()
|
return i.dev.Address()
|
||||||
*out = addr
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Notify(_ context.Context, data api.NotifyData, _ *none) error {
|
func (i *ITD) Notify(_ *server.Context, data api.NotifyData) error {
|
||||||
return i.dev.Notify(data.Title, data.Body)
|
return i.dev.Notify(data.Title, data.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) SetTime(_ context.Context, t time.Time, _ *none) error {
|
func (i *ITD) SetTime(_ *server.Context, t *time.Time) error {
|
||||||
return i.dev.SetTime(t)
|
return i.dev.SetTime(*t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) WeatherUpdate(_ context.Context, _ none, _ *none) error {
|
func (i *ITD) WeatherUpdate(_ *server.Context) {
|
||||||
sendWeatherCh <- struct{}{}
|
sendWeatherCh <- struct{}{}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) FirmwareUpgrade(ctx context.Context, reqData api.FwUpgradeData, out *string) error {
|
func (i *ITD) FirmwareUpgrade(ctx *server.Context, reqData api.FwUpgradeData) error {
|
||||||
i.dev.DFU.Reset()
|
i.dev.DFU.Reset()
|
||||||
|
|
||||||
switch reqData.Type {
|
switch reqData.Type {
|
||||||
@@ -387,29 +256,21 @@ func (i *ITD) FirmwareUpgrade(ctx context.Context, reqData api.FwUpgradeData, ou
|
|||||||
return ErrDFUInvalidUpgType
|
return ErrDFUInvalidUpgType
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
ch, err := ctx.MakeChannel()
|
||||||
*out = id
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Get client message sender
|
|
||||||
msgSender, ok := getMsgSender(ctx, i.srv)
|
|
||||||
// If user is using gateway, the client connection will not be available
|
|
||||||
if ok {
|
|
||||||
go func() {
|
go func() {
|
||||||
// For every progress event
|
// For every progress event
|
||||||
for event := range i.dev.DFU.Progress() {
|
for event := range i.dev.DFU.Progress() {
|
||||||
data, err := msgpack.Marshal(event)
|
ch <- event
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding DFU progress event")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
msgSender.SendMessage(id, "DFUProgress", nil, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
firmwareUpdating = false
|
firmwareUpdating = false
|
||||||
msgSender.SendMessage(id, "Done", nil, nil)
|
// Send zero object to signal completion
|
||||||
|
close(ch)
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
// Set firmwareUpdating
|
// Set firmwareUpdating
|
||||||
firmwareUpdating = true
|
firmwareUpdating = true
|
||||||
@@ -427,18 +288,23 @@ func (i *ITD) FirmwareUpgrade(ctx context.Context, reqData api.FwUpgradeData, ou
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ITD) Done(_ context.Context, id string, _ *none) error {
|
|
||||||
done.Done(id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type FS struct {
|
type FS struct {
|
||||||
dev *infinitime.Device
|
dev *infinitime.Device
|
||||||
fs *blefs.FS
|
fs *blefs.FS
|
||||||
srv *server.Server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) Remove(_ context.Context, paths []string, _ *none) error {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) Remove(_ *server.Context, paths []string) error {
|
||||||
fs.updateFS()
|
fs.updateFS()
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
err := fs.fs.Remove(path)
|
err := fs.fs.Remove(path)
|
||||||
@@ -449,12 +315,23 @@ func (fs *FS) Remove(_ context.Context, paths []string, _ *none) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) Rename(_ context.Context, paths [2]string, _ *none) error {
|
func (fs *FS) Rename(_ *server.Context, paths [2]string) error {
|
||||||
fs.updateFS()
|
fs.updateFS()
|
||||||
return fs.fs.Rename(paths[0], paths[1])
|
return fs.fs.Rename(paths[0], paths[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) Mkdir(_ context.Context, paths []string, _ *none) error {
|
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()
|
fs.updateFS()
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
err := fs.fs.Mkdir(path)
|
err := fs.fs.Mkdir(path)
|
||||||
@@ -465,18 +342,18 @@ func (fs *FS) Mkdir(_ context.Context, paths []string, _ *none) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) ReadDir(_ context.Context, dir string, out *[]api.FileInfo) error {
|
func (fs *FS) ReadDir(_ *server.Context, dir string) ([]api.FileInfo, error) {
|
||||||
fs.updateFS()
|
fs.updateFS()
|
||||||
|
|
||||||
entries, err := fs.fs.ReadDir(dir)
|
entries, err := fs.fs.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
var fileInfo []api.FileInfo
|
var fileInfo []api.FileInfo
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
info, err := entry.Info()
|
info, err := entry.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
fileInfo = append(fileInfo, api.FileInfo{
|
fileInfo = append(fileInfo, api.FileInfo{
|
||||||
Name: info.Name(),
|
Name: info.Name(),
|
||||||
@@ -485,11 +362,10 @@ func (fs *FS) ReadDir(_ context.Context, dir string, out *[]api.FileInfo) error
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
*out = fileInfo
|
return fileInfo, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) Upload(ctx context.Context, paths [2]string, out *string) error {
|
func (fs *FS) Upload(ctx *server.Context, paths [2]string) error {
|
||||||
fs.updateFS()
|
fs.updateFS()
|
||||||
|
|
||||||
localFile, err := os.Open(paths[1])
|
localFile, err := os.Open(paths[1])
|
||||||
@@ -507,31 +383,22 @@ func (fs *FS) Upload(ctx context.Context, paths [2]string, out *string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
ch, err := ctx.MakeChannel()
|
||||||
*out = id
|
if err != nil {
|
||||||
|
return err
|
||||||
// Get client message sender
|
}
|
||||||
msgSender, ok := getMsgSender(ctx, fs.srv)
|
|
||||||
// If user is using gateway, the client connection will not be available
|
|
||||||
if ok {
|
|
||||||
go func() {
|
go func() {
|
||||||
// For every progress event
|
// For every progress event
|
||||||
for sent := range remoteFile.Progress() {
|
for sent := range remoteFile.Progress() {
|
||||||
data, err := msgpack.Marshal(api.FSTransferProgress{
|
ch <- api.FSTransferProgress{
|
||||||
Total: remoteFile.Size(),
|
Total: remoteFile.Size(),
|
||||||
Sent: sent,
|
Sent: sent,
|
||||||
})
|
}
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding filesystem transfer progress event")
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msgSender.SendMessage(id, "FSProgress", nil, data)
|
// Send zero object to signal completion
|
||||||
}
|
close(ch)
|
||||||
|
|
||||||
msgSender.SendMessage(id, "Done", nil, nil)
|
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(remoteFile, localFile)
|
io.Copy(remoteFile, localFile)
|
||||||
@@ -542,7 +409,7 @@ func (fs *FS) Upload(ctx context.Context, paths [2]string, out *string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FS) Download(ctx context.Context, paths [2]string, out *string) error {
|
func (fs *FS) Download(ctx *server.Context, paths [2]string) error {
|
||||||
fs.updateFS()
|
fs.updateFS()
|
||||||
|
|
||||||
localFile, err := os.Create(paths[0])
|
localFile, err := os.Create(paths[0])
|
||||||
@@ -555,39 +422,56 @@ func (fs *FS) Download(ctx context.Context, paths [2]string, out *string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
ch, err := ctx.MakeChannel()
|
||||||
*out = id
|
if err != nil {
|
||||||
|
return err
|
||||||
// Get client message sender
|
}
|
||||||
msgSender, ok := getMsgSender(ctx, fs.srv)
|
|
||||||
// If user is using gateway, the client connection will not be available
|
|
||||||
if ok {
|
|
||||||
go func() {
|
go func() {
|
||||||
// For every progress event
|
// For every progress event
|
||||||
for rcvd := range remoteFile.Progress() {
|
for sent := range remoteFile.Progress() {
|
||||||
data, err := msgpack.Marshal(api.FSTransferProgress{
|
ch <- api.FSTransferProgress{
|
||||||
Total: remoteFile.Size(),
|
Total: remoteFile.Size(),
|
||||||
Sent: rcvd,
|
Sent: sent,
|
||||||
})
|
}
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Error encoding filesystem transfer progress event")
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
msgSender.SendMessage(id, "FSProgress", nil, data)
|
// Send zero object to signal completion
|
||||||
}
|
close(ch)
|
||||||
|
|
||||||
msgSender.SendMessage(id, "Done", nil, nil)
|
|
||||||
localFile.Close()
|
localFile.Close()
|
||||||
remoteFile.Close()
|
remoteFile.Close()
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
go io.Copy(localFile, remoteFile)
|
go io.Copy(localFile, remoteFile)
|
||||||
|
|
||||||
return nil
|
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() {
|
func (fs *FS) updateFS() {
|
||||||
if fs.fs == nil || updateFS {
|
if fs.fs == nil || updateFS {
|
||||||
// Get new FS
|
// Get new FS
|
||||||
@@ -602,87 +486,3 @@ func (fs *FS) updateFS() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanPaths runs strings.TrimSpace and filepath.Clean
|
|
||||||
// on all inputs, and returns the updated slice
|
|
||||||
func cleanPaths(paths []string) []string {
|
|
||||||
for index, path := range paths {
|
|
||||||
newPath := strings.TrimSpace(path)
|
|
||||||
paths[index] = filepath.Clean(newPath)
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMsgSender(ctx context.Context, srv *server.Server) (MessageSender, bool) {
|
|
||||||
// Get client message sender
|
|
||||||
clientConn, ok := ctx.Value(server.RemoteConnContextKey).(net.Conn)
|
|
||||||
// If the connection exists, use rpcMsgSender
|
|
||||||
if ok {
|
|
||||||
return &rpcMsgSender{srv, clientConn}, true
|
|
||||||
} else {
|
|
||||||
// Get metadata if it exists
|
|
||||||
metadata, ok := ctx.Value(share.ReqMetaDataKey).(map[string]string)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
// Get returnURL field from metadata if it exists
|
|
||||||
returnURL, ok := metadata["returnURL"]
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
// Use httpMsgSender
|
|
||||||
return &httpMsgSender{returnURL}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// The MessageSender interface sends messages to the client
|
|
||||||
type MessageSender interface {
|
|
||||||
SendMessage(servicePath, serviceMethod string, metadata map[string]string, data []byte) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// rpcMsgSender sends messages using RPCX, for clients that support it
|
|
||||||
type rpcMsgSender struct {
|
|
||||||
srv *server.Server
|
|
||||||
conn net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessage uses the server to send an RPCX message back to the client
|
|
||||||
func (r *rpcMsgSender) SendMessage(servicePath, serviceMethod string, metadata map[string]string, data []byte) error {
|
|
||||||
return r.srv.SendMessage(r.conn, servicePath, serviceMethod, metadata, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpMsgSender sends messages to the given return URL, for clients that provide it
|
|
||||||
type httpMsgSender struct {
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessage uses HTTP to send a message back to the client
|
|
||||||
func (h *httpMsgSender) SendMessage(servicePath, serviceMethod string, metadata map[string]string, data []byte) error {
|
|
||||||
// Create new POST request with provided URL
|
|
||||||
req, err := http.NewRequest(http.MethodPost, h.url, bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set service path and method headers
|
|
||||||
req.Header.Set("X-RPCX-ServicePath", servicePath)
|
|
||||||
req.Header.Set("X-RPCX-ServiceMethod", serviceMethod)
|
|
||||||
|
|
||||||
// Create new URL query values
|
|
||||||
query := url.Values{}
|
|
||||||
// Transfer values from metadata to query
|
|
||||||
for k, v := range metadata {
|
|
||||||
query.Set(k, v)
|
|
||||||
}
|
|
||||||
// Set metadata header by encoding query values
|
|
||||||
req.Header.Set("X-RPCX-Meta", query.Encode())
|
|
||||||
|
|
||||||
// Perform request
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Close body
|
|
||||||
return res.Body.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
unknown
|
|
||||||
20
weather.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
@@ -60,13 +61,13 @@ type OSMData []struct {
|
|||||||
|
|
||||||
var sendWeatherCh = make(chan struct{}, 1)
|
var sendWeatherCh = make(chan struct{}, 1)
|
||||||
|
|
||||||
func initWeather(dev *infinitime.Device) error {
|
func initWeather(ctx context.Context, dev *infinitime.Device) error {
|
||||||
if !k.Bool("weather.enabled") {
|
if !k.Bool("weather.enabled") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get location based on string in config
|
// Get location based on string in config
|
||||||
lat, lon, err := getLocation(k.String("weather.location"))
|
lat, lon, err := getLocation(ctx, k.String("weather.location"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,7 @@ func initWeather(dev *infinitime.Device) error {
|
|||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
// Attempt to get weather
|
// Attempt to get weather
|
||||||
data, err := getWeather(lat, lon)
|
data, err := getWeather(ctx, lat, lon)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Error getting weather data")
|
log.Warn().Err(err).Msg("Error getting weather data")
|
||||||
// Wait 15 minutes before retrying
|
// Wait 15 minutes before retrying
|
||||||
@@ -181,10 +182,14 @@ func initWeather(dev *infinitime.Device) error {
|
|||||||
|
|
||||||
// getLocation returns the latitude and longitude
|
// getLocation returns the latitude and longitude
|
||||||
// given a location
|
// given a location
|
||||||
func getLocation(loc string) (lat, lon float64, err error) {
|
func getLocation(ctx context.Context, loc string) (lat, lon float64, err error) {
|
||||||
// Create request URL and perform GET request
|
// Create request URL and perform GET request
|
||||||
reqURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search.php?q=%s&format=jsonv2", url.QueryEscape(loc))
|
reqURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search.php?q=%s&format=jsonv2", url.QueryEscape(loc))
|
||||||
res, err := http.Get(reqURL)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -218,9 +223,10 @@ func getLocation(loc string) (lat, lon float64, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getWeather gets weather data given a latitude and longitude
|
// getWeather gets weather data given a latitude and longitude
|
||||||
func getWeather(lat, lon float64) (*METResponse, error) {
|
func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
|
||||||
// Create new GET request
|
// Create new GET request
|
||||||
req, err := http.NewRequest(
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f",
|
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f",
|
||||||
|
|||||||