66 Commits

Author SHA1 Message Date
4a397d4c1e Add go generate script for calculating version number 2022-11-17 21:27:36 -08:00
c5fb3e1a33 Add woodpecker config 2022-11-18 05:02:37 +00:00
908bd7d5f3 Add resource loading to ITD FS tab 2022-11-15 19:20:34 -08:00
c97fcaeefb Mention navigation support in README 2022-11-07 12:24:55 -08:00
992eb2e085 Add navigation support via PureMaps 2022-11-07 12:22:14 -08:00
f33b3d2b56 Update infinitime library 2022-10-25 12:38:02 -07:00
03f3968fe1 Update infinitime library 2022-10-20 01:42:23 -07:00
dea92c6404 Update infinitime library 2022-10-17 12:50:51 -07:00
006f245c10 Add warning if current InfiniTime doesn't support BLE FS (#29) 2022-10-17 12:40:51 -07:00
d232340edd Update infinitime library 2022-10-17 12:24:17 -07:00
c6458720e9 Handle error events in itctl res load command (#29) 2022-10-17 12:23:06 -07:00
1e072a3540 Merge pull request 'Add resource loading to ITD' (#28) from resource-loading into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/28
2022-10-16 20:17:49 +00:00
f639fef992 Add resource loading as part of DFU 2022-10-16 13:17:12 -07:00
2d0db1dcf1 Close channel once resource uploading complete 2022-10-16 12:42:59 -07:00
4efa4380c4 Add -r for rm and -p for mkdir 2022-09-03 16:28:25 -07:00
fca64afbf3 Remove example comments from goreleaser config 2022-09-01 15:09:49 -07:00
cf24c5ace8 Fix file extension of Alpine example 2022-09-01 15:02:11 -07:00
a25b2e3e62 Add --allow-untrusted to Alpine example 2022-09-01 15:00:03 -07:00
2d0b64d92f Add installation instructions for major distros 2022-09-01 14:58:33 -07:00
5efafe9be7 Remove download binary badge and add new AUR badge 2022-09-01 14:33:23 -07:00
271510d528 Allow automatic release to the AUR 2022-09-01 11:50:41 -07:00
4d72a063b2 Update CI badge 2022-09-01 03:14:05 -07:00
643245f16c Add GoReleaser config 2022-09-01 01:52:46 -07:00
6f87980d4b Add resource loading to itctl 2022-08-30 13:01:36 -07:00
851f1975d6 Add LoadResources() to API 2022-08-30 12:13:22 -07:00
5e66fe82ac Improve itgui compilation documentation (Fixes #24) 2022-08-29 13:28:52 -07:00
645541e079 Update infinitime library and bluetooth library (Fixes #22) 2022-08-19 14:05:30 -07:00
1012be6e5b Revert #22 bandaid fix 2022-08-19 14:04:08 -07:00
5973290d6c Temporary bandaid fix for #22 2022-08-14 00:05:56 -07:00
19bacf29b2 Fix comment above goroutine code 2022-07-31 02:40:46 -07:00
a78650e526 Fix bug where itctl doesn't exit on SIGINT/SIGTERM 2022-07-31 02:22:33 -07:00
71e9caf0bc Fix bug where help command doesn't show flags/subcommands 2022-07-31 02:15:42 -07:00
1f5a6365bc Remove GOFLAGS from Makefile as the go tool already looks at that variable 2022-07-22 10:36:19 -07:00
958f2af516 Propagate context to lrpc 2022-05-12 17:14:34 -07:00
60f1eedc9a Create and propagate contexts wherever possible 2022-05-11 13:24:12 -07:00
c05147518d Add metrics screenshot 2022-05-11 12:10:50 -07:00
422f844943 Add metrics graphs to itgui 2022-05-10 23:37:58 -07:00
66618e5bf0 Add metrics collection via sqlite 2022-05-10 18:03:37 -07:00
0c2e57ced0 Update lrpc 2022-05-10 02:12:52 -07:00
6d9f6fc6e6 Update Go compiler requirement 2022-05-09 21:46:03 -07:00
0cbd6a48ae Allow API client to be made from connection 2022-05-07 21:23:42 -07:00
b614138f6b Update lrpc library 2022-05-07 15:12:29 -07:00
3a0491f069 Add new itgui screenshots 2022-05-05 14:05:58 -07:00
093a5632c7 Rewrite itgui and add new screenshots 2022-05-05 14:00:49 -07:00
91662e6f38 Update infinitime library 2022-05-05 12:41:51 -07:00
931966bf1e Update lrpc for data race fixes 2022-05-04 16:16:28 -07:00
ed01700e26 Update lrpc 2022-05-03 18:55:37 -07:00
e9269e8eb8 Update infinitime library 2022-05-02 20:21:47 -07:00
52b85ab361 Allow changing bluetooth adapter ID 2022-05-02 20:17:38 -07:00
bc45943bdc Update lrpc 2022-05-02 16:28:25 -07:00
6933f45683 Fix lrpc response line number in README 2022-05-01 23:06:27 -07:00
0f22d67395 Update lrpc 2022-05-01 21:39:58 -07:00
6da03181a9 Remove version.txt on clean 2022-05-01 21:22:15 -07:00
44a25625da Only update version if version.txt does not exist 2022-05-01 21:21:22 -07:00
14a38351e4 Update README to reflect recent changes 2022-05-01 21:16:47 -07:00
4c27f424b2 Remove debug print 2022-05-01 20:56:14 -07:00
86fbef2e8a Remove the no-longer useful none type alias 2022-05-01 20:51:13 -07:00
7b8658e072 Remove version.txt 2022-05-01 20:49:42 -07:00
73c46cfa66 Remove replace directive and fix firmware upgrade error 2022-05-01 20:40:30 -07:00
1e0f1c5b76 Fix bug where itctl could not be killed 2022-05-01 20:32:59 -07:00
78b5ca1de8 Add context support and update lrpc 2022-05-01 15:22:28 -07:00
b0c4574481 Remove now unnecessary DoneMap 2022-05-01 14:00:31 -07:00
01975f207c Upgrade lrpc version 2022-05-01 13:59:40 -07:00
428e7967c1 Use default codec 2022-05-01 11:41:16 -07:00
56dbf0540e Switch to lrpc and use context to handle signals 2022-05-01 11:36:28 -07:00
240e7a5ee4 Use rpcxlite 2022-04-30 03:25:27 -07:00
61 changed files with 2827 additions and 1811 deletions

3
.gitignore vendored
View File

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

109
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,109 @@
before:
hooks:
- go generate
- 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

8
.woodpecker.yml Normal file
View File

@@ -0,0 +1,8 @@
pipeline:
release:
image: goreleaser/goreleaser
commands:
- goreleaser release
secrets: [ gitea_token, aur_key ]
when:
event: tag

View File

@@ -3,14 +3,14 @@ BIN_PREFIX = $(DESTDIR)$(PREFIX)/bin
SERVICE_PREFIX = $(DESTDIR)$(PREFIX)/lib/systemd/user
CFG_PREFIX = $(DESTDIR)/etc
all: version
go build $(GOFLAGS)
go build ./cmd/itctl $(GOFLAGS)
all: version.txt
go build
go build ./cmd/itctl
clean:
rm -f itctl
rm -f itd
printf "unknown" > version.txt
rm -f version.txt
install:
install -Dm755 ./itd $(BIN_PREFIX)/itd
@@ -24,7 +24,7 @@ uninstall:
rm $(SERVICE_PREFIX)/itd.service
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
.PHONY: all clean install uninstall version
.PHONY: all clean install uninstall

View File

@@ -3,9 +3,9 @@
`itd` is a daemon that uses my infinitime [library](https://go.arsenm.dev/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io).
[![Build status](https://ci.appveyor.com/api/projects/status/xgj5sobw76ndqaod?svg=true)](https://ci.appveyor.com/project/moussaelianarsen/itd)
[![Binary downloads](https://img.shields.io/badge/download-binary-orange)](https://minio.arsenm.dev/minio/itd/)
[![AUR package](https://img.shields.io/aur/version/itd-git?label=itd-git&logo=archlinux)](https://aur.archlinux.org/packages/itd-git/)
[![Build status](https://ci.appveyor.com/api/projects/status/01qpwa2bn7c7fdi2?svg=true)](https://ci.appveyor.com/project/moussaelianarsen/itd-7t6ko)
[![itd-git AUR package](https://img.shields.io/aur/version/itd-git?label=itd-git&logo=archlinux)](https://aur.archlinux.org/packages/itd-git/)
[![itd-bin AUR package](https://img.shields.io/aur/version/itd-bin?label=itd-bin&logo=archlinux)](https://aur.archlinux.org/packages/itd-bin/)
---
@@ -19,6 +19,39 @@
- Set current time
- Control socket
- Firmware upgrades
- Weather
- BLE Filesystem
- Navigation (PureMaps)
---
### 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 +59,13 @@
This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directly control the daemon and, by extension, the connected watch.
The socket accepts JSON requests. For example, sending a notification looks like this:
The socket uses my [lrpc](https://gitea.arsenm.dev/Arsen6331/lrpc) library for requests. This library accepts requests in msgpack, with the following format:
```json
{"type": 5, "data": {"title": "title1", "body": "body1"}}
{"Receiver": "ITD", "Method": "Notify", "Arg": {"title": "title1", "body": "body1"}, "ID": "some-id-here"}
```
It will return a JSON response. A response can have 3 fields: `error`, `msg`, and `value`. Error is a boolean that signals whether an error was returned. If error is true, the msg field will contain the error. Value can contain any data and depends on what the request was.
The various request types and their data requirements can be seen in `internal/types`. I can make separate docs for it if I get enough requests.
It will return a msgpack response, the format of which can be found [here](https://gitea.arsenm.dev/Arsen6331/lrpc/src/branch/master/internal/types/types.go#L30). The response will have the same ID as was sent in the request in order to allow the client to keep track of which request the response belongs to.
---
@@ -105,48 +136,49 @@ Use "itctl [command] --help" for more information about a command.
### `itgui`
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [fyne library](https://fyne.io/) for Go. It can be compiled by running:
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [Fyne library](https://fyne.io/) for Go.
#### Compilation
Before compiling, certain prerequisites must be installed. These are listed on the following page: https://developer.fyne.io/started/#prerequisites
It can be compiled by running:
```shell
go build ./cmd/itgui
```
#### Cross-compilation
Due to the use of OpenGL, cross-compilation of `itgui` isn't as simple as that of `itd` and `itctl`. The following guide from the Fyne website should work for `itgui`: https://developer.fyne.io/started/cross-compiling.
#### Screenshots
![Info tab](https://i.imgur.com/okxG9EI.png)
![Info tab](cmd/itgui/screenshots/info.png)
![Notify tab](https://i.imgur.com/DrVhOAq.png)
![Motion tab](cmd/itgui/screenshots/motion.png)
![Set time tab](https://i.imgur.com/j9civeY.png)
![Notify tab](cmd/itgui/screenshots/notify.png)
![Upgrade tab](https://i.imgur.com/1KY6fG4.png)
![FS tab](cmd/itgui/screenshots/fs.png)
![Upgrade in progress](https://i.imgur.com/w5qbWAw.png)
![FS mkdir](cmd/itgui/screenshots/mkdir.png)
---
![FS resource upload](cmd/itgui/screenshots/resources.png)
#### Interactive mode
![Time tab](cmd/itgui/screenshots/time.png)
Running `itctl` by itself will open interactive mode. It's essentially a shell where you can enter commands. For example:
![Firmware tab](cmd/itgui/screenshots/firmware.png)
```
$ itctl
itctl> fw ver
1.3.0
itctl> get batt
81%
itctl> get heart
92 BPM
itctl> set time 2021-08-22T00:06:18-07:00
itctl> set time now
itctl> exit
```
![Upgrade in progress](cmd/itgui/screenshots/progress.png)
![Metrics tab](cmd/itgui/screenshots/metrics.png)
---
### Installation
To install, install the go compiler and make. Usually, go is provided by a package either named `go` or `golang`, and make is usually provided by `make`. The go compiler must be version 1.16 or newer for the `io/fs` module.
To install, install the go compiler and make. Usually, go is provided by a package either named `go` or `golang`, and make is usually provided by `make`. The go compiler must be version 1.17 or newer for various new `reflect` features.
To install, run
```shell

View File

@@ -1,117 +1,37 @@
package api
import (
"context"
"io"
"net"
"github.com/smallnest/rpcx/client"
"github.com/smallnest/rpcx/protocol"
"github.com/vmihailenco/msgpack/v5"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/lrpc/client"
"go.arsenm.dev/lrpc/codec"
)
const DefaultAddr = "/tmp/itd/socket"
type Client struct {
itdClient client.XClient
itdCh chan *protocol.Message
fsClient client.XClient
fsCh chan *protocol.Message
srvVals map[string]chan interface{}
client *client.Client
}
func New(sockPath string) (*Client, error) {
d, err := client.NewPeer2PeerDiscovery("unix@"+sockPath, "")
conn, err := net.Dial("unix", sockPath)
if err != nil {
return nil, err
}
out := &Client{}
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)
out := &Client{
client: client.New(conn, codec.Default),
}
return out, nil
}
func (c *Client) handleMessages(msgCh chan *protocol.Message) {
for msg := range msgCh {
_, ok := c.srvVals[msg.ServicePath]
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 NewFromConn(conn io.ReadWriteCloser) *Client {
return &Client{
client: client.New(conn, codec.Default),
}
}
func (c *Client) done(id string) error {
return c.itdClient.Call(
context.Background(),
"Done",
id,
nil,
)
}
func (c *Client) Close() error {
err := c.itdClient.Close()
if err != nil {
return err
}
err = c.fsClient.Close()
if err != nil {
return err
}
close(c.itdCh)
close(c.fsCh)
return nil
return c.client.Close()
}

View File

@@ -2,39 +2,25 @@ package api
import (
"context"
"time"
"go.arsenm.dev/infinitime"
)
func (c *Client) FirmwareUpgrade(upgType UpgradeType, files ...string) (chan infinitime.DFUProgress, error) {
var id string
err := c.itdClient.Call(
context.Background(),
func (c *Client) FirmwareUpgrade(ctx context.Context, upgType UpgradeType, files ...string) (chan infinitime.DFUProgress, error) {
progressCh := make(chan infinitime.DFUProgress, 5)
err := c.client.Call(
ctx,
"ITD",
"FirmwareUpgrade",
FwUpgradeData{
Type: upgType,
Files: files,
},
&id,
progressCh,
)
if err != nil {
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
}

105
api/fs.go
View File

@@ -1,40 +1,61 @@
package api
import (
"context"
"time"
)
import "context"
func (c *Client) Remove(paths ...string) error {
return c.fsClient.Call(
context.Background(),
func (c *Client) RemoveAll(ctx context.Context, paths ...string) error {
return c.client.Call(
ctx,
"FS",
"RemoveAll",
paths,
nil,
)
}
func (c *Client) Remove(ctx context.Context, paths ...string) error {
return c.client.Call(
ctx,
"FS",
"Remove",
paths,
nil,
)
}
func (c *Client) Rename(old, new string) error {
return c.fsClient.Call(
context.Background(),
"Remove",
func (c *Client) Rename(ctx context.Context, old, new string) error {
return c.client.Call(
ctx,
"FS",
"Rename",
[2]string{old, new},
nil,
)
}
func (c *Client) Mkdir(paths ...string) error {
return c.fsClient.Call(
context.Background(),
func (c *Client) MkdirAll(ctx context.Context, paths ...string) error {
return c.client.Call(
ctx,
"FS",
"MkdirAll",
paths,
nil,
)
}
func (c *Client) Mkdir(ctx context.Context, paths ...string) error {
return c.client.Call(
ctx,
"FS",
"Mkdir",
paths,
nil,
)
}
func (c *Client) ReadDir(dir string) (out []FileInfo, err error) {
err = c.fsClient.Call(
context.Background(),
func (c *Client) ReadDir(ctx context.Context, dir string) (out []FileInfo, err error) {
err = c.client.Call(
ctx,
"FS",
"ReadDir",
dir,
&out,
@@ -42,60 +63,34 @@ func (c *Client) ReadDir(dir string) (out []FileInfo, err error) {
return
}
func (c *Client) Upload(dst, src string) (chan FSTransferProgress, error) {
var id string
err := c.fsClient.Call(
context.Background(),
func (c *Client) Upload(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
progressCh := make(chan FSTransferProgress, 5)
err := c.client.Call(
ctx,
"FS",
"Upload",
[2]string{dst, src},
&id,
progressCh,
)
if err != nil {
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
}
func (c *Client) Download(dst, src string) (chan FSTransferProgress, error) {
var id string
err := c.fsClient.Call(
context.Background(),
func (c *Client) Download(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
progressCh := make(chan FSTransferProgress, 5)
err := c.client.Call(
ctx,
"FS",
"Download",
[2]string{dst, src},
&id,
progressCh,
)
if err != nil {
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
}

View File

@@ -6,9 +6,10 @@ import (
"go.arsenm.dev/infinitime"
)
func (c *Client) HeartRate() (out uint8, err error) {
err = c.itdClient.Call(
context.Background(),
func (c *Client) HeartRate(ctx context.Context) (out uint8, err error) {
err = c.client.Call(
ctx,
"ITD",
"HeartRate",
nil,
&out,
@@ -16,9 +17,10 @@ func (c *Client) HeartRate() (out uint8, err error) {
return
}
func (c *Client) BatteryLevel() (out uint8, err error) {
err = c.itdClient.Call(
context.Background(),
func (c *Client) BatteryLevel(ctx context.Context) (out uint8, err error) {
err = c.client.Call(
ctx,
"ITD",
"BatteryLevel",
nil,
&out,
@@ -26,9 +28,10 @@ func (c *Client) BatteryLevel() (out uint8, err error) {
return
}
func (c *Client) Motion() (out infinitime.MotionValues, err error) {
err = c.itdClient.Call(
context.Background(),
func (c *Client) Motion(ctx context.Context) (out infinitime.MotionValues, err error) {
err = c.client.Call(
ctx,
"ITD",
"Motion",
nil,
&out,
@@ -36,9 +39,10 @@ func (c *Client) Motion() (out infinitime.MotionValues, err error) {
return
}
func (c *Client) StepCount() (out uint32, err error) {
err = c.itdClient.Call(
context.Background(),
func (c *Client) StepCount(ctx context.Context) (out uint32, err error) {
err = c.client.Call(
ctx,
"ITD",
"StepCount",
nil,
&out,
@@ -46,9 +50,10 @@ func (c *Client) StepCount() (out uint32, err error) {
return
}
func (c *Client) Version() (out string, err error) {
err = c.itdClient.Call(
context.Background(),
func (c *Client) Version(ctx context.Context) (out string, err error) {
err = c.client.Call(
ctx,
"ITD",
"Version",
nil,
&out,
@@ -56,9 +61,10 @@ func (c *Client) Version() (out string, err error) {
return
}
func (c *Client) Address() (out string, err error) {
err = c.itdClient.Call(
context.Background(),
func (c *Client) Address(ctx context.Context) (out string, err error) {
err = c.client.Call(
ctx,
"ITD",
"Address",
nil,
&out,

View File

@@ -1,17 +1,16 @@
package api
import (
"context"
)
import "context"
func (c *Client) Notify(title, body string) error {
return c.itdClient.Call(
context.Background(),
func (c *Client) Notify(ctx context.Context, title, body string) error {
return c.client.Call(
ctx,
"ITD",
"Notify",
NotifyData{
Title: title,
Body: body,
Body: body,
},
nil,
)
}
}

26
api/resources.go Normal file
View File

@@ -0,0 +1,26 @@
package api
import (
"context"
"go.arsenm.dev/infinitime"
)
// LoadResources loads resources onto the watch from the given
// file path to the resources zip
func (c *Client) LoadResources(ctx context.Context, path string) (<-chan infinitime.ResourceLoadProgress, error) {
progCh := make(chan infinitime.ResourceLoadProgress)
err := c.client.Call(
ctx,
"FS",
"LoadResources",
path,
progCh,
)
if err != nil {
return nil, err
}
return progCh, nil
}

View File

@@ -5,9 +5,10 @@ import (
"time"
)
func (c *Client) SetTime(t time.Time) error {
return c.itdClient.Call(
context.Background(),
func (c *Client) SetTime(ctx context.Context, t time.Time) error {
return c.client.Call(
ctx,
"ITD",
"SetTime",
t,
nil,

View File

@@ -2,11 +2,12 @@ package api
import "context"
func (c *Client) WeatherUpdate() error {
return c.itdClient.Call(
context.Background(),
func (c *Client) WeatherUpdate(ctx context.Context) error {
return c.client.Call(
ctx,
"ITD",
"WeatherUpdate",
nil,
nil,
)
}
}

View File

@@ -2,143 +2,70 @@ package api
import (
"context"
"time"
"go.arsenm.dev/infinitime"
)
func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) {
var id string
err := c.itdClient.Call(
context.Background(),
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
outCh := make(chan uint8, 2)
err := c.client.Call(
ctx,
"ITD",
"WatchHeartRate",
nil,
&id,
outCh,
)
if err != nil {
return nil, nil, err
return nil, err
}
outCh := make(chan uint8, 2)
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.(uint8)
}
}()
doneFn := func() {
c.done(id)
close(c.srvVals[id])
delete(c.srvVals, id)
}
return outCh, doneFn, nil
return outCh, nil
}
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
var id string
err := c.itdClient.Call(
context.Background(),
func (c *Client) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
outCh := make(chan uint8, 2)
err := c.client.Call(
ctx,
"ITD",
"WatchBatteryLevel",
nil,
&id,
outCh,
)
if err != nil {
return nil, nil, err
return nil, err
}
outCh := make(chan uint8, 2)
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.(uint8)
}
}()
doneFn := func() {
c.done(id)
close(c.srvVals[id])
delete(c.srvVals, id)
}
return outCh, doneFn, nil
return outCh, nil
}
func (c *Client) WatchStepCount() (<-chan uint32, func(), error) {
var id string
err := c.itdClient.Call(
context.Background(),
func (c *Client) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
outCh := make(chan uint32, 2)
err := c.client.Call(
ctx,
"ITD",
"WatchStepCount",
nil,
&id,
outCh,
)
if err != nil {
return nil, nil, err
return nil, err
}
outCh := make(chan uint32, 2)
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.(uint32)
}
}()
doneFn := func() {
c.done(id)
close(c.srvVals[id])
delete(c.srvVals, id)
}
return outCh, doneFn, nil
return outCh, nil
}
func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) {
var id string
err := c.itdClient.Call(
context.Background(),
func (c *Client) WatchMotion(ctx context.Context) (<-chan infinitime.MotionValues, error) {
outCh := make(chan infinitime.MotionValues, 2)
err := c.client.Call(
ctx,
"ITD",
"WatchMotion",
nil,
&id,
outCh,
)
if err != nil {
return nil, nil, err
return nil, err
}
outCh := make(chan infinitime.MotionValues, 2)
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
return outCh, nil
}

View File

@@ -1,22 +1,23 @@
package main
import (
"context"
"sync"
"github.com/godbus/dbus/v5"
"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.
conn, err := newSystemBusConn()
conn, err := newSystemBusConn(ctx)
if err != nil {
return err
}
// Check if modem manager interface exists
exists, err := modemManagerExists(conn)
exists, err := modemManagerExists(ctx, conn)
if err != nil {
return err
}
@@ -28,7 +29,7 @@ func initCallNotifs(dev *infinitime.Device) error {
}
// Connect to system bus. This connection is for monitoring.
monitorConn, err := newSystemBusConn()
monitorConn, err := newSystemBusConn(ctx)
if err != nil {
return err
}
@@ -78,13 +79,13 @@ func initCallNotifs(dev *infinitime.Device) error {
switch res {
case infinitime.CallStatusAccepted:
// Attempt to accept call
err = acceptCall(conn, callObj)
err = acceptCall(ctx, conn, callObj)
if err != nil {
log.Warn().Err(err).Msg("Error accepting call")
}
case infinitime.CallStatusDeclined:
// Attempt to decline call
err = declineCall(conn, callObj)
err = declineCall(ctx, conn, callObj)
if err != nil {
log.Warn().Err(err).Msg("Error declining call")
}
@@ -101,9 +102,11 @@ func initCallNotifs(dev *infinitime.Device) error {
return nil
}
func modemManagerExists(conn *dbus.Conn) (bool, error) {
func modemManagerExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
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 {
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
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 := callObj.Call("org.freedesktop.ModemManager1.Call.Accept", 0)
call := callObj.CallWithContext(
ctx, "org.freedesktop.ModemManager1.Call.Accept", 0,
)
if call.Err != nil {
return call.Err
}
@@ -132,9 +137,11 @@ func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
}
// 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 := callObj.Call("org.freedesktop.ModemManager1.Call.Hangup", 0)
call := callObj.CallWithContext(
ctx, "org.freedesktop.ModemManager1.Call.Hangup", 0,
)
if call.Err != nil {
return call.Err
}

View File

@@ -6,11 +6,26 @@ import (
"time"
"github.com/cheggaaa/pb/v3"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"go.arsenm.dev/itd/api"
)
func fwUpgrade(c *cli.Context) error {
resources := c.String("resources")
if resources != "" {
absRes, err := filepath.Abs(resources)
if err != nil {
return err
}
err = resLoad(c.Context, []string{absRes})
if err != nil {
log.Error().Msg("Resource loading has returned an error. This can happen if your current version of InfiniTime doesn't support BLE FS. Try updating without resource loading, and then load them after using the `itctl res load` command.")
return err
}
}
start := time.Now()
var upgType api.UpgradeType
@@ -28,7 +43,7 @@ func fwUpgrade(c *cli.Context) error {
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 {
return err
}
@@ -58,7 +73,7 @@ func fwUpgrade(c *cli.Context) error {
}
func fwVersion(c *cli.Context) error {
version, err := client.Version()
version, err := client.Version(c.Context)
if err != nil {
return err
}

View File

@@ -17,7 +17,7 @@ func fsList(c *cli.Context) error {
dirPath = c.Args().Get(0)
}
listing, err := client.ReadDir(dirPath)
listing, err := client.ReadDir(c.Context, dirPath)
if err != nil {
return err
}
@@ -34,7 +34,12 @@ func fsMkdir(c *cli.Context) error {
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 {
return err
}
@@ -47,7 +52,7 @@ func fsMove(c *cli.Context) error {
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 {
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 {
return err
}
@@ -109,7 +114,12 @@ func fsRemove(c *cli.Context) error {
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 {
return err
}
@@ -144,7 +154,7 @@ func fsWrite(c *cli.Context) error {
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 {
return err
}

View File

@@ -9,7 +9,7 @@ import (
)
func getAddress(c *cli.Context) error {
address, err := client.Address()
address, err := client.Address(c.Context)
if err != nil {
return err
}
@@ -19,7 +19,7 @@ func getAddress(c *cli.Context) error {
}
func getBattery(c *cli.Context) error {
battLevel, err := client.BatteryLevel()
battLevel, err := client.BatteryLevel(c.Context)
if err != nil {
return err
}
@@ -30,7 +30,7 @@ func getBattery(c *cli.Context) error {
}
func getHeart(c *cli.Context) error {
heartRate, err := client.HeartRate()
heartRate, err := client.HeartRate(c.Context)
if err != nil {
return err
}
@@ -41,7 +41,7 @@ func getHeart(c *cli.Context) error {
}
func getMotion(c *cli.Context) error {
motionVals, err := client.Motion()
motionVals, err := client.Motion(c.Context)
if err != nil {
return err
}
@@ -60,7 +60,7 @@ func getMotion(c *cli.Context) error {
}
func getSteps(c *cli.Context) error {
stepCount, err := client.StepCount()
stepCount, err := client.StepCount(c.Context)
if err != nil {
return err
}

View File

@@ -1,9 +1,11 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -16,8 +18,24 @@ var client *api.Client
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
ctx := context.Background()
ctx, _ = signal.NotifyContext(
ctx,
syscall.SIGINT,
syscall.SIGTERM,
)
// This goroutine ensures that itctl will exit
// at most 200ms after the user sends SIGINT/SIGTERM.
go func() {
<-ctx.Done()
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}()
app := cli.App{
Name: "itctl",
Name: "itctl",
HideHelpCommand: true,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "socket-path",
@@ -27,6 +45,25 @@ func main() {
},
},
Commands: []*cli.Command{
{
Name: "help",
ArgsUsage: "<command>",
Usage: "Display help screen for a command",
Action: helpCmd,
},
{
Name: "resources",
Aliases: []string{"res"},
Usage: "Handle InfiniTime resource loading",
Subcommands: []*cli.Command{
{
Name: "load",
ArgsUsage: "<path>",
Usage: "Load an InifiniTime resources package",
Action: resourcesLoad,
},
},
},
{
Name: "filesystem",
Aliases: []string{"fs"},
@@ -40,6 +77,13 @@ func main() {
Action: fsList,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "parents",
Aliases: []string{"p"},
Usage: "Make parent directories if needed, no error if already existing",
},
},
Name: "mkdir",
ArgsUsage: "<paths...>",
Usage: "Create new directories",
@@ -60,6 +104,13 @@ func main() {
Action: fsRead,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "recursive",
Aliases: []string{"r", "R"},
Usage: "Remove directories and their contents recursively",
},
},
Name: "remove",
ArgsUsage: "<paths...>",
Aliases: []string{"rm"},
@@ -92,6 +143,11 @@ func main() {
Aliases: []string{"f"},
Usage: "Path to firmware image (.bin file)",
},
&cli.PathFlag{
Name: "resources",
Aliases: []string{"r"},
Usage: "Path to resources file (.zip file)",
},
&cli.PathFlag{
Name: "archive",
Aliases: []string{"a"},
@@ -221,11 +277,13 @@ func main() {
},
},
Before: func(c *cli.Context) error {
newClient, err := api.New(c.String("socket-path"))
if err != nil {
return err
if !isHelpCmd() {
newClient, err := api.New(c.String("socket-path"))
if err != nil {
return err
}
client = newClient
}
client = newClient
return nil
},
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 {
log.Fatal().Err(err).Msg("Error while running app")
}
}
func catchSignal(fn func()) {
sigCh := make(chan os.Signal, 1)
signal.Notify(
sigCh,
syscall.SIGINT,
syscall.SIGTERM,
)
go func() {
<-sigCh
fn()
os.Exit(0)
}()
func helpCmd(c *cli.Context) error {
cmdArgs := append([]string{os.Args[0]}, c.Args().Slice()...)
cmdArgs = append(cmdArgs, "-h")
return c.App.RunContext(c.Context, cmdArgs)
}
func isHelpCmd() bool {
if len(os.Args) == 1 {
return true
}
for _, arg := range os.Args {
if arg == "-h" || arg == "help" {
return true
}
}
return false
}

View File

@@ -8,7 +8,7 @@ func notify(c *cli.Context) error {
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 {
return err
}

57
cmd/itctl/resources.go Normal file
View File

@@ -0,0 +1,57 @@
package main
import (
"context"
"path/filepath"
"github.com/cheggaaa/pb/v3"
"github.com/urfave/cli/v2"
"go.arsenm.dev/infinitime"
)
func resourcesLoad(c *cli.Context) error {
return resLoad(c.Context, c.Args().Slice())
}
func resLoad(ctx context.Context, args []string) error {
if len(args) == 0 {
return cli.Exit("Command load requires one argument.", 1)
}
// Create progress bar templates
rmTmpl := `Removing {{string . "filename"}}`
upTmpl := `Uploading {{string . "filename"}} {{counters . }} B {{bar . "|" "-" (cycle .) " " "|"}} {{percent . }} {{rtime . "%s"}}`
// Start full bar at 0 total
bar := pb.ProgressBarTemplate(rmTmpl).Start(0)
path, err := filepath.Abs(args[0])
if err != nil {
return err
}
progCh, err := client.LoadResources(ctx, path)
if err != nil {
return err
}
for evt := range progCh {
if evt.Err != nil {
return evt.Err
}
if evt.Operation == infinitime.ResourceOperationRemoveObsolete {
bar.SetTemplateString(rmTmpl)
bar.Set("filename", evt.Name)
} else {
bar.SetTemplateString(upTmpl)
bar.Set("filename", evt.Name)
bar.SetTotal(evt.Total)
bar.SetCurrent(evt.Sent)
}
}
bar.Finish()
return nil
}

View File

@@ -13,12 +13,12 @@ func setTime(c *cli.Context) error {
}
if c.Args().Get(0) == "now" {
return client.SetTime(time.Now())
return client.SetTime(c.Context, time.Now())
} else {
parsed, err := time.Parse(time.RFC3339, c.Args().Get(0))
if err != nil {
return err
}
return client.SetTime(parsed)
return client.SetTime(c.Context, parsed)
}
}

View File

@@ -3,5 +3,5 @@ package main
import "github.com/urfave/cli/v2"
func updateWeather(c *cli.Context) error {
return client.WeatherUpdate()
return client.WeatherUpdate(c.Context)
}

View File

@@ -9,96 +9,100 @@ import (
)
func watchHeart(c *cli.Context) error {
heartCh, cancel, err := client.WatchHeartRate()
heartCh, err := client.WatchHeartRate(c.Context)
if err != nil {
return err
}
catchSignal(cancel)
for heartRate := range heartCh {
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint8{"heartRate": heartRate},
)
} else if c.Bool("shell") {
fmt.Printf("HEART_RATE=%d\n", heartRate)
} else {
fmt.Println(heartRate, "BPM")
for {
select {
case heartRate := <-heartCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint8{"heartRate": heartRate},
)
} else if c.Bool("shell") {
fmt.Printf("HEART_RATE=%d\n", heartRate)
} else {
fmt.Println(heartRate, "BPM")
}
case <-c.Done():
return nil
}
}
return nil
}
func watchBattLevel(c *cli.Context) error {
battLevelCh, cancel, err := client.WatchBatteryLevel()
battLevelCh, err := client.WatchBatteryLevel(c.Context)
if err != nil {
return err
}
catchSignal(cancel)
for battLevel := range battLevelCh {
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint8{"battLevel": battLevel},
)
} else if c.Bool("shell") {
fmt.Printf("BATTERY_LEVEL=%d\n", battLevel)
} else {
fmt.Printf("%d%%\n", battLevel)
for {
select {
case battLevel := <-battLevelCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint8{"battLevel": battLevel},
)
} else if c.Bool("shell") {
fmt.Printf("BATTERY_LEVEL=%d\n", battLevel)
} else {
fmt.Printf("%d%%\n", battLevel)
}
case <-c.Done():
return nil
}
}
return nil
}
func watchStepCount(c *cli.Context) error {
stepCountCh, cancel, err := client.WatchStepCount()
stepCountCh, err := client.WatchStepCount(c.Context)
if err != nil {
return err
}
catchSignal(cancel)
for stepCount := range stepCountCh {
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint32{"stepCount": stepCount},
)
} else if c.Bool("shell") {
fmt.Printf("STEP_COUNT=%d\n", stepCount)
} else {
fmt.Println(stepCount, "Steps")
for {
select {
case stepCount := <-stepCountCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint32{"stepCount": stepCount},
)
} else if c.Bool("shell") {
fmt.Printf("STEP_COUNT=%d\n", stepCount)
} else {
fmt.Println(stepCount, "Steps")
}
case <-c.Done():
return nil
}
}
return nil
}
func watchMotion(c *cli.Context) error {
motionCh, cancel, err := client.WatchMotion()
motionCh, err := client.WatchMotion(c.Context)
if err != nil {
return err
}
catchSignal(cancel)
for motionVals := range motionCh {
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(motionVals)
} else if c.Bool("shell") {
fmt.Printf(
"X=%d\nY=%d\nZ=%d\n",
motionVals.X,
motionVals.Y,
motionVals.Z,
)
} else {
fmt.Println(motionVals)
for {
select {
case motionVals := <-motionCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(motionVals)
} else if c.Bool("shell") {
fmt.Printf(
"X=%d\nY=%d\nZ=%d\n",
motionVals.X,
motionVals.Y,
motionVals.Z,
)
} else {
fmt.Println(motionVals)
}
case <-c.Done():
return nil
}
}
return nil
}

View File

@@ -28,10 +28,15 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
)
if err != nil {
// Create new label containing error text
errLbl := widget.NewLabel(err.Error())
errEntry := widget.NewEntry()
errEntry.SetText(err.Error())
// If text changed, change it back
errEntry.OnChanged = func(string) {
errEntry.SetText(err.Error())
}
// Create new dropdown containing error label
content.Add(widget.NewAccordion(
widget.NewAccordionItem("More Details", errLbl),
widget.NewAccordionItem("More Details", errEntry),
))
}
if fatal {
@@ -49,5 +54,4 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
// Show error dialog
dialog.NewCustom("Error", "Ok", content, parent).Show()
}
}

163
cmd/itgui/firmware.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"context"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
)
func firmwareTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
// Create select to chose between archive and files upgrade
typeSelect := widget.NewSelect([]string{"Archive", "Files"}, nil)
typeSelect.PlaceHolder = "Upgrade Type"
// Create map to store files
files := map[string]string{}
// Create and disable start button
startBtn := widget.NewButton("Start", nil)
startBtn.Disable()
// Create new file open dialog for archive
archiveDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
if err != nil || uc == nil {
return
}
defer uc.Close()
// Set archive path in map
files[".zip"] = uc.URI().Path()
// Enable start button
startBtn.Enable()
}, w)
// Only allow .zip files
archiveDlg.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
// Create button to show dialog
archiveBtn := widget.NewButton("Select Archive (.zip)", archiveDlg.Show)
// Create new file open dialog for firmware image
imageDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
if err != nil || uc == nil {
return
}
defer uc.Close()
// Set firmware image path in map
files[".bin"] = uc.URI().Path()
// If the init packet was already selected
_, datOk := files[".dat"]
if datOk {
// Enable start button
startBtn.Enable()
}
}, w)
// Only allow .bin files
imageDlg.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
// Create button to show dialog
imageBtn := widget.NewButton("Select Firmware (.bin)", imageDlg.Show)
// Create new file open dialog for init packet
initDlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
if err != nil || uc == nil {
return
}
defer uc.Close()
// Set init packet path in map
files[".dat"] = uc.URI().Path()
// If the firmware image was already selected
_, binOk := files[".bin"]
if binOk {
// Enable start button
startBtn.Enable()
}
}, w)
// Only allow .dat files
initDlg.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
// Create button to show dialog
initBtn := widget.NewButton("Select Init Packet (.dat)", initDlg.Show)
var upgType api.UpgradeType = 255
// When upgrade type changes
typeSelect.OnChanged = func(s string) {
// Delete all files from map
delete(files, ".bin")
delete(files, ".dat")
delete(files, ".zip")
// Hide all dialog buttons
imageBtn.Hide()
initBtn.Hide()
archiveBtn.Hide()
// Disable start button
startBtn.Disable()
switch s {
case "Files":
// Set file upgrade type
upgType = api.UpgradeTypeFiles
// Show firmware image and init packet buttons
imageBtn.Show()
initBtn.Show()
case "Archive":
// Set archive upgrade type
upgType = api.UpgradeTypeArchive
// Show archive button
archiveBtn.Show()
}
}
// Select archive by default
typeSelect.SetSelectedIndex(0)
// When start button pressed
startBtn.OnTapped = func() {
var args []string
// Append the appropriate files for upgrade type
switch upgType {
case api.UpgradeTypeArchive:
args = append(args, files[".zip"])
case api.UpgradeTypeFiles:
args = append(args, files[".dat"], files[".bin"])
}
// If args are nil (invalid upgrade type)
if args == nil {
return
}
// Create new progress dialog
progress := newProgress(w)
// Start firmware upgrade
progressCh, err := client.FirmwareUpgrade(ctx, upgType, args...)
if err != nil {
guiErr(err, "Error performing firmware upgrade", false, w)
return
}
// Show progress dialog
progress.Show()
// For every progress event
for progressEvt := range progressCh {
// Set progress bar values
progress.SetTotal(float64(progressEvt.Total))
progress.SetValue(float64(progressEvt.Sent))
}
// Hide progress dialog
progress.Hide()
}
return container.NewVBox(
layout.NewSpacer(),
typeSelect,
archiveBtn,
imageBtn,
initBtn,
startBtn,
layout.NewSpacer(),
)
}

407
cmd/itgui/fs.go Normal file
View File

@@ -0,0 +1,407 @@
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/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/infinitime"
"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.FileApplicationIcon(),
func() {
dlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
if err != nil || uc == nil {
return
}
resPath := uc.URI().Path()
uc.Close()
progressDlg := newProgress(w)
progressDlg.Show()
progCh, err := client.LoadResources(ctx, resPath)
if err != nil {
guiErr(err, "Error loading resources", false, w)
return
}
for evt := range progCh {
if evt.Err != nil {
guiErr(evt.Err, "Error loading resources", false, w)
return
}
switch evt.Operation {
case infinitime.ResourceOperationRemoveObsolete:
progressDlg.SetText("Removing " + evt.Name)
case infinitime.ResourceOperationUpload:
progressDlg.SetText("Uploading " + evt.Name)
progressDlg.SetTotal(float64(evt.Total))
progressDlg.SetValue(float64(evt.Sent))
}
}
progressDlg.Hide()
refresh(ctx, cwdData, lsData, client, w, c)
}, w)
dlg.SetConfirmText("Upload Resources")
dlg.SetFilter(storage.NewExtensionFileFilter([]string{
".zip",
}))
dlg.Show()
},
),
widget.NewToolbarAction(
theme.UploadIcon(),
func() {
// Create open dialog for file that will be uploaded
dlg := dialog.NewFileOpen(func(uc fyne.URIReadCloser, err error) {
if err != nil || uc == nil {
return
}
// Get filepath and close
localPath := uc.URI().Path()
uc.Close()
// Create new entry to store filepath
filenameEntry := widget.NewEntry()
// Set entry text to the file name of the selected file
filenameEntry.SetText(filepath.Base(localPath))
// Create new dialog asking for the filename of the file to be stored on the watch
uploadDlg := dialog.NewForm("Upload", "Upload", "Cancel", []*widget.FormItem{
widget.NewFormItem("Filename", filenameEntry),
}, func(ok bool) {
if !ok {
return
}
// Get current directory
cwd, _ := cwdData.Get()
// Get remote path by joining current directory with filename
remotePath := filepath.Join(cwd, filenameEntry.Text)
// Create new progress dialog
progressDlg := newProgress(w)
progressDlg.Show()
// Upload file
progressCh, err := client.Upload(ctx, remotePath, localPath)
if err != nil {
guiErr(err, "Error uploading file", false, w)
return
}
for progressEvt := range progressCh {
progressDlg.SetTotal(float64(progressEvt.Total))
progressDlg.SetValue(float64(progressEvt.Sent))
if progressEvt.Sent == progressEvt.Total {
break
}
}
// Close progress dialog
progressDlg.Hide()
// Add file to listing (avoids full refresh)
lsData.Append(api.FileInfo{
IsDir: false,
Name: filepath.Base(remotePath),
})
}, w)
uploadDlg.Show()
}, w)
dlg.Show()
},
),
widget.NewToolbarAction(
theme.FolderNewIcon(),
func() {
// Create new entry for filename
filenameEntry := widget.NewEntry()
// Create new dialog to ask for the filename
mkdirDialog := dialog.NewForm("Make Directory", "Create", "Cancel", []*widget.FormItem{
widget.NewFormItem("Filename", filenameEntry),
}, func(ok bool) {
if !ok {
return
}
// Get current directory
cwd, _ := cwdData.Get()
// Get remote path by joining current directory and filename
remotePath := filepath.Join(cwd, filenameEntry.Text)
// Make directory
err := client.Mkdir(ctx, remotePath)
if err != nil {
guiErr(err, "Error creating directory", false, w)
return
}
// Add directory to listing (avoids full refresh)
lsData.Append(api.FileInfo{
IsDir: true,
Name: filepath.Base(remotePath),
})
}, w)
mkdirDialog.Show()
},
),
)
// Add listener to listing data to create the new items on the GUI
// whenever the listing changes
lsData.AddListener(binding.NewDataListener(func() {
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
c.Refresh()
}))
return container.NewBorder(
nil,
toolbar,
nil,
nil,
container.NewVScroll(c),
)
}
// makeItems creates GUI objects from listing data
func makeItems(
ctx context.Context,
client *api.Client,
lsData binding.UntypedList,
cwdData binding.String,
w fyne.Window,
c *fyne.Container,
) []fyne.CanvasObject {
// Get listing data
ls, _ := lsData.Get()
// Create output slice with dame length as listing
out := make([]fyne.CanvasObject, len(ls))
for index, val := range ls {
// Assert value as file info
item := val.(api.FileInfo)
var icon fyne.Resource
// Decide which icon to use
if item.IsDir {
if item.Name == ".." {
icon = theme.NavigateBackIcon()
} else {
icon = theme.FolderIcon()
}
} else {
icon = theme.FileIcon()
}
// Create new button with the decided icon and the item name
btn := widget.NewButtonWithIcon(item.Name, icon, nil)
// Align left
btn.Alignment = widget.ButtonAlignLeading
// Decide which callback function to use
if item.IsDir {
btn.OnTapped = func() {
// Get current directory
cwd, _ := cwdData.Get()
// Join current directory with item name
cwd = filepath.Join(cwd, item.Name)
// Set new current directory
cwdData.Set(cwd)
// Refresh GUI to display new directory
refresh(ctx, cwdData, lsData, client, w, c)
}
} else {
btn.OnTapped = func() {
// Get current directory
cwd, _ := cwdData.Get()
// Join current directory with item name
remotePath := filepath.Join(cwd, item.Name)
// Create new save dialog
dlg := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
if err != nil || uc == nil {
return
}
// Get path of selected file
localPath := uc.URI().Path()
// Close WriteCloser (it's not needed)
uc.Close()
// Create new progress dialog
progressDlg := newProgress(w)
progressDlg.Show()
// Download file
progressCh, err := client.Download(ctx, localPath, remotePath)
if err != nil {
guiErr(err, "Error downloading file", false, w)
return
}
// For every progress event
for progressEvt := range progressCh {
progressDlg.SetTotal(float64(progressEvt.Total))
progressDlg.SetValue(float64(progressEvt.Sent))
}
// Close progress dialog
progressDlg.Hide()
}, w)
// Set filename to the item name
dlg.SetFileName(item.Name)
dlg.Show()
}
}
if item.Name == ".." {
out[index] = btn
continue
}
moveBtn := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
moveEntry := widget.NewEntry()
dlg := dialog.NewForm("Move", "Move", "Cancel", []*widget.FormItem{
widget.NewFormItem("New Path", moveEntry),
}, func(ok bool) {
if !ok {
return
}
// Get current directory
cwd, _ := cwdData.Get()
// Join current directory with item name
oldPath := filepath.Join(cwd, item.Name)
// Rename file
err := client.Rename(ctx, oldPath, moveEntry.Text)
if err != nil {
guiErr(err, "Error renaming file", false, w)
return
}
// Refresh GUI
refresh(ctx, cwdData, lsData, client, w, c)
}, w)
dlg.Show()
})
removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), func() {
// Get current directory
cwd, _ := cwdData.Get()
// Join current directory with item name
path := filepath.Join(cwd, item.Name)
// Remove file
err := client.Remove(ctx, path)
if err != nil {
guiErr(err, "Error removing file", false, w)
return
}
// Refresh GUI
refresh(ctx, cwdData, lsData, client, w, c)
})
// Add button to GUI component list
out[index] = container.NewBorder(
nil,
nil,
nil,
container.NewHBox(moveBtn, removeBtn),
btn,
)
}
return out
}
func refresh(
ctx context.Context,
cwdData binding.String,
lsData binding.UntypedList,
client *api.Client,
w fyne.Window,
c *fyne.Container,
) {
// Create and show new loading pop up
loading := newLoadingPopUp(w)
loading.Show()
// Close pop up at the end of the function
defer loading.Hide()
// Get current directory
cwd, _ := cwdData.Get()
// Read directory
ls, err := client.ReadDir(ctx, cwd)
if err != nil {
guiErr(err, "Error reading directory", false, w)
return
}
// Set new listing data
lsData.Set(lsToAny(ls))
// Create new GUI objects
c.Objects = makeItems(ctx, client, lsData, cwdData, w, c)
// Refresh GUI
c.Refresh()
}
func lsToAny(ls []api.FileInfo) []interface{} {
out := make([]interface{}, len(ls)-1)
for i, e := range ls {
// Skip first element as it is always "."
if i == 0 {
continue
}
out[i-1] = e
}
return out
}

150
cmd/itgui/graph.go Normal file
View File

@@ -0,0 +1,150 @@
package main
import (
"context"
"database/sql"
"image/color"
"os"
"path/filepath"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/x/fyne/widget/charts"
"go.arsenm.dev/itd/api"
_ "modernc.org/sqlite"
)
func graphTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
// Get user configuration directory
userCfgDir, err := os.UserConfigDir()
if err != nil {
return nil
}
cfgDir := filepath.Join(userCfgDir, "itd")
dbPath := filepath.Join(cfgDir, "metrics.db")
// If stat on database returns error, return nil
if _, err := os.Stat(dbPath); err != nil {
return nil
}
// Open database
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil
}
// Get heart rate data and create chart
heartRateData := getData(db, "bpm", "heartRate")
heartRate := newLineChartData(nil, heartRateData)
// Get step count data and create chart
stepCountData := getData(db, "steps", "stepCount")
stepCount := newLineChartData(nil, stepCountData)
// Get battery level data and create chart
battLevelData := getData(db, "percent", "battLevel")
battLevel := newLineChartData(nil, battLevelData)
// Get motion data
motionData := getMotionData(db)
// Create chart for each coordinate
xChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorRed), motionData["X"])
yChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorGreen), motionData["Y"])
zChart := newLineChartData(theme.PrimaryColorNamed(theme.ColorBlue), motionData["Z"])
// Create new max container with all the charts
motion := container.NewMax(xChart, yChart, zChart)
// Create tabs for charts
chartTabs := container.NewAppTabs(
container.NewTabItem("Heart Rate", heartRate),
container.NewTabItem("Step Count", stepCount),
container.NewTabItem("Battery Level", battLevel),
container.NewTabItem("Motion", motion),
)
// Place tabs on left
chartTabs.SetTabLocation(container.TabLocationLeading)
return chartTabs
}
func newLineChartData(col color.Color, data []float64) *charts.LineChart {
// Create new line chart
lc := charts.NewLineChart(nil)
setOpts(lc, col)
// If no data, make the stroke transparent
if len(data) == 0 {
lc.Options().StrokeColor = color.RGBA{0, 0, 0, 0}
}
// Set data
lc.SetData(data)
return lc
}
func setOpts(lc *charts.LineChart, col color.Color) {
// Get pointer to options
opts := lc.Options()
// Set fill color to transparent
opts.FillColor = color.RGBA{0, 0, 0, 0}
// Set stroke width
opts.StrokeWidth = 2
// If color provided
if col != nil {
// Set stroke color
opts.StrokeColor = col
} else {
// Set stroke color to orange primary color
opts.StrokeColor = theme.PrimaryColorNamed(theme.ColorOrange)
}
}
func getData(db *sql.DB, field, table string) []float64 {
// Get data from database
rows, err := db.Query("SELECT " + field + " FROM " + table + " ORDER BY time;")
if err != nil {
return nil
}
defer rows.Close()
var out []float64
for rows.Next() {
var val int64
// Scan data into int
err := rows.Scan(&val)
if err != nil {
return nil
}
// Convert to float64 and append to data slice
out = append(out, float64(val))
}
return out
}
func getMotionData(db *sql.DB) map[string][]float64 {
// Get data from database
rows, err := db.Query("SELECT X, Y, Z FROM motion ORDER BY time;")
if err != nil {
return nil
}
defer rows.Close()
out := map[string][]float64{}
for rows.Next() {
var x, y, z int64
// Scan data into ints
err := rows.Scan(&x, &y, &z)
if err != nil {
return nil
}
// Convert to float64 and append to appropriate slice
out["X"] = append(out["X"], float64(x))
out["Y"] = append(out["Y"], float64(y))
out["Z"] = append(out["Z"], float64(z))
}
return out
}

View File

@@ -1,123 +1,86 @@
package main
import (
"context"
"fmt"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"go.arsenm.dev/itd/api"
)
func infoTab(parent fyne.Window, client *api.Client) *fyne.Container {
infoLayout := container.NewVBox(
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
)
func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
c := container.NewVBox()
// Create label for heart rate
heartRateLbl := newText("0 BPM", 24)
// Creae container to store heart rate section
heartRateSect := container.NewVBox(
newText("Heart Rate", 12),
heartRateLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(heartRateSect)
heartRateCh, cancel, err := client.WatchHeartRate()
// Create titled text for heart rate
heartRateText := newTitledText("Heart Rate", "0 BPM")
c.Add(heartRateText)
// Watch heart rate
heartRateCh, err := client.WatchHeartRate(ctx)
if err != nil {
guiErr(err, "Error getting heart rate channel", true, parent)
guiErr(err, "Error watching heart rate", true, w)
}
onClose = append(onClose, cancel)
go func() {
// For every heart rate sample
for heartRate := range heartRateCh {
// Change text of heart rate label
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate)
// Refresh label
heartRateLbl.Refresh()
// Set body of titled text
heartRateText.SetBody(fmt.Sprintf("%d BPM", heartRate))
}
}()
// Create label for heart rate
stepCountLbl := newText("0 Steps", 24)
// Creae container to store heart rate section
stepCountSect := container.NewVBox(
newText("Step Count", 12),
stepCountLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(stepCountSect)
stepCountCh, cancel, err := client.WatchStepCount()
// Create titled text for battery level
battLevelText := newTitledText("Battery Level", "0%")
c.Add(battLevelText)
// Watch battery level
battLevelCh, err := client.WatchBatteryLevel(ctx)
if err != nil {
guiErr(err, "Error getting step count channel", true, parent)
guiErr(err, "Error watching battery level", true, w)
}
onClose = append(onClose, cancel)
go func() {
for stepCount := range stepCountCh {
// Change text of heart rate label
stepCountLbl.Text = fmt.Sprintf("%d Steps", stepCount)
// Refresh label
stepCountLbl.Refresh()
}
}()
// Create label for battery level
battLevelLbl := newText("0%", 24)
// Create container to store battery level section
battLevel := container.NewVBox(
newText("Battery Level", 12),
battLevelLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(battLevel)
battLevelCh, cancel, err := client.WatchBatteryLevel()
if err != nil {
guiErr(err, "Error getting battery level channel", true, parent)
}
onClose = append(onClose, cancel)
go func() {
// For every battery level sample
for battLevel := range battLevelCh {
// Change text of battery level label
battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel)
// Refresh label
battLevelLbl.Refresh()
// Set body of titled text
battLevelText.SetBody(fmt.Sprintf("%d%%", battLevel))
}
}()
fwVerString, err := client.Version()
// Create titled text for step count
stepCountText := newTitledText("Step Count", "0 Steps")
c.Add(stepCountText)
// Watch step count
stepCountCh, err := client.WatchStepCount(ctx)
if err != nil {
guiErr(err, "Error getting firmware string", true, parent)
guiErr(err, "Error watching step count", true, w)
}
go func() {
// For every step count sample
for stepCount := range stepCountCh {
// Set body of titled text
stepCountText.SetBody(fmt.Sprintf("%d Steps", stepCount))
}
}()
fwVer := container.NewVBox(
newText("Firmware Version", 12),
newText(fwVerString, 24),
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(fwVer)
btAddrString, err := client.Address()
// Create new titled text for address
addressText := newTitledText("Address", "")
c.Add(addressText)
// Get address
address, err := client.Address(ctx)
if err != nil {
panic(err)
guiErr(err, "Error getting address", true, w)
}
// Set body of titled text
addressText.SetBody(address)
btAddr := container.NewVBox(
newText("Bluetooth Address", 12),
newText(btAddrString, 24),
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(btAddr)
// Create new titled text for version
versionText := newTitledText("Version", "")
c.Add(versionText)
// Get version
version, err := client.Version(ctx)
if err != nil {
guiErr(err, "Error getting version", true, w)
}
// Set body of titled text
versionText.SetBody(version)
return infoLayout
}
func newText(t string, size float32) *canvas.Text {
text := canvas.NewText(t, theme.ForegroundColor())
text.TextSize = size
return text
return container.NewVScroll(c)
}

21
cmd/itgui/loading.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func newLoadingPopUp(w fyne.Window) *widget.PopUp {
pb := widget.NewProgressBarInfinite()
rect := canvas.NewRectangle(color.Transparent)
rect.SetMinSize(fyne.NewSize(200, 0))
return widget.NewModalPopUp(
container.NewMax(rect, pb),
w.Canvas(),
)
}

View File

@@ -1,43 +1,60 @@
package main
import (
"context"
"sync"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"go.arsenm.dev/itd/api"
)
var onClose []func()
func main() {
// Create new app
a := app.New()
// Create new window with title "itgui"
window := a.NewWindow("itgui")
window.SetOnClosed(func() {
for _, closeFn := range onClose {
closeFn()
}
})
w := a.NewWindow("itgui")
// Create new context for use with the API client
ctx, cancel := context.WithCancel(context.Background())
// Connect to ITD API
client, err := api.New(api.DefaultAddr)
if err != nil {
guiErr(err, "Error connecting to itd", true, window)
guiErr(err, "Error connecting to ITD", true, w)
}
onClose = append(onClose, func() {
client.Close()
})
// Create new app tabs container
// Create channel to signal that the fs tab has been opened
fsOpened := make(chan struct{})
fsOnce := &sync.Once{}
// Create app tabs
tabs := container.NewAppTabs(
container.NewTabItem("Info", infoTab(window, client)),
container.NewTabItem("Motion", motionTab(window, client)),
container.NewTabItem("Notify", notifyTab(window, client)),
container.NewTabItem("Set Time", timeTab(window, client)),
container.NewTabItem("Upgrade", upgradeTab(window, client)),
container.NewTabItem("Info", infoTab(ctx, client, w)),
container.NewTabItem("Motion", motionTab(ctx, client, w)),
container.NewTabItem("Notify", notifyTab(ctx, client, w)),
container.NewTabItem("FS", fsTab(ctx, client, w, fsOpened)),
container.NewTabItem("Time", timeTab(ctx, client, w)),
container.NewTabItem("Firmware", firmwareTab(ctx, client, w)),
)
// Set tabs as window content
window.SetContent(tabs)
// Show window and run app
window.ShowAndRun()
metricsTab := graphTab(ctx, client, w)
if metricsTab != nil {
tabs.Append(container.NewTabItem("Metrics", metricsTab))
}
// When a tab is selected
tabs.OnSelected = func(ti *container.TabItem) {
// If the tab's name is FS
if ti.Text == "FS" {
// Signal fsOpened only once
fsOnce.Do(func() {
fsOpened <- struct{}{}
})
}
}
// Cancel context on close
w.SetOnClosed(cancel)
// Set content and show window
w.SetContent(tabs)
w.ShowAndRun()
}

View File

@@ -1,105 +1,62 @@
package main
import (
"image/color"
"strconv"
"context"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
)
func motionTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Create label for heart rate
xCoordLbl := newText("0", 24)
// Creae container to store heart rate section
xCoordSect := container.NewVBox(
newText("X Coordinate", 12),
xCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
// Create titledText for each coordinate
xText := newTitledText("X Coordinate", "0")
yText := newTitledText("Y Coordinate", "0")
zText := newTitledText("Z Coordinate", "0")
// Create label for heart rate
yCoordLbl := newText("0", 24)
// Creae container to store heart rate section
yCoordSect := container.NewVBox(
newText("Y Coordinate", 12),
yCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create label for heart rate
zCoordLbl := newText("0", 24)
// Creae container to store heart rate section
zCoordSect := container.NewVBox(
newText("Z Coordinate", 12),
zCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
var ctxCancel func()
// Create variable to keep track of whether motion started
started := false
// Create button to stop motion
stopBtn := widget.NewButton("Stop", nil)
// Create button to start motion
startBtn := widget.NewButton("Start", func() {
// if motion is started
if started {
// Do nothing
return
}
// Set motion started
started = true
// Watch motion values
motionCh, cancel, err := client.WatchMotion()
if err != nil {
guiErr(err, "Error getting heart rate channel", true, parent)
}
// Create done channel
done := make(chan struct{}, 1)
go func() {
for {
select {
case <-done:
return
case motion := <-motionCh:
// Set labels to new values
xCoordLbl.Text = strconv.Itoa(int(motion.X))
yCoordLbl.Text = strconv.Itoa(int(motion.Y))
zCoordLbl.Text = strconv.Itoa(int(motion.Z))
// Refresh labels to display new values
xCoordLbl.Refresh()
yCoordLbl.Refresh()
zCoordLbl.Refresh()
}
// Create start button
toggleBtn := widget.NewButton("Start", nil)
// Set button's on tapped callback
toggleBtn.OnTapped = func() {
switch toggleBtn.Text {
case "Start":
// Create new context for motion
motionCtx, cancel := context.WithCancel(ctx)
// Set ctxCancel to function so that stop button can run it
ctxCancel = cancel
// Watch motion
motionCh, err := client.WatchMotion(motionCtx)
if err != nil {
guiErr(err, "Error watching motion", false, w)
return
}
}()
// Create stop function
stopBtn.OnTapped = func() {
done <- struct{}{}
started = false
cancel()
go func() {
// For every motion event
for motion := range motionCh {
// Set coordinates
xText.SetBody(fmt.Sprint(motion.X))
yText.SetBody(fmt.Sprint(motion.Y))
zText.SetBody(fmt.Sprint(motion.Z))
}
}()
// Set button text to "Stop"
toggleBtn.SetText("Stop")
case "Stop":
// Cancel motion context
ctxCancel()
// Set button text to "Start"
toggleBtn.SetText("Start")
}
}
})
// Run stop button function on close if possible
onClose = append(onClose, func() {
if stopBtn.OnTapped != nil {
stopBtn.OnTapped()
}
})
// Return new container containing all elements
return container.NewVBox(
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
startBtn,
stopBtn,
xCoordSect,
yCoordSect,
zCoordSect,
)
return container.NewVScroll(container.NewVBox(
toggleBtn,
xText,
yText,
zText,
))
}

View File

@@ -1,6 +1,8 @@
package main
import (
"context"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
@@ -8,30 +10,31 @@ import (
"go.arsenm.dev/itd/api"
)
func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Create new entry for notification title
func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
c := container.NewVBox()
c.Add(layout.NewSpacer())
// Create new entry for title
titleEntry := widget.NewEntry()
titleEntry.SetPlaceHolder("Title")
c.Add(titleEntry)
// Create multiline entry for notification body
// Create new multiline entry for body
bodyEntry := widget.NewMultiLineEntry()
bodyEntry.SetPlaceHolder("Body")
c.Add(bodyEntry)
// Create new button to send notification
// Create new send button
sendBtn := widget.NewButton("Send", func() {
err := client.Notify(titleEntry.Text, bodyEntry.Text)
// Send notification
err := client.Notify(ctx, titleEntry.Text, bodyEntry.Text)
if err != nil {
guiErr(err, "Error sending notification", false, parent)
guiErr(err, "Error sending notification", false, w)
return
}
})
c.Add(sendBtn)
// Return new container containing all elements
return container.NewVBox(
layout.NewSpacer(),
titleEntry,
bodyEntry,
sendBtn,
layout.NewSpacer(),
)
c.Add(layout.NewSpacer())
return container.NewVScroll(c)
}

64
cmd/itgui/progress.go Normal file
View File

@@ -0,0 +1,64 @@
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
progLbl *widget.Label
pb *widget.ProgressBar
*widget.PopUp
}
func newProgress(w fyne.Window) progress {
out := progress{}
out.lbl = widget.NewLabel("")
out.lbl.Hide()
// Create label to show how many bytes transfered and center it
out.progLbl = widget.NewLabel("0 / 0 B")
out.progLbl.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.progLbl, out.pb)
// Create popup
out.PopUp = widget.NewModalPopUp(container.NewMax(l, sizeRect), w.Canvas())
return out
}
func (p progress) SetText(s string) {
p.lbl.SetText(s)
if s == "" {
p.lbl.Hide()
} else {
p.lbl.Show()
}
}
func (p progress) SetTotal(v float64) {
p.pb.Max = v
p.pb.Refresh()
p.progLbl.SetText(fmt.Sprintf("%.0f / %.0f B", p.pb.Value, v))
}
func (p progress) SetValue(v float64) {
p.pb.SetValue(v)
p.progLbl.SetText(fmt.Sprintf("%.0f / %.0f B", v, p.pb.Max))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"time"
"fyne.io/fyne/v2"
@@ -10,51 +11,47 @@ import (
"go.arsenm.dev/itd/api"
)
func timeTab(parent fyne.Window, client *api.Client) *fyne.Container {
// Create new entry for time string
func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
c := container.NewVBox()
c.Add(layout.NewSpacer())
// Create entry for time string
timeEntry := widget.NewEntry()
// Set text to current time formatter properly
timeEntry.SetText(time.Now().Format(time.RFC1123))
timeEntry.SetPlaceHolder("RFC1123")
// Create button to set current time
currentBtn := widget.NewButton("Set Current", func() {
timeEntry.SetText(time.Now().Format(time.RFC1123))
setTime(client, true)
})
// Create button to set time inside entry
timeBtn := widget.NewButton("Set", func() {
// Parse time as RFC1123 string
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
setCurrentBtn := widget.NewButton("Set current time", func() {
// Set current time
err := client.SetTime(ctx, time.Now())
if err != nil {
guiErr(err, "Error parsing time string", false, parent)
guiErr(err, "Error setting time", false, w)
return
}
// Set time to parsed time
setTime(client, false, parsedTime)
// Set time entry to current time
timeEntry.SetText(time.Now().Format(time.RFC1123))
})
// Return new container with all elements centered
return container.NewVBox(
layout.NewSpacer(),
timeEntry,
currentBtn,
timeBtn,
layout.NewSpacer(),
)
}
// Create button to set time from entry
setBtn := widget.NewButton("Set", func() {
// Parse RFC1123 time string in entry
newTime, err := time.Parse(time.RFC1123, timeEntry.Text)
if err != nil {
guiErr(err, "Error parsing time string", false, w)
return
}
// Set time from parsed string
err = client.SetTime(ctx, newTime)
if err != nil {
guiErr(err, "Error setting time", false, w)
return
}
})
// setTime sets the first element in the variadic parameter
// if current is false, otherwise, it sets the current time.
func setTime(client *api.Client, current bool, t ...time.Time) error {
var err error
if current {
err = client.SetTime(time.Now())
} else {
err = client.SetTime(t[0])
}
if err != nil {
return err
}
return nil
c.Add(timeEntry)
c.Add(setBtn)
c.Add(setCurrentBtn)
c.Add(layout.NewSpacer())
return c
}

35
cmd/itgui/titledText.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import "fyne.io/fyne/v2/widget"
type titledText struct {
*widget.RichText
}
func newTitledText(title, text string) titledText {
titleStyle := widget.RichTextStyleHeading
titleStyle.TextStyle.Bold = false
return titledText{
widget.NewRichText(
&widget.TextSegment{
Style: widget.RichTextStyleParagraph,
Text: title,
},
&widget.TextSegment{
Style: titleStyle,
Text: text,
},
&widget.SeparatorSegment{},
),
}
}
func (t titledText) SetTitle(s string) {
t.RichText.Segments[0].(*widget.TextSegment).Text = s
t.Refresh()
}
func (t titledText) SetBody(s string) {
t.RichText.Segments[1].(*widget.TextSegment).Text = s
t.Refresh()
}

View File

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

View File

@@ -13,22 +13,47 @@ import (
"github.com/rs/zerolog/log"
)
var cfgDir string
func init() {
// Set up logger
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
// Get user's configuration directory
cfgDir, err := os.UserConfigDir()
userCfgDir, err := os.UserConfigDir()
if err != nil {
panic(err)
}
cfgDir = filepath.Join(userCfgDir, "itd")
// If config dir is not readable
if _, err = os.ReadDir(cfgDir); err != nil {
// Create config dir with 700 permissions
err = os.MkdirAll(cfgDir, 0700)
if err != nil {
panic(err)
}
}
// Get current and old config paths
cfgPath := filepath.Join(cfgDir, "itd.toml")
oldCfgPath := filepath.Join(userCfgDir, "itd.toml")
// If old config path exists
if _, err = os.Stat(oldCfgPath); err == nil {
// Move old config to new path
err = os.Rename(oldCfgPath, cfgPath)
if err != nil {
panic(err)
}
}
// Set config defaults
setCfgDefaults()
// Load config files
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(cfgProvider, toml.Parser())
@@ -55,6 +80,8 @@ func cfgWatch(provider *file.File) {
func setCfgDefaults() {
k.Load(confmap.Provider(map[string]interface{}{
"bluetooth.adapter": "hci0",
"socket.path": "/tmp/itd/socket",
"conn.reconnect": true,

14
dbus.go
View File

@@ -1,10 +1,14 @@
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
conn, err := dbus.SystemBusPrivate()
conn, err := dbus.SystemBusPrivate(dbus.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -19,9 +23,9 @@ func newSystemBusConn() (*dbus.Conn, error) {
return conn, nil
}
func newSessionBusConn() (*dbus.Conn, error) {
func newSessionBusConn(ctx context.Context) (*dbus.Conn, error) {
// Connect to dbus session bus
conn, err := dbus.SessionBusPrivate()
conn, err := dbus.SessionBusPrivate(dbus.WithContext(ctx))
if err != nil {
return nil, err
}

147
go.mod
View File

@@ -1,122 +1,83 @@
module gitea.arsenm.dev/cpyarger/itd
module go.arsenm.dev/itd
go 1.17
replace fyne.io/x/fyne => github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb
require (
fyne.io/fyne/v2 v2.1.2
fyne.io/fyne/v2 v2.2.3
fyne.io/x/fyne v0.0.0-20220107050838-c4a1de51d4ce
github.com/cheggaaa/pb/v3 v3.0.8
github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b
github.com/godbus/dbus/v5 v5.0.6
github.com/google/uuid v1.3.0
github.com/godbus/dbus/v5 v5.1.0
github.com/knadh/koanf v1.4.0
github.com/mattn/go-isatty v0.0.14
github.com/mozillazg/go-pinyin v0.19.0
github.com/rs/zerolog v1.26.1
github.com/smallnest/rpcx v1.7.4
github.com/urfave/cli/v2 v2.3.0
github.com/vmihailenco/msgpack/v5 v5.3.5
gitea.arsenm.dev/Arsen6331/infinitime v0.0.0-20220424030849-6c3f1b14c948
github.com/urfave/cli/v2 v2.4.0
go.arsenm.dev/infinitime v0.0.0-20221107042015-72b558707ee3
go.arsenm.dev/lrpc v0.0.0-20220513001344-3bcc01fdb6a0
golang.org/x/text v0.3.7
modernc.org/sqlite v1.17.2
)
require (
github.com/VividCortex/ewma v1.2.0 // 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
fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 // indirect
github.com/VividCortex/ewma v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0 // 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/color v1.10.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // 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-logr/logr v1.2.3 // 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/golang/snappy v0.0.4 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/hashicorp/consul/api v1.12.0 // 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/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/miekg/dns v1.1.48 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muka/go-bluetooth v0.0.0-20220219050759-674a63b8741a // indirect
github.com/nxadm/tail v1.4.8 // 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/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/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/samuel/go-zookeeper v0.0.0-20201211165307-7117e9ea2414 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // 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/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // 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/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44 // indirect
github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41 // indirect
github.com/stretchr/testify v1.7.2 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xtaci/kcp-go v5.4.20+incompatible // 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/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
golang.org/x/tools v0.1.10 // indirect
github.com/yuin/goldmark v1.4.10 // indirect
golang.org/x/image v0.0.0-20220601225756-64ec528b34cd // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.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.1 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // 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
)

937
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,25 @@
[bluetooth]
adapter = "hci0"
[socket]
path = "/tmp/itd/socket"
[metrics]
enabled = false
[metrics.heartRate]
enabled = true
[metrics.stepCount]
enabled = true
[metrics.battLevel]
enabled = true
[metrics.motion]
# This may lower the battery life of the PineTime
enabled = false
[conn]
reconnect = true

49
main.go
View File

@@ -19,11 +19,14 @@
package main
import (
"context"
_ "embed"
"flag"
"fmt"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/gen2brain/dlgs"
@@ -31,14 +34,11 @@ import (
"github.com/mattn/go-isatty"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gitea.arsenm.dev/cpyarger/itd"
"go.arsenm.dev/infinitime"
)
var k = koanf.New(".")
//go:embed version.txt
var version string
var (
firmwareUpdating = false
// The FS must be updated when the watch is reconnected
@@ -60,7 +60,7 @@ func main() {
}
// Initialize infinitime library
infinitime.Init()
infinitime.Init(k.String("bluetooth.adapter"))
// Cleanly exit after function
defer infinitime.Exit()
@@ -74,8 +74,24 @@ func main() {
LogLevel: level,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
go func() {
<-sigCh
cancel()
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}()
signal.Notify(
sigCh,
syscall.SIGINT,
syscall.SIGTERM,
)
// Connect to InfiniTime with default options
dev, err := infinitime.Connect(opts)
dev, err := infinitime.Connect(ctx, opts)
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to InfiniTime")
}
@@ -136,29 +152,40 @@ func main() {
}
// Start control socket
err = initCallNotifs(dev)
err = initCallNotifs(ctx, dev)
if err != nil {
log.Error().Err(err).Msg("Error initializing call notifications")
}
// Initialize notification relay
err = initNotifRelay(dev)
err = initNotifRelay(ctx, dev)
if err != nil {
log.Error().Err(err).Msg("Error initializing notification relay")
}
// Initializa weather
err = initWeather(dev)
err = initWeather(ctx, dev)
if err != nil {
log.Error().Err(err).Msg("Error initializing weather")
}
// Initialize metrics collection
err = initMetrics(ctx, dev)
if err != nil {
log.Error().Err(err).Msg("Error intializing metrics collection")
}
// Initialize metrics collection
err = initPureMaps(ctx, dev)
if err != nil {
log.Error().Err(err).Msg("Error intializing puremaps integration")
}
// Start control socket
err = startSocket(dev)
err = startSocket(ctx, dev)
if err != nil {
log.Error().Err(err).Msg("Error starting socket")
}
// Block forever
select {}
}

210
maps.go Normal file
View File

@@ -0,0 +1,210 @@
package main
import (
"context"
"strings"
"github.com/godbus/dbus/v5"
"github.com/rs/zerolog/log"
"go.arsenm.dev/infinitime"
)
const (
interfaceName = "io.github.rinigus.PureMaps.navigator"
iconProperty = interfaceName + ".icon"
narrativeProperty = interfaceName + ".narrative"
manDistProperty = interfaceName + ".manDist"
progressProperty = interfaceName + ".progress"
)
func initPureMaps(ctx context.Context, dev *infinitime.Device) error {
// Connect to session bus. This connection is for method calls.
conn, err := newSessionBusConn(ctx)
if err != nil {
return err
}
exists, err := pureMapsExists(ctx, conn)
if err != nil {
return err
}
// Connect to session bus. This connection is for method calls.
monitorConn, err := newSessionBusConn(ctx)
if err != nil {
return err
}
// Define rules to listen for
var rules = []string{
"type='signal',interface='io.github.rinigus.PureMaps.navigator'",
}
var flag uint = 0
// Becode monitor for notifications
call := monitorConn.BusObject().CallWithContext(
ctx, "org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag,
)
if call.Err != nil {
return call.Err
}
var navigator dbus.BusObject
if exists {
navigator = conn.Object("io.github.rinigus.PureMaps", "/io/github/rinigus/PureMaps/navigator")
err = setAll(navigator, dev)
if err != nil {
log.Error().Err(err).Msg("Error setting all navigation fields")
}
}
go func() {
signalCh := make(chan *dbus.Message, 10)
monitorConn.Eavesdrop(signalCh)
for {
select {
case sig := <-signalCh:
if sig.Type != dbus.TypeSignal {
continue
}
var member string
err = sig.Headers[dbus.FieldMember].Store(&member)
if err != nil {
log.Error().Err(err).Msg("Error getting dbus member field")
continue
}
if !strings.HasSuffix(member, "Changed") {
continue
}
log.Debug().Str("member", member).Msg("Signal received from PureMaps navigator")
// The object must be retrieved in this loop in case PureMaps was not
// open at the time ITD was started.
navigator = conn.Object("io.github.rinigus.PureMaps", "/io/github/rinigus/PureMaps/navigator")
member = strings.TrimSuffix(member, "Changed")
switch member {
case "icon":
var icon string
err = navigator.StoreProperty(iconProperty, &icon)
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error getting property")
continue
}
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error setting flag")
continue
}
case "narrative":
var narrative string
err = navigator.StoreProperty(narrativeProperty, &narrative)
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error getting property")
continue
}
err = dev.Navigation.SetNarrative(narrative)
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error setting flag")
continue
}
case "manDist":
var manDist string
err = navigator.StoreProperty(manDistProperty, &manDist)
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error getting property")
continue
}
err = dev.Navigation.SetManDist(manDist)
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error setting flag")
continue
}
case "progress":
var progress int32
err = navigator.StoreProperty(progressProperty, &progress)
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error getting property")
continue
}
err = dev.Navigation.SetProgress(uint8(progress))
if err != nil {
log.Error().Err(err).Str("property", member).Msg("Error setting flag")
continue
}
}
case <-ctx.Done():
return
}
}
}()
if exists {
log.Info().Msg("Sending PureMaps data to InfiniTime")
}
return nil
}
func setAll(navigator dbus.BusObject, dev *infinitime.Device) error {
var icon string
err := navigator.StoreProperty(iconProperty, &icon)
if err != nil {
return err
}
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
if err != nil {
return err
}
var narrative string
err = navigator.StoreProperty(narrativeProperty, &narrative)
if err != nil {
return err
}
err = dev.Navigation.SetNarrative(narrative)
if err != nil {
return err
}
var manDist string
err = navigator.StoreProperty(manDistProperty, &manDist)
if err != nil {
return err
}
err = dev.Navigation.SetManDist(manDist)
if err != nil {
return err
}
var progress int32
err = navigator.StoreProperty(progressProperty, &progress)
if err != nil {
return err
}
return dev.Navigation.SetProgress(uint8(progress))
}
// pureMapsExists checks to make sure the PureMaps service exists on the bus
func pureMapsExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
var names []string
err := conn.BusObject().CallWithContext(
ctx, "org.freedesktop.DBus.ListNames", 0,
).Store(&names)
if err != nil {
return false, err
}
return strSlcContains(names, "io.github.rinigus.PureMaps"), nil
}

131
metrics.go Normal file
View File

@@ -0,0 +1,131 @@
package main
import (
"context"
"database/sql"
"path/filepath"
"time"
"github.com/rs/zerolog/log"
"go.arsenm.dev/infinitime"
_ "modernc.org/sqlite"
)
func initMetrics(ctx context.Context, dev *infinitime.Device) error {
// If metrics disabled, return nil
if !k.Bool("metrics.enabled") {
return nil
}
// Open metrics database
db, err := sql.Open("sqlite", filepath.Join(cfgDir, "metrics.db"))
if err != nil {
return err
}
// Create heartRate table
_, err = db.Exec("CREATE TABLE IF NOT EXISTS heartRate(time INT, bpm INT);")
if err != nil {
return err
}
// Create stepCount table
_, err = db.Exec("CREATE TABLE IF NOT EXISTS stepCount(time INT, steps INT);")
if err != nil {
return err
}
// Create battLevel table
_, err = db.Exec("CREATE TABLE IF NOT EXISTS battLevel(time INT, percent INT);")
if err != nil {
return err
}
// Create motion table
_, err = db.Exec("CREATE TABLE IF NOT EXISTS motion(time INT, X INT, Y INT, Z INT);")
if err != nil {
return err
}
// If heart rate metrics enabled in config
if k.Bool("metrics.heartRate.enabled") {
// Watch heart rate
heartRateCh, err := dev.WatchHeartRate(ctx)
if err != nil {
return err
}
go func() {
// For every heart rate sample
for heartRate := range heartRateCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO heartRate VALUES (?, ?);", unixTime, heartRate)
}
}()
}
// If step count metrics enabled in config
if k.Bool("metrics.stepCount.enabled") {
// Watch step count
stepCountCh, err := dev.WatchStepCount(ctx)
if err != nil {
return err
}
go func() {
// For every step count sample
for stepCount := range stepCountCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO stepCount VALUES (?, ?);", unixTime, stepCount)
}
}()
}
// If battery level metrics enabled in config
if k.Bool("metrics.battLevel.enabled") {
// Watch battery level
battLevelCh, err := dev.WatchBatteryLevel(ctx)
if err != nil {
return err
}
go func() {
// For every battery level sample
for battLevel := range battLevelCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample and time into database
db.Exec("INSERT INTO battLevel VALUES (?, ?);", unixTime, battLevel)
}
}()
}
// If motion metrics enabled in config
if k.Bool("metrics.motion.enabled") {
// Watch motion values
motionCh, err := dev.WatchMotion(ctx)
if err != nil {
return err
}
go func() {
// For every motion sample
for motionVals := range motionCh {
// Get current time
unixTime := time.Now().UnixNano()
// Insert sample values and time into database
db.Exec(
"INSERT INTO motion VALUES (?, ?, ?, ?);",
unixTime,
motionVals.X,
motionVals.Y,
motionVals.Z,
)
}
}()
}
log.Info().Msg("Initialized metrics collection")
return nil
}

View File

@@ -19,6 +19,7 @@
package main
import (
"context"
"fmt"
"github.com/godbus/dbus/v5"
@@ -27,9 +28,9 @@ import (
"go.arsenm.dev/itd/translit"
)
func initNotifRelay(dev *infinitime.Device) error {
func initNotifRelay(ctx context.Context, dev *infinitime.Device) error {
// Connect to dbus session bus
bus, err := newSessionBusConn()
bus, err := newSessionBusConn(ctx)
if err != nil {
return err
}
@@ -40,7 +41,9 @@ func initNotifRelay(dev *infinitime.Device) error {
}
var flag uint = 0
// Becode monitor for notifications
call := bus.BusObject().Call("org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag)
call := bus.BusObject().CallWithContext(
ctx, "org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag,
)
if call.Err != nil {
return call.Err
}

3
scripts/gen-version.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
git describe --tags > version.txt

518
socket.go
View File

@@ -19,64 +19,29 @@
package main
import (
"bytes"
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"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/blefs"
"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 (
ErrDFUInvalidFile = errors.New("provided file is invalid for given upgrade type")
ErrDFUNotEnoughFiles = errors.New("not enough files provided for given upgrade type")
ErrDFUInvalidUpgType = errors.New("invalid upgrade type")
ErrRPCXNoReturnURL = errors.New("bidirectional requests over gateway require a returnURL field in the metadata")
)
type DoneMap map[string]chan struct{}
func (dm DoneMap) Exists(key string) bool {
_, ok := dm[key]
return ok
}
func (dm DoneMap) Done(key string) {
ch := dm[key]
ch <- struct{}{}
}
func (dm DoneMap) Create(key string) {
dm[key] = make(chan struct{}, 1)
}
func (dm DoneMap) Remove(key string) {
close(dm[key])
delete(dm, key)
}
var done = DoneMap{}
func startSocket(dev *infinitime.Device) error {
func startSocket(ctx context.Context, dev *infinitime.Device) error {
// Make socket directory if non-existant
err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0755)
if err != nil {
@@ -100,13 +65,12 @@ func startSocket(dev *infinitime.Device) error {
log.Warn().Err(err).Msg("Error getting BLE filesystem")
}
srv := server.NewServer()
srv := server.New()
itdAPI := &ITD{
dev: dev,
srv: srv,
}
err = srv.Register(itdAPI, "")
err = srv.Register(itdAPI)
if err != nil {
return err
}
@@ -114,14 +78,13 @@ func startSocket(dev *infinitime.Device) error {
fsAPI := &FS{
dev: dev,
fs: fs,
srv: srv,
}
err = srv.Register(fsAPI, "")
err = srv.Register(fsAPI)
if err != nil {
return err
}
go srv.ServeListener("tcp", ln)
go srv.Serve(ctx, ln, codec.Default)
// Log socket start
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 {
dev *infinitime.Device
srv *server.Server
}
func (i *ITD) HeartRate(_ context.Context, _ none, out *uint8) error {
heartRate, err := i.dev.HeartRate()
*out = heartRate
return err
func (i *ITD) HeartRate(_ *server.Context) (uint8, error) {
return i.dev.HeartRate()
}
func (i *ITD) WatchHeartRate(ctx context.Context, _ none, out *string) error {
// Get client message sender
msgSender, ok := getMsgSender(ctx, i.srv)
// If user is using gateway, the client connection will not be available
if !ok {
return ErrRPCXNoReturnURL
}
heartRateCh, cancel, err := i.dev.WatchHeartRate()
func (i *ITD) WatchHeartRate(ctx *server.Context) error {
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
heartRateCh, err := i.dev.WatchHeartRate(ctx)
if err != nil {
return err
}
id := uuid.New().String()
go func() {
done.Create(id)
// For every heart rate value
for heartRate := range heartRateCh {
select {
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)
}
ch <- heartRate
}
}()
*out = id
return nil
}
func (i *ITD) BatteryLevel(_ context.Context, _ none, out *uint8) error {
battLevel, err := i.dev.BatteryLevel()
*out = battLevel
return err
func (i *ITD) BatteryLevel(_ *server.Context) (uint8, error) {
return i.dev.BatteryLevel()
}
func (i *ITD) WatchBatteryLevel(ctx context.Context, _ none, out *string) error {
// Get client message sender
msgSender, ok := getMsgSender(ctx, i.srv)
// If user is using gateway, the client connection will not be available
if !ok {
return ErrRPCXNoReturnURL
}
battLevelCh, cancel, err := i.dev.WatchBatteryLevel()
func (i *ITD) WatchBatteryLevel(ctx *server.Context) error {
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
battLevelCh, err := i.dev.WatchBatteryLevel(ctx)
if err != nil {
return err
}
id := uuid.New().String()
go func() {
done.Create(id)
// For every heart rate value
for battLevel := range battLevelCh {
select {
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)
}
ch <- battLevel
}
}()
*out = id
return nil
}
func (i *ITD) Motion(_ context.Context, _ none, out *infinitime.MotionValues) error {
motionVals, err := i.dev.Motion()
*out = motionVals
return err
func (i *ITD) Motion(_ *server.Context) (infinitime.MotionValues, error) {
return i.dev.Motion()
}
func (i *ITD) WatchMotion(ctx context.Context, _ none, out *string) error {
// Get client message sender
msgSender, ok := getMsgSender(ctx, i.srv)
// If user is using gateway, the client connection will not be available
if !ok {
return ErrRPCXNoReturnURL
}
motionValsCh, cancel, err := i.dev.WatchMotion()
func (i *ITD) WatchMotion(ctx *server.Context) error {
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
motionValsCh, err := i.dev.WatchMotion(ctx)
if err != nil {
return err
}
id := uuid.New().String()
go func() {
done.Create(id)
// For every heart rate value
for motionVals := range motionValsCh {
select {
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)
}
ch <- motionVals
}
}()
*out = id
return nil
}
func (i *ITD) StepCount(_ context.Context, _ none, out *uint32) error {
stepCount, err := i.dev.StepCount()
*out = stepCount
return err
func (i *ITD) StepCount(_ *server.Context) (uint32, error) {
return i.dev.StepCount()
}
func (i *ITD) WatchStepCount(ctx context.Context, _ none, out *string) error {
// Get client message sender
msgSender, ok := getMsgSender(ctx, i.srv)
// If user is using gateway, the client connection will not be available
if !ok {
return ErrRPCXNoReturnURL
}
stepCountCh, cancel, err := i.dev.WatchStepCount()
func (i *ITD) WatchStepCount(ctx *server.Context) error {
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
stepCountCh, err := i.dev.WatchStepCount(ctx)
if err != nil {
return err
}
id := uuid.New().String()
go func() {
done.Create(id)
// For every heart rate value
for stepCount := range stepCountCh {
select {
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)
}
ch <- stepCount
}
}()
*out = id
return nil
}
func (i *ITD) Version(_ context.Context, _ none, out *string) error {
version, err := i.dev.Version()
*out = version
return err
func (i *ITD) Version(_ *server.Context) (string, error) {
return i.dev.Version()
}
func (i *ITD) Address(_ context.Context, _ none, out *string) error {
addr := i.dev.Address()
*out = addr
return nil
func (i *ITD) Address(_ *server.Context) string {
return i.dev.Address()
}
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)
}
func (i *ITD) SetTime(_ context.Context, t time.Time, _ *none) error {
return i.dev.SetTime(t)
func (i *ITD) SetTime(_ *server.Context, t *time.Time) error {
return i.dev.SetTime(*t)
}
func (i *ITD) WeatherUpdate(_ context.Context, _ none, _ *none) error {
func (i *ITD) WeatherUpdate(_ *server.Context) {
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()
switch reqData.Type {
@@ -387,30 +256,22 @@ func (i *ITD) FirmwareUpgrade(ctx context.Context, reqData api.FwUpgradeData, ou
return ErrDFUInvalidUpgType
}
id := uuid.New().String()
*out = id
// 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() {
// For every progress event
for event := range i.dev.DFU.Progress() {
data, err := msgpack.Marshal(event)
if err != nil {
log.Error().Err(err).Msg("Error encoding DFU progress event")
continue
}
msgSender.SendMessage(id, "DFUProgress", nil, data)
}
firmwareUpdating = false
msgSender.SendMessage(id, "Done", nil, nil)
}()
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
go func() {
// For every progress event
for event := range i.dev.DFU.Progress() {
ch <- event
}
firmwareUpdating = false
// Send zero object to signal completion
close(ch)
}()
// Set firmwareUpdating
firmwareUpdating = true
@@ -427,18 +288,23 @@ func (i *ITD) FirmwareUpgrade(ctx context.Context, reqData api.FwUpgradeData, ou
return nil
}
func (i *ITD) Done(_ context.Context, id string, _ *none) error {
done.Done(id)
return nil
}
type FS struct {
dev *infinitime.Device
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()
for _, path := range paths {
err := fs.fs.Remove(path)
@@ -449,12 +315,23 @@ func (fs *FS) Remove(_ context.Context, paths []string, _ *none) error {
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()
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()
for _, path := range paths {
err := fs.fs.Mkdir(path)
@@ -465,18 +342,18 @@ func (fs *FS) Mkdir(_ context.Context, paths []string, _ *none) error {
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()
entries, err := fs.fs.ReadDir(dir)
if err != nil {
return err
return nil, err
}
var fileInfo []api.FileInfo
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
return err
return nil, err
}
fileInfo = append(fileInfo, api.FileInfo{
Name: info.Name(),
@@ -485,11 +362,10 @@ func (fs *FS) ReadDir(_ context.Context, dir string, out *[]api.FileInfo) error
})
}
*out = fileInfo
return nil
return fileInfo, 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()
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
}
id := uuid.New().String()
*out = id
// 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() {
// For every progress event
for sent := range remoteFile.Progress() {
data, err := msgpack.Marshal(api.FSTransferProgress{
Total: remoteFile.Size(),
Sent: sent,
})
if err != nil {
log.Error().Err(err).Msg("Error encoding filesystem transfer progress event")
continue
}
msgSender.SendMessage(id, "FSProgress", nil, data)
}
msgSender.SendMessage(id, "Done", nil, nil)
}()
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
go func() {
// For every progress event
for sent := range remoteFile.Progress() {
ch <- api.FSTransferProgress{
Total: remoteFile.Size(),
Sent: sent,
}
}
// Send zero object to signal completion
close(ch)
}()
go func() {
io.Copy(remoteFile, localFile)
@@ -542,7 +409,7 @@ func (fs *FS) Upload(ctx context.Context, paths [2]string, out *string) error {
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()
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
}
id := uuid.New().String()
*out = id
// 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() {
// For every progress event
for rcvd := range remoteFile.Progress() {
data, err := msgpack.Marshal(api.FSTransferProgress{
Total: remoteFile.Size(),
Sent: rcvd,
})
if err != nil {
log.Error().Err(err).Msg("Error encoding filesystem transfer progress event")
continue
}
msgSender.SendMessage(id, "FSProgress", nil, data)
}
msgSender.SendMessage(id, "Done", nil, nil)
localFile.Close()
remoteFile.Close()
}()
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
go func() {
// For every progress event
for sent := range remoteFile.Progress() {
ch <- api.FSTransferProgress{
Total: remoteFile.Size(),
Sent: sent,
}
}
// Send zero object to signal completion
close(ch)
localFile.Close()
remoteFile.Close()
}()
go io.Copy(localFile, remoteFile)
return nil
}
func (fs *FS) LoadResources(ctx *server.Context, path string) error {
resFl, err := os.Open(path)
if err != nil {
return err
}
progCh, err := infinitime.LoadResources(resFl, fs.fs)
if err != nil {
return err
}
ch, err := ctx.MakeChannel()
if err != nil {
return err
}
go func() {
for evt := range progCh {
ch <- evt
}
close(ch)
}()
return nil
}
func (fs *FS) updateFS() {
if fs.fs == nil || updateFS {
// Get new FS
@@ -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()
}

8
version.go Normal file
View File

@@ -0,0 +1,8 @@
package main
import _ "embed"
//go:generate scripts/gen-version.sh
//go:embed version.txt
var version string

View File

@@ -1 +0,0 @@
unknown

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"math"
@@ -60,13 +61,13 @@ type OSMData []struct {
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") {
return nil
}
// 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 {
return err
}
@@ -76,7 +77,7 @@ func initWeather(dev *infinitime.Device) error {
go func() {
for {
// Attempt to get weather
data, err := getWeather(lat, lon)
data, err := getWeather(ctx, lat, lon)
if err != nil {
log.Warn().Err(err).Msg("Error getting weather data")
// Wait 15 minutes before retrying
@@ -181,10 +182,14 @@ func initWeather(dev *infinitime.Device) error {
// getLocation returns the latitude and longitude
// 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
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 {
return
}
@@ -218,9 +223,10 @@ func getLocation(loc string) (lat, lon float64, err error) {
}
// 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
req, err := http.NewRequest(
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf(
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f",