261 Commits

Author SHA1 Message Date
vchigrin d7606c4327 Fix crash in filesystem API.
Signed-off-by: Vyacheslav Chigrin <vyacheslav.chigrin@izba.dev>
2024-07-04 00:23:20 +03:00
Elara6331 bec703c300 Update README status badge 2023-04-26 22:38:56 -07:00
Elara6331 25220cf334 Update goreleaser config for new name/domain 2023-04-26 22:11:24 -07:00
Elara6331 858edb0f55 Only send call notification for incoming calls 2023-04-25 16:13:38 -07:00
Elara6331 9998915959 Update domain 2023-04-20 19:54:58 -07:00
Elara6331 e0023907a0 Update go.mod domain 2023-04-20 19:15:10 -07:00
Elara6331 f974abed3b Update drpc library version 2023-03-28 13:27:14 -07:00
Elara6331 ebca255fa8 Remove replace directive for drpc 2023-03-28 13:25:26 -07:00
Elara6331 f7ac77273e Gracefully shut down each component before exiting 2023-03-26 14:34:29 -07:00
Elara6331 5ce83902a4 Use EINVAL for Invalid Offset error in FUSE 2023-03-26 13:01:25 -07:00
Elara6331 ee5cb174fb Use type switch for syscallErr 2023-03-25 16:03:56 -07:00
Elara6331 6667ba576c Set some log levels to Debug 2023-03-25 15:53:46 -07:00
Elara6331 92acdee152 Removed unreachable code 2023-03-25 15:52:46 -07:00
Elara6331 a90069999d Create new type for node kinds 2023-03-25 15:52:13 -07:00
Elara6331 6d2469311e Mention FUSE support in README 2023-03-25 15:27:28 -07:00
Elara6331 510f183db2 Run formatter 2023-03-25 15:24:46 -07:00
yannickulrich 77680704e1 Added FUSE support (#55)
This exposes the watches' file system over FUSE. This way, we can access files on the watch without having to go through `itctl` or developing 3rd party tools.

**Features**

- [x] read/write access to the file system
- [x] read access to momentary sensor data
- [x] live access to sensor data (i.e. WatchMotion rather than Motion)
- [x] configuration of mount point

Co-authored-by: Yannick Ulrich <yannick.ulrich@durham.ac.uk>
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/55
Co-authored-by: yannickulrich <yannick.ulrich@protonmail.com>
Co-committed-by: yannickulrich <yannick.ulrich@protonmail.com>
2023-03-25 22:23:51 +00:00
Elara6331 35701b1396 Switch from zerolog to go.arsenm.dev/logger in itctl 2023-01-04 15:17:14 -08:00
Elara6331 e858c43b5e Switch from zerolog to go.arsenm.dev/logger 2023-01-04 15:06:05 -08:00
Elara6331 6337fde64e Upgrade go.arsenm.dev/drpc 2023-01-04 14:25:58 -08:00
Elara6331 4dec1d70b8 Add error handling for RPC registration functions 2023-01-03 16:47:25 -08:00
Elara6331 19a9f64525 Move multiplexing code into separate module 2023-01-03 13:06:38 -08:00
Elara6331 053f8f50d7 Update itgui screenshots 2023-01-03 09:39:21 -08:00
Elara6331 33f772d80c Add doc comments 2023-01-03 09:30:04 -08:00
Elara6331 a787e58128 Use multiplexed connection in NewFromConn() 2023-01-03 09:24:36 -08:00
Elara6331 b8746cc924 Run formatter 2023-01-03 09:18:57 -08:00
Elara6331 3d02ff5b64 Use correct paths in README 2023-01-03 09:16:59 -08:00
Elara6331 86a77f6f1f Properly close multiplexed streams 2023-01-03 09:15:38 -08:00
Elara6331 94ec82c4a6 Start separate goroutine for multiplexed stream handling 2023-01-03 01:02:48 -08:00
Elara6331 b656c69350 Add connection multiplexing, fixing itgui 2023-01-03 00:54:00 -08:00
Elara6331 01919e67a3 Fix dependencies 2023-01-02 23:06:04 -08:00
Elara6331 c9444e276a Restructure and revise README 2023-01-02 22:42:12 -08:00
Elara6331 19c87ddde1 Remove itgui CI config 2023-01-02 22:32:04 -08:00
Elara6331 d41872ab64 Switch to autogenerated DRPC framework 2023-01-02 22:30:17 -08:00
Hunman 15b5d2888e Warn when Koanf read fails (#47)
Figured out the problem in issue #32, the toml file syntax was invalid (I had `'` instead of `"` at some values), but there was nothing in the logs about it.

Moved the config reading (and watching) into the same function, which logs the error as a warning.

I wanted to try moving the whole `if` into a separate function, but I kept getting errors when I tried to extract the `path` from the `File`, so I have that attempt in a separate branch not in this pull request: https://gitea.arsenm.dev/Hunman/itd/commit/5a84bf81489d3dc57f197f5feef5521950645ba5

Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/47
Co-authored-by: Hunman <sanyi.exe@gmail.com>
Co-committed-by: Hunman <sanyi.exe@gmail.com>
2023-01-02 09:05:23 +00:00
Elara6331 131a16df6f Add tests 2022-12-08 01:16:00 -08:00
Elara6331 9f0ca5a7df Move mpris out of pkg directory and run gofumpt 2022-11-24 17:36:25 -08:00
Elara6331 b3be8e7027 Fix goreleaser aur config 2022-11-24 16:24:45 -08:00
Elara6331 6a92eba062 Add archlinux package to goreleaser config 2022-11-24 16:23:03 -08:00
Elara6331 33a01a64d6 Add itgui.desktop to goreleaser and fix itgui permissions 2022-11-24 16:20:11 -08:00
Elara6331 919375d214 Add itgui.desktop 2022-11-24 16:18:26 -08:00
Elara6331 ff0ead0343 Prepare for itgui cross-compilation 2022-11-24 16:16:25 -08:00
Elara6331 f3f66176b8 Switch version.txt target to use go generate 2022-11-22 03:23:14 +00:00
Elara6331 2c733edeec Merge pull request 'Move mpris implementation from infinitime library to itd, where it really belongs' (#41) from FloralExMachina/itd:master into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/41
2022-11-22 02:24:51 +00:00
razorkitty 8ee8c39aa7 fixed type in comment about DBus 2022-11-22 02:23:03 +00:00
razorkitty 5699375b2a copy mpris implementation from infinitime library to itd, where it really belongs
moved dbus.go to an internal utils package
added context function parameter to initMusicCtrl and updated main.go to pass it
updated calls.go, maps.go, music.go, and notifs.go to use utils package for getting a dus connection
2022-11-21 19:59:54 +00:00
Elara6331 7ba643888c Remove gitm config as it's no longer needed 2022-11-19 15:38:23 -08:00
Elara6331 23f9344378 Remove pactl dependencies (Arsen6331/infinitime#6) 2022-11-19 15:29:10 -08:00
Elara6331 5284619ac3 Merge pull request 'update itctl usage screen to current output' (#39) from mashuptwice/itd:master into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/39
2022-11-19 04:04:31 +00:00
mashuptwice 954e653092 update itctl usage screen to current output 2022-11-19 03:55:22 +00:00
Elara6331 1c0c89b3d4 Switch badge to self-hosted CI 2022-11-18 07:50:21 +00:00
Elara6331 16adc3d4c7 Add go generate script for calculating version number 2022-11-17 21:27:36 -08:00
Elara6331 6dd1faafac Add woodpecker config 2022-11-18 05:02:37 +00:00
Elara6331 d6fa98fdac Add resource loading to ITD FS tab 2022-11-15 19:20:34 -08:00
Elara6331 d84f34f5b9 Mention navigation support in README 2022-11-07 12:24:55 -08:00
Elara6331 87bca3270a Add navigation support via PureMaps 2022-11-07 12:22:14 -08:00
Elara6331 9298d7c920 Update infinitime library 2022-10-25 12:38:02 -07:00
Elara6331 ecca2b320e Update infinitime library 2022-10-20 01:42:23 -07:00
Elara6331 5301546e7e Update infinitime library 2022-10-17 12:50:51 -07:00
Elara6331 1231dd5308 Add warning if current InfiniTime doesn't support BLE FS (#29) 2022-10-17 12:40:51 -07:00
Elara6331 cdc5d22867 Update infinitime library 2022-10-17 12:24:17 -07:00
Elara6331 be57cdeea4 Handle error events in itctl res load command (#29) 2022-10-17 12:23:06 -07:00
Elara6331 53bc192607 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
Elara6331 5719e77b59 Add resource loading as part of DFU 2022-10-16 13:17:12 -07:00
Elara6331 aefe6a82ff Close channel once resource uploading complete 2022-10-16 12:42:59 -07:00
Elara6331 a04c95b0be Add -r for rm and -p for mkdir 2022-09-03 16:28:25 -07:00
Elara6331 5dfcd33563 Remove example comments from goreleaser config 2022-09-01 15:09:49 -07:00
Elara6331 a1c6e08987 Fix file extension of Alpine example 2022-09-01 15:02:11 -07:00
Elara6331 e82a9e3f18 Add --allow-untrusted to Alpine example 2022-09-01 15:00:03 -07:00
Elara6331 61d14857d8 Add installation instructions for major distros 2022-09-01 14:58:33 -07:00
Elara6331 4c34634220 Remove download binary badge and add new AUR badge 2022-09-01 14:33:23 -07:00
Elara6331 828d4944f4 Allow automatic release to the AUR 2022-09-01 11:50:41 -07:00
Elara6331 2eb25219d9 Update CI badge 2022-09-01 03:14:05 -07:00
Elara6331 2ab9fbcb80 Add GoReleaser config 2022-09-01 01:52:46 -07:00
Elara6331 a178319e28 Add resource loading to itctl 2022-08-30 13:01:36 -07:00
Elara6331 ec6d216346 Add LoadResources() to API 2022-08-30 12:13:22 -07:00
Elara6331 e50c8430be Improve itgui compilation documentation (Fixes #24) 2022-08-29 13:28:52 -07:00
Elara6331 df6f9f1872 Update infinitime library and bluetooth library (Fixes #22) 2022-08-19 14:05:30 -07:00
Elara6331 74ef74f90d Revert #22 bandaid fix 2022-08-19 14:04:08 -07:00
Elara6331 f2d50d5bd9 Temporary bandaid fix for #22 2022-08-14 00:05:56 -07:00
Elara6331 d59e7af2d1 Fix comment above goroutine code 2022-07-31 02:40:46 -07:00
Elara6331 3c31bd2921 Fix bug where itctl doesn't exit on SIGINT/SIGTERM 2022-07-31 02:22:33 -07:00
Elara6331 900be6f2d0 Fix bug where help command doesn't show flags/subcommands 2022-07-31 02:15:42 -07:00
Elara6331 5cd395a41b Remove GOFLAGS from Makefile as the go tool already looks at that variable 2022-07-22 10:36:19 -07:00
Elara6331 bcf0d33531 Propagate context to lrpc 2022-05-12 17:14:34 -07:00
Elara6331 f5092cd2e1 Create and propagate contexts wherever possible 2022-05-11 13:24:12 -07:00
Elara6331 4250541891 Add metrics screenshot 2022-05-11 12:10:50 -07:00
Elara6331 cab8e4b089 Add metrics graphs to itgui 2022-05-10 23:37:58 -07:00
Elara6331 5043bb8cfe Add metrics collection via sqlite 2022-05-10 18:03:37 -07:00
Elara6331 74fbb79363 Update lrpc 2022-05-10 02:12:52 -07:00
Elara6331 5ece2415c5 Update Go compiler requirement 2022-05-09 21:46:03 -07:00
Elara6331 31c5c51b87 Allow API client to be made from connection 2022-05-07 21:23:42 -07:00
Elara6331 63cacded4a Update lrpc library 2022-05-07 15:12:29 -07:00
Elara6331 e7942b263d Add new itgui screenshots 2022-05-05 14:05:58 -07:00
Elara6331 263e8c1bce Rewrite itgui and add new screenshots 2022-05-05 14:00:49 -07:00
Elara6331 0429dc8197 Update infinitime library 2022-05-05 12:41:51 -07:00
Elara6331 095b82e2e9 Update lrpc for data race fixes 2022-05-04 16:16:28 -07:00
Elara6331 8723dddbc1 Update lrpc 2022-05-03 18:55:37 -07:00
Elara6331 2cda6e2bde Update infinitime library 2022-05-02 20:21:47 -07:00
Elara6331 711df70edc Allow changing bluetooth adapter ID 2022-05-02 20:17:38 -07:00
Elara6331 889d16fb0a Update lrpc 2022-05-02 16:28:25 -07:00
Elara6331 f67f653427 Fix lrpc response line number in README 2022-05-01 23:06:27 -07:00
Elara6331 4166903397 Update lrpc 2022-05-01 21:39:58 -07:00
Elara6331 ce6080e920 Remove version.txt on clean 2022-05-01 21:22:15 -07:00
Elara6331 f532a05c66 Only update version if version.txt does not exist 2022-05-01 21:21:22 -07:00
Elara6331 68d8588978 Update README to reflect recent changes 2022-05-01 21:16:47 -07:00
Elara6331 3689fcf889 Remove debug print 2022-05-01 20:56:14 -07:00
Elara6331 f1a7a87ef8 Remove the no-longer useful none type alias 2022-05-01 20:51:13 -07:00
Elara6331 e5e6bab7e3 Remove version.txt 2022-05-01 20:49:42 -07:00
Elara6331 d75d893409 Remove replace directive and fix firmware upgrade error 2022-05-01 20:40:30 -07:00
Elara6331 321afe0121 Fix bug where itctl could not be killed 2022-05-01 20:32:59 -07:00
Elara6331 6ba50fb7de Add context support and update lrpc 2022-05-01 15:22:28 -07:00
Elara6331 78e64fe3ed Remove now unnecessary DoneMap 2022-05-01 14:00:31 -07:00
Elara6331 51b8e581f7 Upgrade lrpc version 2022-05-01 13:59:40 -07:00
Elara6331 fe00e8bb65 Use default codec 2022-05-01 11:41:16 -07:00
Elara6331 a1ee021675 Switch to lrpc and use context to handle signals 2022-05-01 11:36:28 -07:00
Elara6331 c929635029 Use rpcxlite 2022-04-30 03:25:27 -07:00
Elara6331 c17ba102dd Add comments 2022-04-24 00:58:39 -07:00
Elara6331 0ae40d69bc Support bidirectional requests over gateway 2022-04-24 00:54:04 -07:00
Elara6331 44c89408d2 Add debug logs 2022-04-23 20:20:13 -07:00
Elara6331 dc87e144e0 Re-add watch commands to itctl 2022-04-23 18:46:49 -07:00
Elara6331 11af134444 Enable RPCX gateway 2022-04-23 11:29:16 -07:00
Elara6331 ac56dd5f57 Merge branch 'master' of ssh://192.168.100.62:2222/Arsen6331/itd 2022-04-22 19:22:32 -07:00
Elara6331 6dedd187d4 Improve error handling 2022-04-22 18:43:13 -07:00
Elara6331 4caa504db1 Remove old code comment 2022-04-22 17:19:23 -07:00
Elara6331 de19b77c13 Update module go version to 1.17 2022-04-22 17:15:41 -07:00
Elara6331 9990e92f19 Switch from custom socket API to rpcx 2022-04-22 17:12:30 -07:00
Elara6331 4534de7157 Fix typo in code (Czeck -> Czech) 2022-04-16 10:15:55 -07:00
Elara6331 d94d484484 Fix typo (Czeck -> Czech) 2022-04-16 10:14:18 -07:00
Elara6331 7309674dcf Use new changes in infinitime library to stop removing InfiniTime devices (Fixes #10) 2022-04-16 04:28:53 -07:00
Elara6331 c5ea3df255 Fix itctl panic when itd is not running (Fixes #14) 2022-04-02 15:20:31 -07:00
Elara6331 f7d4fc1b58 Merge pull request 'emoji translation: Add my frequently received emojis' (#15) from earboxer/itd:common-emojis into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/15
2022-03-25 17:22:02 -07:00
earboxer 0a3bacc3ee emoji translation: Add my frequently received emojis
mapped to a common ASCII emoticon, or to the shortcode
2022-03-21 11:58:13 -04:00
Elara6331 68aad1c0ed Remove debug code 2022-03-15 19:25:37 -07:00
Elara6331 5b87af872f Update 'cmd/itctl/main.go' 2022-03-15 16:16:44 -07:00
Elara6331 ffed644beb Remove exit error handler because it causes duplicated help text 2022-03-15 16:06:05 -07:00
Elara6331 b61baf5760 Transliterate song metadata (Fixes #13) 2022-03-11 13:14:23 -08:00
Elara6331 8d4e53b35f Merge pull request 'Romanian transliterate' (#12) from eugenr/itd:romanian into master
Reviewed-on: https://gitea.arsenm.dev/Arsen6331/itd/pulls/12
2022-03-11 10:04:26 -08:00
eugenr d56760b494 Add Romanian to README.md 2022-03-11 04:17:12 -08:00
eugenr 6369d46211 Romanian translit 2022-03-11 04:15:10 -08:00
Elara6331 597e7dab5f Make sure fs is only updated if dev.FS() succeeds (#11) 2022-03-08 08:32:31 -08:00
Elara6331 2112894889 Update infinitime library for #9 fix 2022-03-04 12:05:58 -08:00
Elara6331 02bf90f10e Rewrite itctl to use urfave/cli instead of spf13/cobra 2022-02-24 21:26:40 -08:00
Elara6331 65cae3aeab Add update weather command to itctl 2022-02-23 21:22:03 -08:00
Elara6331 dddc8780d6 Add default version.txt file 2022-02-22 08:44:50 -08:00
Elara6331 08dce2a110 Add version flag 2022-02-22 08:43:29 -08:00
Elara6331 b1d2fe6efb Add enable switch for weather to config 2022-02-22 08:33:27 -08:00
Elara6331 d83923a917 Add error logging for weather 2022-02-21 16:27:04 -08:00
Elara6331 032f735526 Implement weather via MET Norway 2022-02-21 16:18:52 -08:00
Elara6331 57768acb94 Switch from viper to koanf 2022-02-21 11:20:02 -08:00
Elara6331 953db860c2 Update version of infinitime library for rewritten connection code 2022-02-21 02:47:48 -08:00
Elara6331 78098b3833 Reorganize and clean code 2021-12-17 00:31:05 -08:00
Elara6331 3d89a03ca7 Update Infinitime library to use custom agent 2021-12-16 21:32:06 -08:00
Elara6331 dcec9f593b Propagate FS errors on read/write and close files when finished writing 2021-12-13 09:58:34 -08:00
Elara6331 233b0f77f0 Make paths absolute for firmware upgrades 2021-12-12 17:46:50 -08:00
Elara6331 8c020f792d Implement file transfer progress 2021-12-12 17:08:48 -08:00
Elara6331 56ed3dda88 Remove debug code 2021-12-11 22:23:01 -08:00
Elara6331 38eda50537 Create absolute directories for ITD to read/write 2021-12-11 22:13:21 -08:00
Elara6331 9bfdaef70c Directly read/write files from ITD 2021-12-11 22:11:01 -08:00
Elara6331 488b5a7d89 Fix comments in filesystem commands 2021-11-27 00:11:37 -08:00
Elara6331 9108dbd40b Fix and add error messages to fs operations 2021-11-27 00:03:13 -08:00
Elara6331 d8f4fd8aa5 Ensure that the FS works after a reconnect 2021-11-25 20:35:03 -08:00
Elara6331 371ffcc21f Remove replace directive 2021-11-25 19:49:07 -08:00
Elara6331 50db403614 Add missing responses to some FS operations 2021-11-25 19:46:04 -08:00
Elara6331 e88e8e487e Add newline for read file command if output is stdout 2021-11-25 19:44:43 -08:00
Elara6331 eda92e2d62 Get BLE FS once rather than on every connection 2021-11-25 19:41:44 -08:00
Elara6331 3eb93cdb35 Allow multiple call notification responses 2021-11-25 12:41:36 -08:00
Elara6331 d86216ef74 Remove playerctl from depencency list 2021-11-24 16:46:57 -08:00
Elara6331 9f055010c0 Update music control implementation 2021-11-24 16:44:36 -08:00
Elara6331 3e69b75a27 Remove useless function call 2021-11-24 13:07:48 -08:00
Elara6331 be99136bb5 Make sure modemmanager exists for call notifications 2021-11-24 13:04:20 -08:00
Elara6331 3a16bff83f Add comments 2021-11-24 12:00:44 -08:00
Elara6331 9bd78bccce Use clearer variable names 2021-11-24 11:54:16 -08:00
Elara6331 62817e4fbe Use new helper functions 2021-11-24 11:52:52 -08:00
Elara6331 715a53a91c Switch calls to use dbus library and add helpers for private connections 2021-11-24 11:36:36 -08:00
Elara6331 22709439c4 Switch to private bus connection 2021-11-23 22:03:41 -08:00
Elara6331 082fdc02f4 Add filesystem to itctl 2021-11-23 14:14:45 -08:00
Elara6331 49a7a51527 Allow multiple paths in mkdir and remove 2021-11-23 13:35:18 -08:00
Elara6331 0720feba96 Fix write file in api package 2021-11-23 11:19:21 -08:00
Elara6331 fcb5bcd967 Add BLE FS to API package 2021-11-23 11:12:16 -08:00
Elara6331 fe1ebc40a0 Implement BLE FS 2021-11-22 22:04:09 -08:00
Elara6331 8d6746e365 Update infinitime library to fix compatibility with BlueZ 5.62 2021-11-22 01:18:40 -08:00
Elara6331 e89e97749e Add reminder to validate firmware to itctl and itgui 2021-11-06 19:06:17 -07:00
Elara6331 ed3b7b2a99 Update default values to reflect new config fields 2021-11-01 11:28:55 -07:00
Elara6331 5cb6cfa5fb Upgrade infinitime library version 2021-11-01 11:21:37 -07:00
Elara6331 1c8fa559e4 Remove config version field 2021-10-27 08:34:10 -07:00
Elara6331 a3e9f1f4c4 Add whitelist support 2021-10-27 07:27:12 -07:00
Elara6331 2b75e64daf Add motion service to itgui 2021-10-25 09:45:19 -07:00
Elara6331 2a07dd3d1c Remove debug print and add error handling (itgui) 2021-10-25 00:11:41 -07:00
Elara6331 c881502b44 Use api package in itgui 2021-10-24 13:27:14 -07:00
Elara6331 c57c1f89b9 Add watch commands to itctl 2021-10-24 11:02:29 -07:00
Elara6331 5c8fd72f85 Handle unknown request type 2021-10-24 01:11:57 -07:00
Elara6331 cf7dbd0b9c Use request type for error response type 2021-10-24 01:09:27 -07:00
Elara6331 2426582bd3 Fix API package 2021-10-24 00:49:48 -07:00
Elara6331 8d29ece214 Return request type for response type 2021-10-24 00:45:50 -07:00
Elara6331 4b6b2a4581 Disable firmware updating error once progress channel closed 2021-10-23 22:03:33 -07:00
Elara6331 37c4fe5577 Use sent bytes to check if transfer complete 2021-10-23 19:36:23 -07:00
Elara6331 45621a98d5 Remove test 2021-10-23 18:42:22 -07:00
Elara6331 32cab6d00f Update itctl to use api 2021-10-23 18:41:03 -07:00
Elara6331 e45bfe3de8 Generalize socket cancellation and update API accordingly 2021-10-23 18:03:17 -07:00
Elara6331 2ab8d24a43 Reorganize itctl structure 2021-10-23 15:11:04 -07:00
Elara6331 d16c5ea96d Add cancellation to api package 2021-10-22 22:30:58 -07:00
Elara6331 46a891d852 Add responses to cancellation requests 2021-10-22 22:15:35 -07:00
Elara6331 ff8ce1b2a5 Add cancellation to watchable values 2021-10-22 22:14:01 -07:00
Elara6331 68bac8859f Add doc comments to api package 2021-10-22 21:01:18 -07:00
Elara6331 a235903583 Send response types in socket responses and create api package 2021-10-22 20:47:57 -07:00
Elara6331 b63960ed88 Update readme 2021-10-22 17:12:46 -07:00
Elara6331 869f487456 Add MotionValues type 2021-10-22 13:42:33 -07:00
Elara6331 86db246d4a Add motion service to itctl 2021-10-22 13:40:16 -07:00
Elara6331 4b95af905d Implement motion service 2021-10-22 13:21:14 -07:00
Elara6331 0054a0b3cf Update infinitime library version to fix intermittent DFU issues 2021-10-21 20:27:58 -07:00
Elara6331 0024a9fe69 Remove replace directive 2021-10-15 00:27:10 -07:00
Elara6331 ba443d3836 Mention call notifications in readme 2021-10-15 00:26:14 -07:00
Elara6331 a5f552fe8f Add call notifications for ModemManager 2021-10-15 00:25:34 -07:00
Elara6331 ce31929a73 Show update file names when selected in itgui 2021-10-07 13:38:13 -07:00
Elara6331 b5803873ce Remove replace directive 2021-10-06 17:47:07 -07:00
Elara6331 85699e4999 Update infinitime library 2021-10-06 17:15:42 -07:00
Elara6331 47edf7773f Add and fix comments, fix transliteration maps so only first character is capitalized 2021-10-06 13:26:16 -07:00
Elara6331 e70c9f4d7c Fix chinese transliteration when chinese characters are not followed by non-chinese characters 2021-10-06 13:15:49 -07:00
Elara6331 a4598269e5 Only do init once for Armenian transliteration 2021-10-06 13:08:25 -07:00
Elara6331 fe4b0ec203 Add init functions to transliterators 2021-10-06 09:41:33 -07:00
Elara6331 3274eb6acf Fix capital letters for Armenian transliteration 2021-10-05 09:09:19 -07:00
Elara6331 8019a3521e Add Chinese transliteration via Pinyin conversion library 2021-10-04 22:26:16 -07:00
Elara6331 b365fdfcd3 Update variable names and comments for interface-based transliteration 2021-10-04 20:23:54 -07:00
Elara6331 a3281a7e15 Fix Korean transliteration 2021-10-04 20:06:08 -07:00
Elara6331 12c5e924a2 Add korean transliteration 2021-10-04 19:07:54 -07:00
Elara6331 91f2f28076 Use interface to allow for more complex transliteration implementations 2021-10-04 17:45:26 -07:00
Elara6331 471de06158 Fix German transliteration for Ü and add attribution 2021-10-04 13:17:48 -07:00
Elara6331 ca02e8c62f Add transliteration 2021-10-04 01:05:01 -07:00
Elara6331 9b2507de4c Break transfer loops after refreshing progress bar 2021-08-27 09:01:46 -07:00
Elara6331 5bc63b7864 Add fatal error dialog 2021-08-27 08:47:24 -07:00
Elara6331 316e113e5d Mention GUI in README 2021-08-26 09:01:03 -07:00
Elara6331 50f3f244a3 Add comments to gui 2021-08-26 08:47:17 -07:00
Elara6331 ea63f43638 Add GUI frontend 2021-08-25 21:18:24 -07:00
Elara6331 9574f3dd36 Fix indentation in config 2021-08-24 20:35:25 -07:00
Elara6331 36b683204d Switch to iota for request types and move to types package 2021-08-24 20:32:17 -07:00
Elara6331 0fe83fccc1 Fix debug config paths 2021-08-24 08:55:22 -07:00
Elara6331 ef6c37c20b Add config defaults and run go fmt 2021-08-24 08:54:08 -07:00
Elara6331 59ecd11340 Create new config format 2021-08-24 08:33:41 -07:00
Elara6331 7b5c228591 Remove replace directive 2021-08-22 15:07:45 -07:00
Elara6331 eed4a41230 Fix find and replace error 2021-08-22 13:53:32 -07:00
Elara6331 c1134926aa Update infinitime library to fix connection bug 2021-08-22 13:13:37 -07:00
Elara6331 c0bbfff872 Use new pair timeout option 2021-08-21 20:35:21 -07:00
Elara6331 4308789af5 Mention interactive mode in readme 2021-08-21 19:07:59 -07:00
Elara6331 76c54339cf Disable completion command 2021-08-21 19:03:18 -07:00
Elara6331 688f5e5004 Fix binary download link 2021-08-21 18:52:11 -07:00
Elara6331 f4b2a21bd8 Add badges 2021-08-21 18:48:43 -07:00
Elara6331 a769bac3c2 Add uninstall rule to makefile 2021-08-21 17:17:25 -07:00
Elara6331 3dfa572eb3 Add CI status to readme 2021-08-21 16:36:10 -07:00
Elara6331 ff4a62bc0b Add GOFLAGS environment variable to makefile 2021-08-21 16:03:54 -07:00
Elara6331 a2ab8ad4f3 Add gitm for mirroring 2021-08-21 15:59:19 -07:00
Elara6331 b48b774e97 Specify minimum go version in readme 2021-08-21 15:35:36 -07:00
Elara6331 ce505280ec Add starting to readme 2021-08-21 15:14:37 -07:00
Elara6331 70dde9554d Change recoverable errors to warn log level to stop shell from exiting 2021-08-21 14:15:55 -07:00
Elara6331 bf1cda2e7e Add interactive mode to itctl 2021-08-21 12:30:16 -07:00
Elara6331 43ea68e62f Prioritize config in home directory over /etc 2021-08-21 10:26:12 -07:00
Elara6331 b68e81cf8d Mention getting info from watch in readme 2021-08-21 09:40:29 -07:00
Elara6331 763f11e191 Mention automatic config updates in readme 2021-08-21 09:37:20 -07:00
Elara6331 031295ad9a Watch config for changes and apply automatically 2021-08-21 03:07:48 -07:00
Elara6331 cf1794215e Remove replace directive and update infinitime library 2021-08-21 01:37:16 -07:00
Elara6331 203224ed4a Initial Commit 2021-08-21 01:19:49 -07:00
85 changed files with 8126 additions and 2178 deletions
+2
View File
@@ -1,4 +1,6 @@
/itctl /itctl
/itd /itd
/itgui /itgui
/itgui-linux-*
/version.txt /version.txt
dist/
-3
View File
@@ -1,3 +0,0 @@
[repos]
origin = "ssh://git@192.168.100.62:2222/Arsen6331/itd.git"
gitlab = "git@gitlab.com:moussaelianarsen/itd.git"
+119
View File
@@ -0,0 +1,119 @@
before:
hooks:
- go generate
- go mod tidy
builds:
- id: itd
env:
- CGO_ENABLED=0
binary: itd
goos:
- linux
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 7
- id: itctl
env:
- CGO_ENABLED=0
main: ./cmd/itctl
binary: itctl
goos:
- linux
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 7
archives:
- name_template: >-
{{- .ProjectName }}-{{.Version}}-{{.Os}}-
{{- if eq .Arch "386" }}i386
{{- else if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{.Arch}}
{{- end }}
files:
- LICENSE
- README.md
- itd.toml
- itd.service
nfpms:
- id: itd
file_name_template: >-
{{- .PackageName }}-{{.Version}}-{{.Os}}-
{{- if eq .Arch "386" }}i386
{{- else if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{.Arch}}
{{- end }}
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
homepage: 'https://gitea.elara.ws/Elara6331/itd'
maintainer: 'Elara Musayelyan <elara@elara.ws>'
license: GPLv3
formats:
- apk
- deb
- rpm
- archlinux
dependencies:
- dbus
- bluez
contents:
- src: itd.toml
dst: /etc/itd.toml
type: "config|noreplace"
- src: itd.service
dst: /usr/lib/systemd/user/itd.service
file_info:
mode: 0755
aurs:
- name: itd-bin
homepage: 'https://gitea.elara.ws/Elara6331/itd'
description: "Companion daemon for the InfiniTime firmware on the PineTime smartwatch"
maintainers:
- 'Elara Musayelyan <elara@elara.ws>'
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
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: Elara6331
name: itd
gitea_urls:
api: 'https://gitea.elara.ws/api/v1/'
download: 'https://gitea.elara.ws'
skip_tls_verify: false
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
+8
View File
@@ -0,0 +1,8 @@
pipeline:
release:
image: goreleaser/goreleaser
commands:
- goreleaser release
secrets: [ gitea_token, aur_key ]
when:
event: tag
+7 -7
View File
@@ -3,14 +3,14 @@ BIN_PREFIX = $(DESTDIR)$(PREFIX)/bin
SERVICE_PREFIX = $(DESTDIR)$(PREFIX)/lib/systemd/user SERVICE_PREFIX = $(DESTDIR)$(PREFIX)/lib/systemd/user
CFG_PREFIX = $(DESTDIR)/etc CFG_PREFIX = $(DESTDIR)/etc
all: version all: version.txt
go build $(GOFLAGS) go build
go build ./cmd/itctl $(GOFLAGS) go build ./cmd/itctl
clean: clean:
rm -f itctl rm -f itctl
rm -f itd rm -f itd
printf "unknown" > version.txt rm -f version.txt
install: install:
install -Dm755 ./itd $(BIN_PREFIX)/itd install -Dm755 ./itd $(BIN_PREFIX)/itd
@@ -24,7 +24,7 @@ uninstall:
rm $(SERVICE_PREFIX)/itd.service rm $(SERVICE_PREFIX)/itd.service
rm $(CFG_PREFIX)/itd.toml rm $(CFG_PREFIX)/itd.toml
version: version.txt:
printf "r%s.%s" "$(shell git rev-list --count HEAD)" "$(shell git rev-parse --short HEAD)" > version.txt go generate
.PHONY: all clean install uninstall version .PHONY: all clean install uninstall
+82 -78
View File
@@ -1,11 +1,11 @@
# ITD # ITD
## InfiniTime Daemon ## InfiniTime Daemon
`itd` is a daemon that uses my infinitime [library](https://go.arsenm.dev/infinitime) to interact with the [PineTime](https://www.pine64.org/pinetime/) running [InfiniTime](https://infinitime.io). `itd` is a daemon that uses my infinitime [library](https://go.elara.ws/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) [![status-badge](https://ci.elara.ws/api/badges/Elara6331/itd/status.svg)](https://ci.elara.ws/Elara6331/itd)
[![Binary downloads](https://img.shields.io/badge/download-binary-orange)](https://minio.arsenm.dev/minio/itd/) [![itd-git AUR package](https://img.shields.io/aur/version/itd-git?label=itd-git&logo=archlinux)](https://aur.archlinux.org/packages/itd-git/)
[![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,59 +19,40 @@
- Set current time - Set current time
- Control socket - Control socket
- Firmware upgrades - Firmware upgrades
- Weather
- BLE Filesystem
- Navigation (PureMaps)
- FUSE Filesystem
--- ---
### Socket ### Installation
This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directly control the daemon and, by extension, the connected watch. Since ITD 0.0.7, packages are built and uploaded whenever a new release is created.
The socket accepts JSON requests. For example, sending a notification looks like this: #### Arch Linux
```json Use the `itd-bin` or `itd-git` AUR packages.
{"type": 5, "data": {"title": "title1", "body": "body1"}}
```
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. #### Debian/Ubuntu
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. - 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
### Transliteration - 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`
Since the PineTime does not have enough space to store all unicode glyphs, it only stores the ASCII space and Cyrillic. Therefore, this daemon can transliterate unsupported characters into supported ones. Since some languages have different transliterations, the transliterators to be used must be specified in the config. Here are the available transliterators: #### Alpine (and postmarketOS)
- eASCII - 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.
- Scandinavian - Run `sudo apk add --allow-untrusted <package>`, replacing `<package>` with the path to the downloaded file.
- German - Example: `sudo apk add --allow-untrusted ~/Downloads/itd-0.0.7-linux-aarch64.apk`
- Hebrew
- Greek
- Russian
- Ukranian
- Arabic
- Farsi
- Polish
- Lithuanian
- Estonian
- Icelandic
- Czeck
- French
- Armenian
- Korean
- Chinese
- Romanian
- Emoji
Place the desired map names in an array as `notifs.translit.use`. They will be evaluated in order. You can also put custom transliterations in `notifs.translit.custom`. These take priority over any other maps. The `notifs.translit` config section should look like this: Note: `--allow-untrusted` is required because ITD isn't part of a repository, and therefore is not signed.
```toml
[notifs.translit]
use = ["eASCII", "Russian", "Emoji"]
custom = [
"test", "replaced"
]
```
--- ---
@@ -81,72 +62,85 @@ This daemon comes with a binary called `itctl` which uses the socket to control
This is the `itctl` usage screen: This is the `itctl` usage screen:
``` ```
Control the itd daemon for InfiniTime smartwatches NAME:
itctl - A new cli application
Usage: USAGE:
itctl [flags] itctl [global options] command [command options] [arguments...]
itctl [command]
Available Commands: COMMANDS:
firmware Manage InfiniTime firmware help Display help screen for a command
resources, res Handle InfiniTime resource loading
filesystem, fs Perform filesystem operations on the PineTime
firmware, fw Manage InfiniTime firmware
get Get information from InfiniTime get Get information from InfiniTime
help Help about any command
notify Send notification to InfiniTime notify Send notification to InfiniTime
set Set information on InfiniTime set Set information on InfiniTime
update, upd Update information on InfiniTime
watch Watch a value for changes
Flags: GLOBAL OPTIONS:
-h, --help help for itctl --socket-path value, -s value Path to itd socket (default: "/tmp/itd/socket")
-s, --socket-path string Path to itd socket
Use "itctl [command] --help" for more information about a command.
``` ```
--- ---
### `itgui` ### `itgui`
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [fyne library](https://fyne.io/) for Go. It can be compiled by running: In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [Fyne library](https://fyne.io/) for Go.
#### Easy Installation
The easiest way to install `itgui` is to use my other project, [LURE](https://gitea.arsenm.dev/Arsen6331/lure). LURE will only work if your package manager is `apt`, `dnf`, `yum`, `zypper`, `pacman`, or `apk`.
Instructions:
1. Install LURE. This can be done with the following command: `curl https://www.arsenm.dev/lure.sh | bash`.
2. Check to make sure LURE is properly installed by running `lure ref`.
3. Run `lure in itgui`. This process may take a while as it will compile `itgui` from source and package it for your distro.
4. Once the process is complete, you should be able to open and use `itgui` like any other app.
#### Compilation
Before compiling, certain prerequisites must be installed. These are listed on the following page: https://developer.fyne.io/started/#prerequisites
It can be compiled by running:
```shell ```shell
go build ./cmd/itgui go build ./cmd/itgui
``` ```
#### Cross-compilation
Due to the use of OpenGL, cross-compilation of `itgui` isn't as simple as that of `itd` and `itctl`. The following guide from the Fyne website should work for `itgui`: https://developer.fyne.io/started/cross-compiling.
#### Screenshots #### Screenshots
![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)
``` ![Upgrade in progress](cmd/itgui/screenshots/progress.png)
$ itctl
itctl> fw ver ![Metrics tab](cmd/itgui/screenshots/metrics.png)
1.3.0
itctl> get batt
81%
itctl> get heart
92 BPM
itctl> set time 2021-08-22T00:06:18-07:00
itctl> set time now
itctl> exit
```
--- ---
### Installation ### Installation
To install, install the go compiler and make. Usually, go is provided by a package either named `go` or `golang`, and make is usually provided by `make`. The go compiler must be version 1.16 or newer for the `io/fs` module. To install, install the go compiler and make. Usually, go is provided by a package either named `go` or `golang`, and make is usually provided by `make`. The go compiler must be version 1.17 or newer for various new `reflect` features.
To install, run To install, run
```shell ```shell
@@ -155,6 +149,16 @@ make && sudo make install
--- ---
### Socket
This daemon creates a UNIX socket at `/tmp/itd/socket`. It allows you to directly control the daemon and, by extension, the connected watch.
The socket uses the [DRPC](https://github.com/storj/drpc) library for requests. The code generated by this framework is located in [`internal/rpc`](internal/rpc)
The API description is located in the [`internal/rpc/itd.proto`](internal/rpc/itd.proto) file.
---
### Starting ### Starting
To start the daemon, run the following **without root**: To start the daemon, run the following **without root**:
+62
View File
@@ -0,0 +1,62 @@
package api
import (
"io"
"net"
"go.elara.ws/drpc/muxconn"
"go.elara.ws/itd/internal/rpc"
"storj.io/drpc"
)
const DefaultAddr = "/tmp/itd/socket"
// Client is a client for ITD's socket API
type Client struct {
conn drpc.Conn
client rpc.DRPCITDClient
}
// New connects to the UNIX socket at the given
// path, and returns a client that communicates
// with that socket.
func New(sockPath string) (*Client, error) {
conn, err := net.Dial("unix", sockPath)
if err != nil {
return nil, err
}
mconn, err := muxconn.New(conn)
if err != nil {
return nil, err
}
return &Client{
conn: mconn,
client: rpc.NewDRPCITDClient(mconn),
}, nil
}
// NewFromConn returns a client that communicates
// over the given connection.
func NewFromConn(conn io.ReadWriteCloser) (*Client, error) {
mconn, err := muxconn.New(conn)
if err != nil {
return nil, err
}
return &Client{
conn: mconn,
client: rpc.NewDRPCITDClient(mconn),
}, nil
}
// FS returns the filesystem API client
func (c *Client) FS() *FSClient {
return &FSClient{rpc.NewDRPCFSClient(c.conn)}
}
// Close closes the client connection
func (c *Client) Close() error {
return c.conn.Close()
}
-153
View File
@@ -1,153 +0,0 @@
package api
import (
"bufio"
"encoding/json"
"errors"
"net"
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/types"
)
// Default socket address
const DefaultAddr = "/tmp/itd/socket"
// Client is the socket API client
type Client struct {
conn net.Conn
respCh chan types.Response
heartRateCh chan types.Response
battLevelCh chan types.Response
stepCountCh chan types.Response
motionCh chan types.Response
dfuProgressCh chan types.Response
readProgressCh chan types.FSTransferProgress
writeProgressCh chan types.FSTransferProgress
}
// New creates a new client and sets it up
func New(addr string) (*Client, error) {
conn, err := net.Dial("unix", addr)
if err != nil {
return nil, err
}
out := &Client{
conn: conn,
respCh: make(chan types.Response, 5),
}
go func() {
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
var res types.Response
err = json.Unmarshal(scanner.Bytes(), &res)
if err != nil {
continue
}
out.handleResp(res)
}
}()
return out, err
}
func (c *Client) Close() error {
err := c.conn.Close()
if err != nil {
return err
}
close(c.respCh)
return nil
}
// request sends a request to itd and waits for and returns the response
func (c *Client) request(req types.Request) (types.Response, error) {
// Encode request into connection
err := json.NewEncoder(c.conn).Encode(req)
if err != nil {
return types.Response{}, err
}
res := <-c.respCh
if res.Error {
return res, errors.New(res.Message)
}
return res, nil
}
// requestNoRes sends a request to itd and does not wait for the response
func (c *Client) requestNoRes(req types.Request) error {
// Encode request into connection
err := json.NewEncoder(c.conn).Encode(req)
if err != nil {
return err
}
return nil
}
// handleResp handles the received response as needed
func (c *Client) handleResp(res types.Response) error {
switch res.Type {
case types.ReqTypeWatchHeartRate:
c.heartRateCh <- res
case types.ReqTypeWatchBattLevel:
c.battLevelCh <- res
case types.ReqTypeWatchStepCount:
c.stepCountCh <- res
case types.ReqTypeWatchMotion:
c.motionCh <- res
case types.ReqTypeFwUpgrade:
c.dfuProgressCh <- res
case types.ReqTypeFS:
if res.Value == nil {
c.respCh <- res
break
}
var progress types.FSTransferProgress
if err := mapstructure.Decode(res.Value, &progress); err != nil {
c.respCh <- res
break
}
switch progress.Type {
case types.FSTypeRead:
c.readProgressCh <- progress
case types.FSTypeWrite:
c.writeProgressCh <- progress
default:
c.respCh <- res
}
default:
c.respCh <- res
}
return nil
}
func decodeUint8(val interface{}) uint8 {
return uint8(val.(float64))
}
func decodeUint32(val interface{}) uint32 {
return uint32(val.(float64))
}
func decodeMotion(val interface{}) (infinitime.MotionValues, error) {
out := infinitime.MotionValues{}
err := mapstructure.Decode(val, &out)
if err != nil {
return out, err
}
return out, nil
}
func decodeDFUProgress(val interface{}) (DFUProgress, error) {
out := DFUProgress{}
err := mapstructure.Decode(val, &out)
if err != nil {
return out, err
}
return out, nil
}
+36
View File
@@ -0,0 +1,36 @@
package api
import (
"context"
"go.elara.ws/itd/internal/rpc"
)
type DFUProgress struct {
Sent int64
Received int64
Total int64
Err error
}
func (c *Client) FirmwareUpgrade(ctx context.Context, upgType UpgradeType, files ...string) (chan DFUProgress, error) {
progressCh := make(chan DFUProgress, 5)
fc, err := c.client.FirmwareUpgrade(ctx, &rpc.FirmwareUpgradeRequest{
Type: rpc.FirmwareUpgradeRequest_Type(upgType),
Files: files,
})
if err != nil {
return nil, err
}
go fsRecvToChannel[rpc.DFUProgress](fc, progressCh, func(evt *rpc.DFUProgress, err error) DFUProgress {
return DFUProgress{
Sent: evt.Sent,
Received: evt.Recieved,
Total: evt.Total,
Err: err,
}
})
return progressCh, nil
}
+91 -71
View File
@@ -1,102 +1,122 @@
package api package api
import ( import (
"github.com/mitchellh/mapstructure" "context"
"go.arsenm.dev/itd/internal/types" "errors"
"io"
"go.elara.ws/itd/internal/rpc"
) )
func (c *Client) Rename(old, new string) error { type FSClient struct {
_, err := c.request(types.Request{ client rpc.DRPCFSClient
Type: types.ReqTypeFS, }
Data: types.ReqDataFS{
Type: types.FSTypeMove, func (c *FSClient) RemoveAll(ctx context.Context, paths ...string) error {
Files: []string{old, new}, _, err := c.client.RemoveAll(ctx, &rpc.PathsRequest{Paths: paths})
},
})
if err != nil {
return err return err
}
return nil
} }
func (c *Client) Remove(paths ...string) error { func (c *FSClient) Remove(ctx context.Context, paths ...string) error {
_, err := c.request(types.Request{ _, err := c.client.Remove(ctx, &rpc.PathsRequest{Paths: paths})
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeDelete,
Files: paths,
},
})
if err != nil {
return err return err
}
return nil
} }
func (c *Client) Mkdir(paths ...string) error { func (c *FSClient) Rename(ctx context.Context, old, new string) error {
_, err := c.request(types.Request{ _, err := c.client.Rename(ctx, &rpc.RenameRequest{
Type: types.ReqTypeFS, From: old,
Data: types.ReqDataFS{ To: new,
Type: types.FSTypeMkdir,
Files: paths,
},
}) })
if err != nil {
return err return err
}
return nil
} }
func (c *Client) ReadDir(path string) ([]types.FileInfo, error) { func (c *FSClient) MkdirAll(ctx context.Context, paths ...string) error {
res, err := c.request(types.Request{ _, err := c.client.MkdirAll(ctx, &rpc.PathsRequest{Paths: paths})
Type: types.ReqTypeFS, return err
Data: types.ReqDataFS{ }
Type: types.FSTypeList,
Files: []string{path}, func (c *FSClient) Mkdir(ctx context.Context, paths ...string) error {
}, _, err := c.client.Mkdir(ctx, &rpc.PathsRequest{Paths: paths})
}) return err
}
func (c *FSClient) ReadDir(ctx context.Context, dir string) ([]FileInfo, error) {
res, err := c.client.ReadDir(ctx, &rpc.PathRequest{Path: dir})
if err != nil { if err != nil {
return nil, err return nil, err
} }
var out []types.FileInfo return convertEntries(res.Entries), nil
err = mapstructure.Decode(res.Value, &out) }
func convertEntries(e []*rpc.FileInfo) []FileInfo {
out := make([]FileInfo, len(e))
for i, fi := range e {
out[i] = FileInfo{
Name: fi.Name,
Size: fi.Size,
IsDir: fi.IsDir,
}
}
return out
}
func (c *FSClient) Upload(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
progressCh := make(chan FSTransferProgress, 5)
tc, err := c.client.Upload(ctx, &rpc.TransferRequest{Source: src, Destination: dst})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return out, nil
}
func (c *Client) ReadFile(localPath, remotePath string) (<-chan types.FSTransferProgress, error) { go fsRecvToChannel[rpc.TransferProgress](tc, progressCh, func(evt *rpc.TransferProgress, err error) FSTransferProgress {
c.readProgressCh = make(chan types.FSTransferProgress, 5) return FSTransferProgress{
Sent: evt.Sent,
_, err := c.request(types.Request{ Total: evt.Total,
Type: types.ReqTypeFS, Err: err,
Data: types.ReqDataFS{ }
Type: types.FSTypeRead,
Files: []string{localPath, remotePath},
},
}) })
return progressCh, nil
}
func (c *FSClient) Download(ctx context.Context, dst, src string) (chan FSTransferProgress, error) {
progressCh := make(chan FSTransferProgress, 5)
tc, err := c.client.Download(ctx, &rpc.TransferRequest{Source: src, Destination: dst})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.readProgressCh, nil go fsRecvToChannel[rpc.TransferProgress](tc, progressCh, func(evt *rpc.TransferProgress, err error) FSTransferProgress {
} return FSTransferProgress{
Sent: evt.Sent,
func (c *Client) WriteFile(localPath, remotePath string) (<-chan types.FSTransferProgress, error) { Total: evt.Total,
c.writeProgressCh = make(chan types.FSTransferProgress, 5) Err: err,
}
_, err := c.request(types.Request{
Type: types.ReqTypeFS,
Data: types.ReqDataFS{
Type: types.FSTypeWrite,
Files: []string{remotePath, localPath},
},
}) })
if err != nil {
return nil, err
}
return c.writeProgressCh, nil return progressCh, nil
}
// fsRecvToChannel converts a DRPC stream client to a Go channel, using cf to convert
// RPC generated types to API response types.
func fsRecvToChannel[R any, A any](s StreamClient[R], ch chan<- A, cf func(evt *R, err error) A) {
defer close(ch)
var err error
var evt *R
for {
select {
case <-s.Context().Done():
return
default:
evt, err = s.Recv()
if errors.Is(err, io.EOF) {
return
} else if err != nil {
ch <- cf(new(R), err)
return
}
ch <- cf(evt, nil)
}
}
} }
+41
View File
@@ -0,0 +1,41 @@
package api
import (
"context"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) HeartRate(ctx context.Context) (uint8, error) {
res, err := c.client.HeartRate(ctx, &rpc.Empty{})
return uint8(res.Value), err
}
func (c *Client) BatteryLevel(ctx context.Context) (uint8, error) {
res, err := c.client.BatteryLevel(ctx, &rpc.Empty{})
return uint8(res.Value), err
}
type MotionValues struct {
X, Y, Z int16
}
func (c *Client) Motion(ctx context.Context) (MotionValues, error) {
res, err := c.client.Motion(ctx, &rpc.Empty{})
return MotionValues{int16(res.X), int16(res.Y), int16(res.Z)}, err
}
func (c *Client) StepCount(ctx context.Context) (out uint32, err error) {
res, err := c.client.StepCount(ctx, &rpc.Empty{})
return res.Value, err
}
func (c *Client) Version(ctx context.Context) (out string, err error) {
res, err := c.client.Version(ctx, &rpc.Empty{})
return res.Value, err
}
func (c *Client) Address(ctx context.Context) (out string, err error) {
res, err := c.client.Address(ctx, &rpc.Empty{})
return res.Value, err
}
-209
View File
@@ -1,209 +0,0 @@
package api
import (
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/types"
)
// Address gets the bluetooth address of the connected device
func (c *Client) Address() (string, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeBtAddress,
})
if err != nil {
return "", err
}
return res.Value.(string), nil
}
// Version gets the firmware version of the connected device
func (c *Client) Version() (string, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeFwVersion,
})
if err != nil {
return "", err
}
return res.Value.(string), nil
}
// BatteryLevel gets the battery level of the connected device
func (c *Client) BatteryLevel() (uint8, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeBattLevel,
})
if err != nil {
return 0, err
}
return uint8(res.Value.(float64)), nil
}
// WatchBatteryLevel returns a channel which will contain
// new battery level values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchBatteryLevel() (<-chan uint8, func(), error) {
c.battLevelCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchBattLevel,
})
if err != nil {
return nil, nil, err
}
res := <-c.battLevelCh
done, cancel := c.cancelFn(res.ID, c.battLevelCh)
out := make(chan uint8, 2)
go func() {
for res := range c.battLevelCh {
select {
case <-done:
return
default:
out <- decodeUint8(res.Value)
}
}
}()
return out, cancel, nil
}
// HeartRate gets the heart rate from the connected device
func (c *Client) HeartRate() (uint8, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeHeartRate,
})
if err != nil {
return 0, err
}
return decodeUint8(res.Value), nil
}
// WatchHeartRate returns a channel which will contain
// new heart rate values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchHeartRate() (<-chan uint8, func(), error) {
c.heartRateCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchHeartRate,
})
if err != nil {
return nil, nil, err
}
res := <-c.heartRateCh
done, cancel := c.cancelFn(res.ID, c.heartRateCh)
out := make(chan uint8, 2)
go func() {
for res := range c.heartRateCh {
select {
case <-done:
return
default:
out <- decodeUint8(res.Value)
}
}
}()
return out, cancel, nil
}
// cancelFn generates a cancellation function for the given
// request type and channel
func (c *Client) cancelFn(reqID string, ch chan types.Response) (chan struct{}, func()) {
done := make(chan struct{}, 1)
return done, func() {
done <- struct{}{}
close(ch)
c.requestNoRes(types.Request{
Type: types.ReqTypeCancel,
Data: reqID,
})
}
}
// StepCount gets the step count from the connected device
func (c *Client) StepCount() (uint32, error) {
res, err := c.request(types.Request{
Type: types.ReqTypeStepCount,
})
if err != nil {
return 0, err
}
return uint32(res.Value.(float64)), nil
}
// WatchStepCount returns a channel which will contain
// new step count values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchStepCount() (<-chan uint32, func(), error) {
c.stepCountCh = make(chan types.Response, 2)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchStepCount,
})
if err != nil {
return nil, nil, err
}
res := <-c.stepCountCh
done, cancel := c.cancelFn(res.ID, c.stepCountCh)
out := make(chan uint32, 2)
go func() {
for res := range c.stepCountCh {
select {
case <-done:
return
default:
out <- decodeUint32(res.Value)
}
}
}()
return out, cancel, nil
}
// Motion gets the motion values from the connected device
func (c *Client) Motion() (infinitime.MotionValues, error) {
out := infinitime.MotionValues{}
res, err := c.request(types.Request{
Type: types.ReqTypeMotion,
})
if err != nil {
return out, err
}
err = mapstructure.Decode(res.Value, &out)
if err != nil {
return out, err
}
return out, nil
}
// WatchMotion returns a channel which will contain
// new motion values as they update. Do not use after
// calling cancellation function
func (c *Client) WatchMotion() (<-chan infinitime.MotionValues, func(), error) {
c.motionCh = make(chan types.Response, 5)
err := c.requestNoRes(types.Request{
Type: types.ReqTypeWatchMotion,
})
if err != nil {
return nil, nil, err
}
res := <-c.motionCh
done, cancel := c.cancelFn(res.ID, c.motionCh)
out := make(chan infinitime.MotionValues, 5)
go func() {
for res := range c.motionCh {
select {
case <-done:
return
default:
motion, err := decodeMotion(res.Value)
if err != nil {
continue
}
out <- motion
}
}
}()
return out, cancel, nil
}
+7 -6
View File
@@ -1,14 +1,15 @@
package api package api
import "go.arsenm.dev/itd/internal/types" import (
"context"
func (c *Client) Notify(title string, body string) error { "go.elara.ws/itd/internal/rpc"
_, err := c.request(types.Request{ )
Type: types.ReqTypeNotify,
Data: types.ReqDataNotify{ func (c *Client) Notify(ctx context.Context, title, body string) error {
_, err := c.client.Notify(ctx, &rpc.NotifyRequest{
Title: title, Title: title,
Body: body, Body: body,
},
}) })
return err return err
} }
+51
View File
@@ -0,0 +1,51 @@
package api
import (
"context"
"go.elara.ws/infinitime"
"go.elara.ws/itd/internal/rpc"
)
type ResourceOperation uint8
const (
ResourceOperationRemoveObsolete = infinitime.ResourceOperationRemoveObsolete
ResourceOperationUpload = infinitime.ResourceOperationUpload
)
type ResourceLoadProgress struct {
Operation ResourceOperation
Name string
Total int64
Sent int64
Err error
}
// LoadResources loads resources onto the watch from the given
// file path to the resources zip
func (c *FSClient) LoadResources(ctx context.Context, path string) (<-chan ResourceLoadProgress, error) {
progCh := make(chan ResourceLoadProgress, 2)
rc, err := c.client.LoadResources(ctx, &rpc.PathRequest{Path: path})
if err != nil {
return nil, err
}
go fsRecvToChannel[rpc.ResourceLoadProgress](rc, progCh, func(evt *rpc.ResourceLoadProgress, err error) ResourceLoadProgress {
return ResourceLoadProgress{
Operation: ResourceOperation(evt.Operation),
Name: evt.Name,
Sent: evt.Sent,
Total: evt.Total,
Err: err,
}
})
return progCh, nil
}
type StreamClient[T any] interface {
Recv() (*T, error)
Context() context.Context
}
+13
View File
@@ -0,0 +1,13 @@
package api
import (
"context"
"time"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) SetTime(ctx context.Context, t time.Time) error {
_, err := c.client.SetTime(ctx, &rpc.SetTimeRequest{UnixNano: t.UnixNano()})
return err
}
-33
View File
@@ -1,33 +0,0 @@
package api
import (
"time"
"go.arsenm.dev/itd/internal/types"
)
// SetTime sets the given time on the connected device
func (c *Client) SetTime(t time.Time) error {
_, err := c.request(types.Request{
Type: types.ReqTypeSetTime,
Data: t.Format(time.RFC3339),
})
if err != nil {
return err
}
return nil
}
// SetTimeNow sets the time on the connected device to
// the current time. This is more accurate than
// SetTime(time.Now()) due to RFC3339 formatting
func (c *Client) SetTimeNow() error {
_, err := c.request(types.Request{
Type: types.ReqTypeSetTime,
Data: "now",
})
if err != nil {
return err
}
return nil
}
+97
View File
@@ -0,0 +1,97 @@
package api
import (
"fmt"
"strconv"
)
type UpgradeType uint8
const (
UpgradeTypeArchive UpgradeType = iota
UpgradeTypeFiles
)
type FSData struct {
Files []string
Data string
}
type FwUpgradeData struct {
Type UpgradeType
Files []string
}
type NotifyData struct {
Title string
Body string
}
type FSTransferProgress struct {
Total uint32
Sent uint32
Err error
}
type FileInfo struct {
Name string
Size int64
IsDir bool
}
func (fi FileInfo) String() string {
var isDirChar rune
if fi.IsDir {
isDirChar = 'd'
} else {
isDirChar = '-'
}
// Get human-readable value for file size
val, unit := bytesHuman(fi.Size)
prec := 0
// If value is less than 10, set precision to 1
if val < 10 {
prec = 1
}
// Convert float to string
valStr := strconv.FormatFloat(val, 'f', prec, 64)
// Return string formatted like so:
// - 10 kB file
// or:
// d 0 B .
return fmt.Sprintf(
"%c %3s %-2s %s",
isDirChar,
valStr,
unit,
fi.Name,
)
}
// bytesHuman returns a human-readable string for
// the amount of bytes inputted.
func bytesHuman(b int64) (float64, string) {
const unit = 1000
// Set possible units prefixes (PineTime flash is 4MB)
units := [2]rune{'k', 'M'}
// If amount of bytes is less than smallest unit
if b < unit {
// Return unchanged with unit "B"
return float64(b), "B"
}
div, exp := int64(unit), 0
// Get decimal values and unit prefix index
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
// Create string for full unit
unitStr := string([]rune{units[exp], 'B'})
// Return decimal with unit string
return float64(b) / float64(div), unitStr
}
+12
View File
@@ -0,0 +1,12 @@
package api
import (
"context"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) WeatherUpdate(ctx context.Context) error {
_, err := c.client.WeatherUpdate(ctx, &rpc.Empty{})
return err
}
-48
View File
@@ -1,48 +0,0 @@
package api
import (
"encoding/json"
"go.arsenm.dev/itd/internal/types"
)
// DFUProgress stores the progress of a DFU upfate
type DFUProgress types.DFUProgress
// UpgradeType indicates the type of upgrade to be performed
type UpgradeType uint8
// Type of DFU upgrade
const (
UpgradeTypeArchive UpgradeType = iota
UpgradeTypeFiles
)
// FirmwareUpgrade initiates a DFU update and returns the progress channel
func (c *Client) FirmwareUpgrade(upgType UpgradeType, files ...string) (<-chan DFUProgress, error) {
err := json.NewEncoder(c.conn).Encode(types.Request{
Type: types.ReqTypeFwUpgrade,
Data: types.ReqDataFwUpgrade{
Type: int(upgType),
Files: files,
},
})
if err != nil {
return nil, err
}
c.dfuProgressCh = make(chan types.Response, 5)
out := make(chan DFUProgress, 5)
go func() {
for res := range c.dfuProgressCh {
progress, err := decodeDFUProgress(res.Value)
if err != nil {
continue
}
out <- progress
}
}()
return out, nil
}
+135
View File
@@ -0,0 +1,135 @@
package api
import (
"context"
"go.elara.ws/itd/internal/rpc"
)
func (c *Client) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
outCh := make(chan uint8, 2)
wc, err := c.client.WatchHeartRate(ctx, &rpc.Empty{})
if err != nil {
return nil, err
}
go func() {
defer close(outCh)
var err error
var evt *rpc.IntResponse
for {
select {
case <-ctx.Done():
wc.Close()
return
default:
evt, err = wc.Recv()
if err != nil {
return
}
}
outCh <- uint8(evt.Value)
}
}()
return outCh, nil
}
func (c *Client) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
outCh := make(chan uint8, 2)
wc, err := c.client.WatchBatteryLevel(ctx, &rpc.Empty{})
if err != nil {
return nil, err
}
go func() {
defer close(outCh)
var err error
var evt *rpc.IntResponse
for {
select {
case <-ctx.Done():
wc.Close()
return
default:
evt, err = wc.Recv()
if err != nil {
return
}
}
outCh <- uint8(evt.Value)
}
}()
return outCh, nil
}
func (c *Client) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
outCh := make(chan uint32, 2)
wc, err := c.client.WatchStepCount(ctx, &rpc.Empty{})
if err != nil {
return nil, err
}
go func() {
defer close(outCh)
var err error
var evt *rpc.IntResponse
for {
select {
case <-ctx.Done():
wc.Close()
return
default:
evt, err = wc.Recv()
if err != nil {
return
}
}
outCh <- evt.Value
}
}()
return outCh, nil
}
func (c *Client) WatchMotion(ctx context.Context) (<-chan MotionValues, error) {
outCh := make(chan MotionValues, 2)
wc, err := c.client.WatchMotion(ctx, &rpc.Empty{})
if err != nil {
return nil, err
}
go func() {
defer close(outCh)
var err error
var evt *rpc.MotionResponse
for {
select {
case <-ctx.Done():
wc.Close()
return
default:
evt, err = wc.Recv()
if err != nil {
return
}
}
outCh <- MotionValues{int16(evt.X), int16(evt.Y), int16(evt.Z)}
}
}()
return outCh, nil
}
-17
View File
@@ -1,17 +0,0 @@
package api
import (
"go.arsenm.dev/itd/internal/types"
)
// UpdateWeather sends the update weather signal,
// immediately sending current weather data
func (c *Client) UpdateWeather() error {
_, err := c.request(types.Request{
Type: types.ReqTypeWeatherUpdate,
})
if err != nil {
return err
}
return nil
}
+65 -21
View File
@@ -1,22 +1,24 @@
package main package main
import ( import (
"context"
"sync" "sync"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/rs/zerolog/log" "go.elara.ws/infinitime"
"go.arsenm.dev/infinitime" "go.elara.ws/itd/internal/utils"
"go.elara.ws/logger/log"
) )
func initCallNotifs(dev *infinitime.Device) error { func initCallNotifs(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Connect to system bus. This connection is for method calls. // Connect to system bus. This connection is for method calls.
conn, err := newSystemBusConn() conn, err := utils.NewSystemBusConn(ctx)
if err != nil { if err != nil {
return err return err
} }
// Check if modem manager interface exists // Check if modem manager interface exists
exists, err := modemManagerExists(conn) exists, err := modemManagerExists(ctx, conn)
if err != nil { if err != nil {
return err return err
} }
@@ -28,7 +30,7 @@ func initCallNotifs(dev *infinitime.Device) error {
} }
// Connect to system bus. This connection is for monitoring. // Connect to system bus. This connection is for monitoring.
monitorConn, err := newSystemBusConn() monitorConn, err := utils.NewSystemBusConn(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -51,9 +53,12 @@ func initCallNotifs(dev *infinitime.Device) error {
var respHandlerOnce sync.Once var respHandlerOnce sync.Once
var callObj dbus.BusObject var callObj dbus.BusObject
wg.Add(1)
go func() { go func() {
// For every message received defer wg.Done("callNotifs")
for event := range callCh { for {
select {
case event := <-callCh:
// Get path to call object // Get path to call object
callPath := event.Body[0].(dbus.ObjectPath) callPath := event.Body[0].(dbus.ObjectPath)
// Get call object // Get call object
@@ -62,7 +67,18 @@ func initCallNotifs(dev *infinitime.Device) error {
// Get phone number from call object using method call connection // Get phone number from call object using method call connection
phoneNum, err := getPhoneNum(conn, callObj) phoneNum, err := getPhoneNum(conn, callObj)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error getting phone number") log.Error("Error getting phone number").Err(err).Send()
continue
}
// Get direction of call object using method call connection
direction, err := getDirection(conn, callObj)
if err != nil {
log.Error("Error getting call direction").Err(err).Send()
continue
}
if direction != MMCallDirectionIncoming {
continue continue
} }
@@ -78,32 +94,37 @@ func initCallNotifs(dev *infinitime.Device) error {
switch res { switch res {
case infinitime.CallStatusAccepted: case infinitime.CallStatusAccepted:
// Attempt to accept call // Attempt to accept call
err = acceptCall(conn, callObj) err = acceptCall(ctx, conn, callObj)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Error accepting call") log.Warn("Error accepting call").Err(err).Send()
} }
case infinitime.CallStatusDeclined: case infinitime.CallStatusDeclined:
// Attempt to decline call // Attempt to decline call
err = declineCall(conn, callObj) err = declineCall(ctx, conn, callObj)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Error declining call") log.Warn("Error declining call").Err(err).Send()
} }
case infinitime.CallStatusMuted: case infinitime.CallStatusMuted:
// Warn about unimplemented muting // Warn about unimplemented muting
log.Warn().Msg("Muting calls is not implemented") log.Warn("Muting calls is not implemented").Send()
} }
} }
}) })
case <-ctx.Done():
return
}
} }
}() }()
log.Info().Msg("Relaying calls to InfiniTime") log.Info("Relaying calls to InfiniTime").Send()
return nil return nil
} }
func modemManagerExists(conn *dbus.Conn) (bool, error) { func modemManagerExists(ctx context.Context, conn *dbus.Conn) (bool, error) {
var names []string var names []string
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names) err := conn.BusObject().CallWithContext(
ctx, "org.freedesktop.DBus.ListNames", 0,
).Store(&names)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -121,10 +142,31 @@ func getPhoneNum(conn *dbus.Conn, callObj dbus.BusObject) (string, error) {
return out, nil return out, nil
} }
type MMCallDirection int
const (
MMCallDirectionUnknown MMCallDirection = iota
MMCallDirectionIncoming
MMCallDirectionOutgoing
)
// getDirection gets the direction of a call object using a DBus connection
func getDirection(conn *dbus.Conn, callObj dbus.BusObject) (MMCallDirection, error) {
var out MMCallDirection
// Get number property on DBus object and store return value in out
err := callObj.StoreProperty("org.freedesktop.ModemManager1.Call.Direction", &out)
if err != nil {
return 0, err
}
return out, nil
}
// getPhoneNum accepts a call using a DBus connection // getPhoneNum accepts a call using a DBus connection
func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error { func acceptCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
// Call Accept() method on DBus object // Call Accept() method on DBus object
call := callObj.Call("org.freedesktop.ModemManager1.Call.Accept", 0) call := callObj.CallWithContext(
ctx, "org.freedesktop.ModemManager1.Call.Accept", 0,
)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
@@ -132,9 +174,11 @@ func acceptCall(conn *dbus.Conn, callObj dbus.BusObject) error {
} }
// getPhoneNum declines a call using a DBus connection // getPhoneNum declines a call using a DBus connection
func declineCall(conn *dbus.Conn, callObj dbus.BusObject) error { func declineCall(ctx context.Context, conn *dbus.Conn, callObj dbus.BusObject) error {
// Call Hangup() method on DBus object // Call Hangup() method on DBus object
call := callObj.Call("org.freedesktop.ModemManager1.Call.Hangup", 0) call := callObj.CallWithContext(
ctx, "org.freedesktop.ModemManager1.Call.Hangup", 0,
)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
+26 -8
View File
@@ -7,11 +7,25 @@ import (
"github.com/cheggaaa/pb/v3" "github.com/cheggaaa/pb/v3"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.arsenm.dev/itd/api" "go.elara.ws/itd/api"
"go.arsenm.dev/itd/internal/types" "go.elara.ws/logger/log"
) )
func fwUpgrade(c *cli.Context) error { func fwUpgrade(c *cli.Context) error {
resources := c.String("resources")
if resources != "" {
absRes, err := filepath.Abs(resources)
if err != nil {
return err
}
err = resLoad(c.Context, []string{absRes})
if err != nil {
log.Error("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.").Send()
return err
}
}
start := time.Now() start := time.Now()
var upgType api.UpgradeType var upgType api.UpgradeType
@@ -19,17 +33,17 @@ func fwUpgrade(c *cli.Context) error {
// Get relevant data struct // Get relevant data struct
if c.String("archive") != "" { if c.String("archive") != "" {
// Get archive data struct // Get archive data struct
upgType = types.UpgradeTypeArchive upgType = api.UpgradeTypeArchive
files = []string{c.String("archive")} files = []string{c.String("archive")}
} else if c.String("init-packet") != "" && c.String("firmware") != "" { } else if c.String("init-packet") != "" && c.String("firmware") != "" {
// Get files data struct // Get files data struct
upgType = types.UpgradeTypeFiles upgType = api.UpgradeTypeFiles
files = []string{c.String("init-packet"), c.String("firmware")} files = []string{c.String("init-packet"), c.String("firmware")}
} else { } else {
return cli.Exit("Upgrade command requires either archive or init packet and firmware.", 1) return cli.Exit("Upgrade command requires either archive or init packet and firmware.", 1)
} }
progress, err := client.FirmwareUpgrade(upgType, abs(files)...) progress, err := client.FirmwareUpgrade(c.Context, upgType, abs(files)...)
if err != nil { if err != nil {
return err return err
} }
@@ -40,12 +54,16 @@ func fwUpgrade(c *cli.Context) error {
bar := pb.ProgressBarTemplate(barTmpl).Start(0) bar := pb.ProgressBarTemplate(barTmpl).Start(0)
// Create new scanner of connection // Create new scanner of connection
for event := range progress { for event := range progress {
if event.Err != nil {
return event.Err
}
// Set total bytes in progress bar // Set total bytes in progress bar
bar.SetTotal(event.Total) bar.SetTotal(event.Total)
// Set amount of bytes received in progress bar // Set amount of bytes received in progress bar
bar.SetCurrent(event.Received) bar.SetCurrent(int64(event.Received))
// If transfer finished, break // If transfer finished, break
if event.Sent == event.Total { if int64(event.Sent) == event.Total {
break break
} }
} }
@@ -59,7 +77,7 @@ func fwUpgrade(c *cli.Context) error {
} }
func fwVersion(c *cli.Context) error { func fwVersion(c *cli.Context) error {
version, err := client.Version() version, err := client.Version(c.Context)
if err != nil { if err != nil {
return err return err
} }
+27 -19
View File
@@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@@ -17,7 +16,7 @@ func fsList(c *cli.Context) error {
dirPath = c.Args().Get(0) dirPath = c.Args().Get(0)
} }
listing, err := client.ReadDir(dirPath) listing, err := client.FS().ReadDir(c.Context, dirPath)
if err != nil { if err != nil {
return err return err
} }
@@ -34,7 +33,12 @@ func fsMkdir(c *cli.Context) error {
return cli.Exit("Command mkdir requires one or more arguments", 1) return cli.Exit("Command mkdir requires one or more arguments", 1)
} }
err := client.Mkdir(c.Args().Slice()...) var err error
if c.Bool("parents") {
err = client.FS().MkdirAll(c.Context, c.Args().Slice()...)
} else {
err = client.FS().Mkdir(c.Context, c.Args().Slice()...)
}
if err != nil { if err != nil {
return err return err
} }
@@ -47,7 +51,7 @@ func fsMove(c *cli.Context) error {
return cli.Exit("Command move requires two arguments", 1) return cli.Exit("Command move requires two arguments", 1)
} }
err := client.Rename(c.Args().Get(0), c.Args().Get(1)) err := client.FS().Rename(c.Context, c.Args().Get(0), c.Args().Get(1))
if err != nil { if err != nil {
return err return err
} }
@@ -64,7 +68,7 @@ func fsRead(c *cli.Context) error {
var path string var path string
var err error var err error
if c.Args().Get(1) == "-" { if c.Args().Get(1) == "-" {
tmpFile, err = ioutil.TempFile("/tmp", "itctl.*") tmpFile, err = os.CreateTemp("/tmp", "itctl.*")
if err != nil { if err != nil {
return err return err
} }
@@ -76,7 +80,7 @@ func fsRead(c *cli.Context) error {
} }
} }
progress, err := client.ReadFile(path, c.Args().Get(0)) progress, err := client.FS().Download(c.Context, path, c.Args().Get(0))
if err != nil { if err != nil {
return err return err
} }
@@ -87,16 +91,16 @@ func fsRead(c *cli.Context) error {
bar := pb.ProgressBarTemplate(barTmpl).Start(0) bar := pb.ProgressBarTemplate(barTmpl).Start(0)
// Get progress events // Get progress events
for event := range progress { for event := range progress {
if event.Err != nil {
return event.Err
}
// Set total bytes in progress bar // Set total bytes in progress bar
bar.SetTotal(int64(event.Total)) bar.SetTotal(int64(event.Total))
// Set amount of bytes sent in progress bar // Set amount of bytes sent in progress bar
bar.SetCurrent(int64(event.Sent)) bar.SetCurrent(int64(event.Sent))
// If transfer finished, break }
if event.Done {
bar.Finish() bar.Finish()
break
}
}
if c.Args().Get(1) == "-" { if c.Args().Get(1) == "-" {
io.Copy(os.Stdout, tmpFile) io.Copy(os.Stdout, tmpFile)
@@ -113,7 +117,12 @@ func fsRemove(c *cli.Context) error {
return cli.Exit("Command remove requires one or more arguments", 1) return cli.Exit("Command remove requires one or more arguments", 1)
} }
err := client.Remove(c.Args().Slice()...) var err error
if c.Bool("recursive") {
err = client.FS().RemoveAll(c.Context, c.Args().Slice()...)
} else {
err = client.FS().Remove(c.Context, c.Args().Slice()...)
}
if err != nil { if err != nil {
return err return err
} }
@@ -130,7 +139,7 @@ func fsWrite(c *cli.Context) error {
var path string var path string
var err error var err error
if c.Args().Get(0) == "-" { if c.Args().Get(0) == "-" {
tmpFile, err = ioutil.TempFile("/tmp", "itctl.*") tmpFile, err = os.CreateTemp("/tmp", "itctl.*")
if err != nil { if err != nil {
return err return err
} }
@@ -148,7 +157,7 @@ func fsWrite(c *cli.Context) error {
defer os.Remove(path) defer os.Remove(path)
} }
progress, err := client.WriteFile(path, c.Args().Get(1)) progress, err := client.FS().Upload(c.Context, c.Args().Get(1), path)
if err != nil { if err != nil {
return err return err
} }
@@ -159,15 +168,14 @@ func fsWrite(c *cli.Context) error {
bar := pb.ProgressBarTemplate(barTmpl).Start(0) bar := pb.ProgressBarTemplate(barTmpl).Start(0)
// Get progress events // Get progress events
for event := range progress { for event := range progress {
if event.Err != nil {
return event.Err
}
// Set total bytes in progress bar // Set total bytes in progress bar
bar.SetTotal(int64(event.Total)) bar.SetTotal(int64(event.Total))
// Set amount of bytes sent in progress bar // Set amount of bytes sent in progress bar
bar.SetCurrent(int64(event.Sent)) bar.SetCurrent(int64(event.Sent))
// If transfer finished, break
if event.Done {
bar.Finish()
break
}
} }
return nil return nil
+5 -5
View File
@@ -9,7 +9,7 @@ import (
) )
func getAddress(c *cli.Context) error { func getAddress(c *cli.Context) error {
address, err := client.Address() address, err := client.Address(c.Context)
if err != nil { if err != nil {
return err return err
} }
@@ -19,7 +19,7 @@ func getAddress(c *cli.Context) error {
} }
func getBattery(c *cli.Context) error { func getBattery(c *cli.Context) error {
battLevel, err := client.BatteryLevel() battLevel, err := client.BatteryLevel(c.Context)
if err != nil { if err != nil {
return err return err
} }
@@ -30,7 +30,7 @@ func getBattery(c *cli.Context) error {
} }
func getHeart(c *cli.Context) error { func getHeart(c *cli.Context) error {
heartRate, err := client.HeartRate() heartRate, err := client.HeartRate(c.Context)
if err != nil { if err != nil {
return err return err
} }
@@ -41,7 +41,7 @@ func getHeart(c *cli.Context) error {
} }
func getMotion(c *cli.Context) error { func getMotion(c *cli.Context) error {
motionVals, err := client.Motion() motionVals, err := client.Motion(c.Context)
if err != nil { if err != nil {
return err return err
} }
@@ -60,7 +60,7 @@ func getMotion(c *cli.Context) error {
} }
func getSteps(c *cli.Context) error { func getSteps(c *cli.Context) error {
stepCount, err := client.StepCount() stepCount, err := client.StepCount(c.Context)
if err != nil { if err != nil {
return err return err
} }
+127 -6
View File
@@ -1,21 +1,41 @@
package main package main
import ( import (
"context"
"os" "os"
"os/signal"
"syscall"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.arsenm.dev/itd/api" "go.elara.ws/itd/api"
"go.elara.ws/logger"
"go.elara.ws/logger/log"
) )
var client *api.Client var client *api.Client
func main() { func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = logger.NewPretty(os.Stderr)
ctx := context.Background()
ctx, _ = signal.NotifyContext(
ctx,
syscall.SIGINT,
syscall.SIGTERM,
)
// This goroutine ensures that itctl will exit
// at most 200ms after the user sends SIGINT/SIGTERM.
go func() {
<-ctx.Done()
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}()
app := cli.App{ app := cli.App{
Name: "itctl", Name: "itctl",
HideHelpCommand: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "socket-path", Name: "socket-path",
@@ -25,6 +45,25 @@ func main() {
}, },
}, },
Commands: []*cli.Command{ Commands: []*cli.Command{
{
Name: "help",
ArgsUsage: "<command>",
Usage: "Display help screen for a command",
Action: helpCmd,
},
{
Name: "resources",
Aliases: []string{"res"},
Usage: "Handle InfiniTime resource loading",
Subcommands: []*cli.Command{
{
Name: "load",
ArgsUsage: "<path>",
Usage: "Load an InifiniTime resources package",
Action: resourcesLoad,
},
},
},
{ {
Name: "filesystem", Name: "filesystem",
Aliases: []string{"fs"}, Aliases: []string{"fs"},
@@ -38,6 +77,13 @@ func main() {
Action: fsList, Action: fsList,
}, },
{ {
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "parents",
Aliases: []string{"p"},
Usage: "Make parent directories if needed, no error if already existing",
},
},
Name: "mkdir", Name: "mkdir",
ArgsUsage: "<paths...>", ArgsUsage: "<paths...>",
Usage: "Create new directories", Usage: "Create new directories",
@@ -58,6 +104,13 @@ func main() {
Action: fsRead, Action: fsRead,
}, },
{ {
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "recursive",
Aliases: []string{"r", "R"},
Usage: "Remove directories and their contents recursively",
},
},
Name: "remove", Name: "remove",
ArgsUsage: "<paths...>", ArgsUsage: "<paths...>",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
@@ -90,6 +143,11 @@ func main() {
Aliases: []string{"f"}, Aliases: []string{"f"},
Usage: "Path to firmware image (.bin file)", Usage: "Path to firmware image (.bin file)",
}, },
&cli.PathFlag{
Name: "resources",
Aliases: []string{"r"},
Usage: "Path to resources file (.zip file)",
},
&cli.PathFlag{ &cli.PathFlag{
Name: "archive", Name: "archive",
Aliases: []string{"a"}, Aliases: []string{"a"},
@@ -174,13 +232,58 @@ func main() {
}, },
}, },
}, },
{
Name: "watch",
Usage: "Watch a value for changes",
Subcommands: []*cli.Command{
{
Flags: []cli.Flag{
&cli.BoolFlag{Name: "json"},
&cli.BoolFlag{Name: "shell"},
},
Name: "heart",
Usage: "Watch heart rate value for changes",
Action: watchHeart,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{Name: "json"},
&cli.BoolFlag{Name: "shell"},
},
Name: "steps",
Usage: "Watch step count value for changes",
Action: watchStepCount,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{Name: "json"},
&cli.BoolFlag{Name: "shell"},
},
Name: "motion",
Usage: "Watch motion coordinates for changes",
Action: watchMotion,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{Name: "json"},
&cli.BoolFlag{Name: "shell"},
},
Name: "battery",
Aliases: []string{"batt"},
Usage: "Watch battery level value for changes",
Action: watchBattLevel,
},
},
},
}, },
Before: func(c *cli.Context) error { Before: func(c *cli.Context) error {
if !isHelpCmd() {
newClient, err := api.New(c.String("socket-path")) newClient, err := api.New(c.String("socket-path"))
if err != nil { if err != nil {
return err return err
} }
client = newClient client = newClient
}
return nil return nil
}, },
After: func(*cli.Context) error { After: func(*cli.Context) error {
@@ -191,8 +294,26 @@ func main() {
}, },
} }
err := app.Run(os.Args) err := app.RunContext(ctx, os.Args)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Error while running app") log.Fatal("Error while running app").Err(err).Send()
} }
} }
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
}
+1 -1
View File
@@ -8,7 +8,7 @@ func notify(c *cli.Context) error {
return cli.Exit("Command notify requires two arguments", 1) return cli.Exit("Command notify requires two arguments", 1)
} }
err := client.Notify(c.Args().Get(0), c.Args().Get(1)) err := client.Notify(c.Context, c.Args().Get(0), c.Args().Get(1))
if err != nil { if err != nil {
return err return err
} }
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"context"
"path/filepath"
"github.com/cheggaaa/pb/v3"
"github.com/urfave/cli/v2"
"go.elara.ws/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.FS().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
}
+2 -2
View File
@@ -13,12 +13,12 @@ func setTime(c *cli.Context) error {
} }
if c.Args().Get(0) == "now" { if c.Args().Get(0) == "now" {
return client.SetTimeNow() return client.SetTime(c.Context, time.Now())
} else { } else {
parsed, err := time.Parse(time.RFC3339, c.Args().Get(0)) parsed, err := time.Parse(time.RFC3339, c.Args().Get(0))
if err != nil { if err != nil {
return err return err
} }
return client.SetTime(parsed) return client.SetTime(c.Context, parsed)
} }
} }
+1 -1
View File
@@ -3,5 +3,5 @@ package main
import "github.com/urfave/cli/v2" import "github.com/urfave/cli/v2"
func updateWeather(c *cli.Context) error { func updateWeather(c *cli.Context) error {
return client.UpdateWeather() return client.WeatherUpdate(c.Context)
} }
+108
View File
@@ -0,0 +1,108 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/urfave/cli/v2"
)
func watchHeart(c *cli.Context) error {
heartCh, err := client.WatchHeartRate(c.Context)
if err != nil {
return err
}
for {
select {
case heartRate := <-heartCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint8{"heartRate": heartRate},
)
} else if c.Bool("shell") {
fmt.Printf("HEART_RATE=%d\n", heartRate)
} else {
fmt.Println(heartRate, "BPM")
}
case <-c.Done():
return nil
}
}
}
func watchBattLevel(c *cli.Context) error {
battLevelCh, err := client.WatchBatteryLevel(c.Context)
if err != nil {
return err
}
for {
select {
case battLevel := <-battLevelCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint8{"battLevel": battLevel},
)
} else if c.Bool("shell") {
fmt.Printf("BATTERY_LEVEL=%d\n", battLevel)
} else {
fmt.Printf("%d%%\n", battLevel)
}
case <-c.Done():
return nil
}
}
}
func watchStepCount(c *cli.Context) error {
stepCountCh, err := client.WatchStepCount(c.Context)
if err != nil {
return err
}
for {
select {
case stepCount := <-stepCountCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(
map[string]uint32{"stepCount": stepCount},
)
} else if c.Bool("shell") {
fmt.Printf("STEP_COUNT=%d\n", stepCount)
} else {
fmt.Println(stepCount, "Steps")
}
case <-c.Done():
return nil
}
}
}
func watchMotion(c *cli.Context) error {
motionCh, err := client.WatchMotion(c.Context)
if err != nil {
return err
}
for {
select {
case motionVals := <-motionCh:
if c.Bool("json") {
json.NewEncoder(os.Stdout).Encode(motionVals)
} else if c.Bool("shell") {
fmt.Printf(
"X=%d\nY=%d\nZ=%d\n",
motionVals.X,
motionVals.Y,
motionVals.Z,
)
} else {
fmt.Println(motionVals)
}
case <-c.Done():
return nil
}
}
}
+7 -3
View File
@@ -28,10 +28,15 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
) )
if err != nil { if err != nil {
// Create new label containing error text // Create new label containing error text
errLbl := widget.NewLabel(err.Error()) errEntry := widget.NewEntry()
errEntry.SetText(err.Error())
// If text changed, change it back
errEntry.OnChanged = func(string) {
errEntry.SetText(err.Error())
}
// Create new dropdown containing error label // Create new dropdown containing error label
content.Add(widget.NewAccordion( content.Add(widget.NewAccordion(
widget.NewAccordionItem("More Details", errLbl), widget.NewAccordionItem("More Details", errEntry),
)) ))
} }
if fatal { if fatal {
@@ -49,5 +54,4 @@ func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
// Show error dialog // Show error dialog
dialog.NewCustom("Error", "Ok", content, parent).Show() dialog.NewCustom("Error", "Ok", content, parent).Show()
} }
} }
+163
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.elara.ws/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(),
)
}
+402
View File
@@ -0,0 +1,402 @@
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.elara.ws/infinitime"
"go.elara.ws/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.FS().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.FS().LoadResources(ctx, resPath)
if err != nil {
guiErr(err, "Error loading resources", false, w)
return
}
for evt := range progCh {
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.FS().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.FS().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.FS().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.FS().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.FS().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.FS().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
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.elara.ws/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
}
+54 -91
View File
@@ -1,123 +1,86 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"image/color"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme" "go.elara.ws/itd/api"
"go.arsenm.dev/itd/api"
) )
func infoTab(parent fyne.Window, client *api.Client) *fyne.Container { func infoTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
infoLayout := container.NewVBox( c := container.NewVBox()
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
)
// Create label for heart rate // Create titled text for heart rate
heartRateLbl := newText("0 BPM", 24) heartRateText := newTitledText("Heart Rate", "0 BPM")
// Creae container to store heart rate section c.Add(heartRateText)
heartRateSect := container.NewVBox( // Watch heart rate
newText("Heart Rate", 12), heartRateCh, err := client.WatchHeartRate(ctx)
heartRateLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(heartRateSect)
heartRateCh, cancel, err := client.WatchHeartRate()
if err != nil { if err != nil {
guiErr(err, "Error getting heart rate channel", true, parent) guiErr(err, "Error watching heart rate", true, w)
} }
onClose = append(onClose, cancel)
go func() { go func() {
// For every heart rate sample
for heartRate := range heartRateCh { for heartRate := range heartRateCh {
// Change text of heart rate label // Set body of titled text
heartRateLbl.Text = fmt.Sprintf("%d BPM", heartRate) heartRateText.SetBody(fmt.Sprintf("%d BPM", heartRate))
// Refresh label
heartRateLbl.Refresh()
} }
}() }()
// Create label for heart rate // Create titled text for battery level
stepCountLbl := newText("0 Steps", 24) battLevelText := newTitledText("Battery Level", "0%")
// Creae container to store heart rate section c.Add(battLevelText)
stepCountSect := container.NewVBox( // Watch battery level
newText("Step Count", 12), battLevelCh, err := client.WatchBatteryLevel(ctx)
stepCountLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(stepCountSect)
stepCountCh, cancel, err := client.WatchStepCount()
if err != nil { if err != nil {
guiErr(err, "Error getting step count channel", true, parent) guiErr(err, "Error watching battery level", true, w)
} }
onClose = append(onClose, cancel)
go func() {
for stepCount := range stepCountCh {
// Change text of heart rate label
stepCountLbl.Text = fmt.Sprintf("%d Steps", stepCount)
// Refresh label
stepCountLbl.Refresh()
}
}()
// Create label for battery level
battLevelLbl := newText("0%", 24)
// Create container to store battery level section
battLevel := container.NewVBox(
newText("Battery Level", 12),
battLevelLbl,
canvas.NewLine(theme.ShadowColor()),
)
infoLayout.Add(battLevel)
battLevelCh, cancel, err := client.WatchBatteryLevel()
if err != nil {
guiErr(err, "Error getting battery level channel", true, parent)
}
onClose = append(onClose, cancel)
go func() { go func() {
// For every battery level sample
for battLevel := range battLevelCh { for battLevel := range battLevelCh {
// Change text of battery level label // Set body of titled text
battLevelLbl.Text = fmt.Sprintf("%d%%", battLevel) battLevelText.SetBody(fmt.Sprintf("%d%%", battLevel))
// Refresh label
battLevelLbl.Refresh()
} }
}() }()
fwVerString, err := client.Version() // Create titled text for step count
stepCountText := newTitledText("Step Count", "0 Steps")
c.Add(stepCountText)
// Watch step count
stepCountCh, err := client.WatchStepCount(ctx)
if err != nil { if err != nil {
guiErr(err, "Error getting firmware string", true, parent) guiErr(err, "Error watching step count", true, w)
} }
go func() {
// For every step count sample
for stepCount := range stepCountCh {
// Set body of titled text
stepCountText.SetBody(fmt.Sprintf("%d Steps", stepCount))
}
}()
fwVer := container.NewVBox( // Create new titled text for address
newText("Firmware Version", 12), addressText := newTitledText("Address", "")
newText(fwVerString, 24), c.Add(addressText)
canvas.NewLine(theme.ShadowColor()), // Get address
) address, err := client.Address(ctx)
infoLayout.Add(fwVer)
btAddrString, err := client.Address()
if err != nil { if err != nil {
panic(err) guiErr(err, "Error getting address", true, w)
} }
// Set body of titled text
addressText.SetBody(address)
btAddr := container.NewVBox( // Create new titled text for version
newText("Bluetooth Address", 12), versionText := newTitledText("Version", "")
newText(btAddrString, 24), c.Add(versionText)
canvas.NewLine(theme.ShadowColor()), // Get version
) version, err := client.Version(ctx)
infoLayout.Add(btAddr) if err != nil {
guiErr(err, "Error getting version", true, w)
}
// Set body of titled text
versionText.SetBody(version)
return infoLayout return container.NewVScroll(c)
}
func newText(t string, size float32) *canvas.Text {
text := canvas.NewText(t, theme.ForegroundColor())
text.TextSize = size
return text
} }
+21
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(),
)
}
+42 -25
View File
@@ -1,43 +1,60 @@
package main package main
import ( import (
"context"
"sync"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"go.arsenm.dev/itd/api" "go.elara.ws/itd/api"
) )
var onClose []func()
func main() { func main() {
// Create new app
a := app.New() a := app.New()
// Create new window with title "itgui" w := a.NewWindow("itgui")
window := a.NewWindow("itgui")
window.SetOnClosed(func() {
for _, closeFn := range onClose {
closeFn()
}
})
// Create new context for use with the API client
ctx, cancel := context.WithCancel(context.Background())
// Connect to ITD API
client, err := api.New(api.DefaultAddr) client, err := api.New(api.DefaultAddr)
if err != nil { if err != nil {
guiErr(err, "Error connecting to itd", true, window) guiErr(err, "Error connecting to ITD", true, w)
} }
onClose = append(onClose, func() {
client.Close()
})
// Create new app tabs container // Create channel to signal that the fs tab has been opened
fsOpened := make(chan struct{})
fsOnce := &sync.Once{}
// Create app tabs
tabs := container.NewAppTabs( tabs := container.NewAppTabs(
container.NewTabItem("Info", infoTab(window, client)), container.NewTabItem("Info", infoTab(ctx, client, w)),
container.NewTabItem("Motion", motionTab(window, client)), container.NewTabItem("Motion", motionTab(ctx, client, w)),
container.NewTabItem("Notify", notifyTab(window, client)), container.NewTabItem("Notify", notifyTab(ctx, client, w)),
container.NewTabItem("Set Time", timeTab(window, client)), container.NewTabItem("FS", fsTab(ctx, client, w, fsOpened)),
container.NewTabItem("Upgrade", upgradeTab(window, client)), container.NewTabItem("Time", timeTab(ctx, client, w)),
container.NewTabItem("Firmware", firmwareTab(ctx, client, w)),
) )
// Set tabs as window content metricsTab := graphTab(ctx, client, w)
window.SetContent(tabs) if metricsTab != nil {
// Show window and run app tabs.Append(container.NewTabItem("Metrics", metricsTab))
window.ShowAndRun() }
// When a tab is selected
tabs.OnSelected = func(ti *container.TabItem) {
// If the tab's name is FS
if ti.Text == "FS" {
// Signal fsOpened only once
fsOnce.Do(func() {
fsOpened <- struct{}{}
})
}
}
// Cancel context on close
w.SetOnClosed(cancel)
// Set content and show window
w.SetContent(tabs)
w.ShowAndRun()
} }
+43 -86
View File
@@ -1,105 +1,62 @@
package main package main
import ( import (
"image/color" "context"
"strconv" "fmt"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api" "go.elara.ws/itd/api"
) )
func motionTab(parent fyne.Window, client *api.Client) *fyne.Container { func motionTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
// Create label for heart rate // Create titledText for each coordinate
xCoordLbl := newText("0", 24) xText := newTitledText("X Coordinate", "0")
// Creae container to store heart rate section yText := newTitledText("Y Coordinate", "0")
xCoordSect := container.NewVBox( zText := newTitledText("Z Coordinate", "0")
newText("X Coordinate", 12),
xCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create label for heart rate var ctxCancel func()
yCoordLbl := newText("0", 24)
// Creae container to store heart rate section
yCoordSect := container.NewVBox(
newText("Y Coordinate", 12),
yCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create label for heart rate
zCoordLbl := newText("0", 24)
// Creae container to store heart rate section
zCoordSect := container.NewVBox(
newText("Z Coordinate", 12),
zCoordLbl,
canvas.NewLine(theme.ShadowColor()),
)
// Create variable to keep track of whether motion started // Create start button
started := false toggleBtn := widget.NewButton("Start", nil)
// Set button's on tapped callback
// Create button to stop motion toggleBtn.OnTapped = func() {
stopBtn := widget.NewButton("Stop", nil) switch toggleBtn.Text {
// Create button to start motion case "Start":
startBtn := widget.NewButton("Start", func() { // Create new context for motion
// if motion is started motionCtx, cancel := context.WithCancel(ctx)
if started { // Set ctxCancel to function so that stop button can run it
// Do nothing ctxCancel = cancel
return // Watch motion
} motionCh, err := client.WatchMotion(motionCtx)
// Set motion started
started = true
// Watch motion values
motionCh, cancel, err := client.WatchMotion()
if err != nil { if err != nil {
guiErr(err, "Error getting heart rate channel", true, parent) guiErr(err, "Error watching motion", false, w)
}
// Create done channel
done := make(chan struct{}, 1)
go func() {
for {
select {
case <-done:
return return
case motion := <-motionCh:
// Set labels to new values
xCoordLbl.Text = strconv.Itoa(int(motion.X))
yCoordLbl.Text = strconv.Itoa(int(motion.Y))
zCoordLbl.Text = strconv.Itoa(int(motion.Z))
// Refresh labels to display new values
xCoordLbl.Refresh()
yCoordLbl.Refresh()
zCoordLbl.Refresh()
} }
go func() {
// For every motion event
for motion := range motionCh {
// Set coordinates
xText.SetBody(fmt.Sprint(motion.X))
yText.SetBody(fmt.Sprint(motion.Y))
zText.SetBody(fmt.Sprint(motion.Z))
} }
}() }()
// Create stop function // Set button text to "Stop"
stopBtn.OnTapped = func() { toggleBtn.SetText("Stop")
done <- struct{}{} case "Stop":
started = false // Cancel motion context
cancel() ctxCancel()
// Set button text to "Start"
toggleBtn.SetText("Start")
}
} }
}) return container.NewVScroll(container.NewVBox(
// Run stop button function on close if possible toggleBtn,
onClose = append(onClose, func() { xText,
if stopBtn.OnTapped != nil { yText,
stopBtn.OnTapped() zText,
} ))
})
// Return new container containing all elements
return container.NewVBox(
// Add rectangle for a bit of padding
canvas.NewRectangle(color.Transparent),
startBtn,
stopBtn,
xCoordSect,
yCoordSect,
zCoordSect,
)
} }
+18 -15
View File
@@ -1,37 +1,40 @@
package main package main
import ( import (
"context"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api" "go.elara.ws/itd/api"
) )
func notifyTab(parent fyne.Window, client *api.Client) *fyne.Container { func notifyTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
// Create new entry for notification title c := container.NewVBox()
c.Add(layout.NewSpacer())
// Create new entry for title
titleEntry := widget.NewEntry() titleEntry := widget.NewEntry()
titleEntry.SetPlaceHolder("Title") titleEntry.SetPlaceHolder("Title")
c.Add(titleEntry)
// Create multiline entry for notification body // Create new multiline entry for body
bodyEntry := widget.NewMultiLineEntry() bodyEntry := widget.NewMultiLineEntry()
bodyEntry.SetPlaceHolder("Body") bodyEntry.SetPlaceHolder("Body")
c.Add(bodyEntry)
// Create new button to send notification // Create new send button
sendBtn := widget.NewButton("Send", func() { sendBtn := widget.NewButton("Send", func() {
err := client.Notify(titleEntry.Text, bodyEntry.Text) // Send notification
err := client.Notify(ctx, titleEntry.Text, bodyEntry.Text)
if err != nil { if err != nil {
guiErr(err, "Error sending notification", false, parent) guiErr(err, "Error sending notification", false, w)
return return
} }
}) })
c.Add(sendBtn)
// Return new container containing all elements c.Add(layout.NewSpacer())
return container.NewVBox( return container.NewVScroll(c)
layout.NewSpacer(),
titleEntry,
bodyEntry,
sendBtn,
layout.NewSpacer(),
)
} }
+64
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: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+34 -37
View File
@@ -1,60 +1,57 @@
package main package main
import ( import (
"context"
"time" "time"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api" "go.elara.ws/itd/api"
) )
func timeTab(parent fyne.Window, client *api.Client) *fyne.Container { func timeTab(ctx context.Context, client *api.Client, w fyne.Window) fyne.CanvasObject {
// Create new entry for time string c := container.NewVBox()
c.Add(layout.NewSpacer())
// Create entry for time string
timeEntry := widget.NewEntry() timeEntry := widget.NewEntry()
// Set text to current time formatter properly
timeEntry.SetText(time.Now().Format(time.RFC1123)) timeEntry.SetText(time.Now().Format(time.RFC1123))
timeEntry.SetPlaceHolder("RFC1123")
// Create button to set current time // Create button to set current time
currentBtn := widget.NewButton("Set Current", func() { setCurrentBtn := widget.NewButton("Set current time", func() {
timeEntry.SetText(time.Now().Format(time.RFC1123)) // Set current time
setTime(client, true) err := client.SetTime(ctx, time.Now())
})
// Create button to set time inside entry
timeBtn := widget.NewButton("Set", func() {
// Parse time as RFC1123 string
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
if err != nil { if err != nil {
guiErr(err, "Error parsing time string", false, parent) guiErr(err, "Error setting time", false, w)
return return
} }
// Set time to parsed time // Set time entry to current time
setTime(client, false, parsedTime) timeEntry.SetText(time.Now().Format(time.RFC1123))
}) })
// Return new container with all elements centered // Create button to set time from entry
return container.NewVBox( setBtn := widget.NewButton("Set", func() {
layout.NewSpacer(), // Parse RFC1123 time string in entry
timeEntry, newTime, err := time.Parse(time.RFC1123, timeEntry.Text)
currentBtn,
timeBtn,
layout.NewSpacer(),
)
}
// setTime sets the first element in the variadic parameter
// if current is false, otherwise, it sets the current time.
func setTime(client *api.Client, current bool, t ...time.Time) error {
var err error
if current {
err = client.SetTimeNow()
} else {
err = client.SetTime(t[0])
}
if err != nil { if err != nil {
return err guiErr(err, "Error parsing time string", false, w)
return
} }
return nil // Set time from parsed string
err = client.SetTime(ctx, newTime)
if err != nil {
guiErr(err, "Error setting time", false, w)
return
}
})
c.Add(timeEntry)
c.Add(setBtn)
c.Add(setCurrentBtn)
c.Add(layout.NewSpacer())
return c
} }
+35
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()
}
-181
View File
@@ -1,181 +0,0 @@
package main
import (
"fmt"
"path/filepath"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"go.arsenm.dev/itd/api"
"go.arsenm.dev/itd/internal/types"
)
func upgradeTab(parent fyne.Window, client *api.Client) *fyne.Container {
var (
archivePath string
firmwarePath string
initPktPath string
)
var archiveBtn *widget.Button
// Create archive selection dialog
archiveDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
if e != nil || uc == nil {
return
}
uc.Close()
archivePath = uc.URI().Path()
archiveBtn.SetText(fmt.Sprintf("Select archive (.zip) [%s]", filepath.Base(archivePath)))
}, parent)
// Limit dialog to .zip files
archiveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".zip"}))
// Create button to show dialog
archiveBtn = widget.NewButton("Select archive (.zip)", archiveDialog.Show)
var firmwareBtn *widget.Button
// Create firmware selection dialog
firmwareDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
if e != nil || uc == nil {
return
}
uc.Close()
firmwarePath = uc.URI().Path()
firmwareBtn.SetText(fmt.Sprintf("Select firmware (.bin) [%s]", filepath.Base(firmwarePath)))
}, parent)
// Limit dialog to .bin files
firmwareDialog.SetFilter(storage.NewExtensionFileFilter([]string{".bin"}))
// Create button to show dialog
firmwareBtn = widget.NewButton("Select firmware (.bin)", firmwareDialog.Show)
var initPktBtn *widget.Button
// Create init packet selection dialog
initPktDialog := dialog.NewFileOpen(func(uc fyne.URIReadCloser, e error) {
if e != nil || uc == nil {
return
}
uc.Close()
initPktPath = uc.URI().Path()
initPktBtn.SetText(fmt.Sprintf("Select init packet (.dat) [%s]", filepath.Base(initPktPath)))
}, parent)
// Limit dialog to .dat files
initPktDialog.SetFilter(storage.NewExtensionFileFilter([]string{".dat"}))
// Create button to show dialog
initPktBtn = widget.NewButton("Select init packet (.dat)", initPktDialog.Show)
// Hide init packet and firmware buttons
initPktBtn.Hide()
firmwareBtn.Hide()
// Create dropdown to select upgrade type
upgradeTypeSelect := widget.NewSelect([]string{
"Archive",
"Files",
}, func(s string) {
// Hide all buttons
archiveBtn.Hide()
initPktBtn.Hide()
firmwareBtn.Hide()
// Unhide appropriate button(s)
switch s {
case "Archive":
archiveBtn.Show()
case "Files":
initPktBtn.Show()
firmwareBtn.Show()
}
})
// Select first elemetn
upgradeTypeSelect.SetSelectedIndex(0)
// Create new button to start DFU
startBtn := widget.NewButton("Start", func() {
// If archive path does not exist and both init packet and firmware paths
// also do not exist, return error
if archivePath == "" && (initPktPath == "" && firmwarePath == "") {
guiErr(nil, "Upgrade requires archive or files selected", false, parent)
return
}
// Create new label for byte progress
progressLbl := widget.NewLabelWithStyle("0 / 0 B", fyne.TextAlignCenter, fyne.TextStyle{})
// Create new progress bar
progressBar := widget.NewProgressBar()
// Create modal dialog containing label and progress bar
progressDlg := widget.NewModalPopUp(container.NewVBox(
layout.NewSpacer(),
progressLbl,
progressBar,
layout.NewSpacer(),
), parent.Canvas())
// Resize modal to 300x100
progressDlg.Resize(fyne.NewSize(300, 100))
var fwUpgType api.UpgradeType
var files []string
// Get appropriate upgrade type and file paths
switch upgradeTypeSelect.Selected {
case "Archive":
fwUpgType = types.UpgradeTypeArchive
files = append(files, archivePath)
case "Files":
fwUpgType = types.UpgradeTypeFiles
files = append(files, initPktPath, firmwarePath)
}
progress, err := client.FirmwareUpgrade(fwUpgType, files...)
if err != nil {
guiErr(err, "Error initiating DFU", false, parent)
return
}
// Show progress dialog
progressDlg.Show()
for event := range progress {
// Set label text to received / total B
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
// Set progress bar values
progressBar.Max = float64(event.Total)
progressBar.Value = float64(event.Received)
// Refresh progress bar
progressBar.Refresh()
// If transfer finished, break
if event.Sent == event.Total {
break
}
}
// 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(),
)
}
+49 -15
View File
@@ -9,32 +9,53 @@ import (
"github.com/knadh/koanf/providers/confmap" "github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/file"
"github.com/rs/zerolog" "go.elara.ws/logger"
"github.com/rs/zerolog/log" "go.elara.ws/logger/log"
) )
var cfgDir string
func init() { func init() {
etcPath := "/etc/itd.toml"
// Set up logger // Set up logger
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = logger.NewPretty(os.Stderr)
// Get user's configuration directory // Get user's configuration directory
cfgDir, err := os.UserConfigDir() userCfgDir, err := os.UserConfigDir()
if err != nil { if err != nil {
panic(err) panic(err)
} }
cfgDir = filepath.Join(userCfgDir, "itd")
// If config dir is not readable
if _, err = os.ReadDir(cfgDir); err != nil {
// Create config dir with 700 permissions
err = os.MkdirAll(cfgDir, 0o700)
if err != nil {
panic(err)
}
}
// Get current and old config paths
cfgPath := filepath.Join(cfgDir, "itd.toml")
oldCfgPath := filepath.Join(userCfgDir, "itd.toml")
// If old config path exists
if _, err = os.Stat(oldCfgPath); err == nil {
// Move old config to new path
err = os.Rename(oldCfgPath, cfgPath)
if err != nil {
panic(err)
}
}
// Set config defaults // Set config defaults
setCfgDefaults() setCfgDefaults()
// Load config files // Load and watch config files
etcProvider := file.Provider("/etc/itd.toml") loadAndwatchCfgFile(etcPath)
cfgProvider := file.Provider(filepath.Join(cfgDir, "itd.toml")) loadAndwatchCfgFile(cfgPath)
k.Load(etcProvider, toml.Parser())
k.Load(cfgProvider, toml.Parser())
// Watch configs for changes
cfgWatch(etcProvider)
cfgWatch(cfgProvider)
// Load envireonment variables // Load envireonment variables
k.Load(env.Provider("ITD_", "_", func(s string) string { k.Load(env.Provider("ITD_", "_", func(s string) string {
@@ -42,19 +63,29 @@ func init() {
}), nil) }), nil)
} }
func cfgWatch(provider *file.File) { func loadAndwatchCfgFile(filename string) {
provider := file.Provider(filename)
if cfgError := k.Load(provider, toml.Parser()); cfgError != nil {
log.Warn("Error while trying to read config file").Str("filename", filename).Err(cfgError).Send()
}
// Watch for changes and reload when detected // Watch for changes and reload when detected
provider.Watch(func(_ interface{}, err error) { provider.Watch(func(_ interface{}, err error) {
if err != nil { if err != nil {
return return
} }
k.Load(provider, toml.Parser()) if cfgError := k.Load(provider, toml.Parser()); cfgError != nil {
log.Warn("Error while trying to read config file").Str("filename", filename).Err(cfgError).Send()
}
}) })
} }
func setCfgDefaults() { func setCfgDefaults() {
k.Load(confmap.Provider(map[string]interface{}{ k.Load(confmap.Provider(map[string]interface{}{
"bluetooth.adapter": "hci0",
"socket.path": "/tmp/itd/socket", "socket.path": "/tmp/itd/socket",
"conn.reconnect": true, "conn.reconnect": true,
@@ -75,5 +106,8 @@ func setCfgDefaults() {
"notifs.ignore.body": []string{}, "notifs.ignore.body": []string{},
"music.vol.interval": 5, "music.vol.interval": 5,
"fuse.enabled": false,
"fuse.mountpoint": "/tmp/itd/mnt",
}, "."), nil) }, "."), nil)
} }
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"context"
"os"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"go.elara.ws/infinitime"
"go.elara.ws/itd/internal/fusefs"
"go.elara.ws/logger/log"
)
func startFUSE(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// This is where we'll mount the FS
err := os.MkdirAll(k.String("fuse.mountpoint"), 0o755)
if err != nil && !os.IsExist(err) {
return err
}
// Ignore the error because nothing might be mounted on the mountpoint
_ = fusefs.Unmount(k.String("fuse.mountpoint"))
root, err := fusefs.BuildRootNode(dev)
if err != nil {
log.Error("Building root node failed").
Err(err).
Send()
return err
}
server, err := fs.Mount(k.String("fuse.mountpoint"), root, &fs.Options{
MountOptions: fuse.MountOptions{
// Set to true to see how the file system works.
Debug: false,
SingleThreaded: true,
},
})
if err != nil {
log.Error("Mounting failed").
Str("target", k.String("fuse.mountpoint")).
Err(err).
Send()
return err
}
log.Info("Mounted on target").
Str("target", k.String("fuse.mountpoint")).
Send()
fusefs.BuildProperties(dev)
if err != nil {
log.Warn("Error getting BLE filesystem").Err(err).Send()
return err
}
wg.Add(1)
go func() {
defer wg.Done("fuse")
<-ctx.Done()
server.Unmount()
}()
return nil
}
+85 -32
View File
@@ -1,37 +1,90 @@
module go.arsenm.dev/itd module go.elara.ws/itd
go 1.16 go 1.18
replace fyne.io/x/fyne => github.com/metal3d/fyne-x v0.0.0-20220508095732-177117e583fb
require ( require (
fyne.io/fyne/v2 v2.1.2 fyne.io/fyne/v2 v2.3.0
github.com/VividCortex/ewma v1.2.0 // indirect fyne.io/x/fyne v0.0.0-20220107050838-c4a1de51d4ce
github.com/cheggaaa/pb/v3 v3.0.8 github.com/cheggaaa/pb/v3 v3.1.0
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d
github.com/fatih/color v1.13.0 // indirect github.com/godbus/dbus/v5 v5.1.0
github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/hanwen/go-fuse/v2 v2.2.0
github.com/gen2brain/dlgs v0.0.0-20211108104213-bade24837f0b github.com/knadh/koanf v1.4.4
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/mattn/go-isatty v0.0.17
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211204153444-caad923f49f4 // indirect
github.com/godbus/dbus/v5 v5.0.6
github.com/google/uuid v1.3.0
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/knadh/koanf v1.4.0
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.3
github.com/mozillazg/go-pinyin v0.19.0 github.com/mozillazg/go-pinyin v0.19.0
github.com/pelletier/go-toml v1.9.4 // indirect github.com/urfave/cli/v2 v2.23.7
github.com/rs/zerolog v1.26.0 go.elara.ws/drpc v0.0.0-20230421021209-fe4c05460a3d
github.com/sirupsen/logrus v1.8.1 // indirect go.elara.ws/infinitime v0.0.0-20230421025334-f2640203e9e9
github.com/srwiley/oksvg v0.0.0-20211120171407-1837d6608d8c // indirect go.elara.ws/logger v0.0.0-20230421022458-e80700db2090
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 // indirect golang.org/x/text v0.5.0
github.com/urfave/cli/v2 v2.3.0 google.golang.org/protobuf v1.28.1
github.com/yuin/goldmark v1.4.4 // indirect modernc.org/sqlite v1.20.1
go.arsenm.dev/infinitime v0.0.0-20220416112421-b7a50271bece storj.io/drpc v0.0.32
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect )
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/text v0.3.7 require (
gopkg.in/yaml.v2 v2.4.0 // indirect fyne.io/systray v1.10.1-0.20221115204952-d16a6177e6f1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect github.com/VividCortex/ewma v1.2.0 // indirect
github.com/benoitkugler/textlayout v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fredbi/uri v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/fyne-io/gl-js v0.0.0-20220802150000-8e339395f381 // indirect
github.com/fyne-io/glfw-js v0.0.0-20220517201726-bebc2019cd33 // indirect
github.com/fyne-io/image v0.0.0-20221020213044-f609c6a24345 // indirect
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/go-text/typesetting v0.0.0-20221219135543-5d0d724ee181 // indirect
github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.1 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/hashicorp/yamux v0.1.1 // 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.13 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muka/go-bluetooth v0.0.0-20220819140550-1d8857e3b268 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.8.1 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yuin/goldmark v1.5.3 // indirect
github.com/zeebo/errs v1.3.0 // indirect
golang.org/x/image v0.2.0 // indirect
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/js/dom v0.0.0-20221001195520-26252dedbe70 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
) )
+760 -54
View File
File diff suppressed because it is too large Load Diff
+607
View File
@@ -0,0 +1,607 @@
package fusefs
import (
"bytes"
"context"
"io"
"strconv"
"syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"go.elara.ws/infinitime"
"go.elara.ws/infinitime/blefs"
"go.elara.ws/logger/log"
)
type ITProperty struct {
name string
Ino uint64
gen func() ([]byte, error)
}
type DirEntry struct {
isDir bool
modtime uint64
size uint32
path string
}
type ITNode struct {
fs.Inode
kind nodeKind
Ino uint64
lst []DirEntry
self DirEntry
path string
}
type nodeKind uint8
const (
nodeKindRoot = iota
nodeKindInfo
nodeKindFS
nodeKindReadOnly
)
var (
myfs *blefs.FS = nil
inodemap map[string]uint64 = nil
)
func BuildRootNode(dev *infinitime.Device) (*ITNode, error) {
var err error
inodemap = make(map[string]uint64)
myfs, err = dev.FS()
if err != nil {
log.Error("FUSE Failed to get filesystem").Err(err).Send()
return nil, err
}
return &ITNode{kind: nodeKindRoot}, nil
}
var properties = make([]ITProperty, 6)
func BuildProperties(dev *infinitime.Device) {
properties[0] = ITProperty{
"heartrate", 2,
func() ([]byte, error) {
ans, err := dev.HeartRate()
return []byte(strconv.Itoa(int(ans)) + "\n"), err
},
}
properties[1] = ITProperty{
"battery", 3,
func() ([]byte, error) {
ans, err := dev.BatteryLevel()
return []byte(strconv.Itoa(int(ans)) + "\n"), err
},
}
properties[2] = ITProperty{
"motion", 4,
func() ([]byte, error) {
ans, err := dev.Motion()
return []byte(strconv.Itoa(int(ans.X)) + " " + strconv.Itoa(int(ans.Y)) + " " + strconv.Itoa(int(ans.Z)) + "\n"), err
},
}
properties[3] = ITProperty{
"stepcount", 6,
func() ([]byte, error) {
ans, err := dev.StepCount()
return []byte(strconv.Itoa(int(ans)) + "\n"), err
},
}
properties[4] = ITProperty{
"version", 7,
func() ([]byte, error) {
ans, err := dev.Version()
return []byte(ans + "\n"), err
},
}
properties[5] = ITProperty{
"address", 8,
func() ([]byte, error) {
ans := dev.Address()
return []byte(ans + "\n"), nil
},
}
}
var _ fs.NodeReaddirer = (*ITNode)(nil)
// Readdir is part of the NodeReaddirer interface
func (n *ITNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
switch n.kind {
case 0:
// root folder
r := make([]fuse.DirEntry, 2)
r[0] = fuse.DirEntry{
Name: "info",
Ino: 0,
Mode: fuse.S_IFDIR,
}
r[1] = fuse.DirEntry{
Name: "fs",
Ino: 1,
Mode: fuse.S_IFDIR,
}
return fs.NewListDirStream(r), 0
case 1:
// info folder
r := make([]fuse.DirEntry, 6)
for ind, value := range properties {
r[ind] = fuse.DirEntry{
Name: value.name,
Ino: value.Ino,
Mode: fuse.S_IFREG,
}
}
return fs.NewListDirStream(r), 0
case 2:
// on info
files, err := myfs.ReadDir(n.path)
if err != nil {
log.Error("FUSE ReadDir failed").Str("path", n.path).Err(err).Send()
return nil, syscallErr(err)
}
log.Debug("FUSE ReadDir succeeded").Str("path", n.path).Int("objects", len(files)).Send()
r := make([]fuse.DirEntry, len(files))
n.lst = make([]DirEntry, len(files))
for ind, entry := range files {
info, err := entry.Info()
if err != nil {
log.Error("FUSE Info failed").Str("path", n.path).Err(err).Send()
return nil, syscallErr(err)
}
name := info.Name()
file := DirEntry{
path: n.path + "/" + name,
size: uint32(info.Size()),
modtime: uint64(info.ModTime().Unix()),
isDir: info.IsDir(),
}
n.lst[ind] = file
ino := inodemap[file.path]
if ino == 0 {
ino = uint64(len(inodemap)) + 1
inodemap[file.path] = ino
}
if file.isDir {
r[ind] = fuse.DirEntry{
Name: name,
Mode: fuse.S_IFDIR,
Ino: ino + 10,
}
} else {
r[ind] = fuse.DirEntry{
Name: name,
Mode: fuse.S_IFREG,
Ino: ino + 10,
}
}
}
return fs.NewListDirStream(r), 0
}
r := make([]fuse.DirEntry, 0)
return fs.NewListDirStream(r), 0
}
var _ fs.NodeLookuper = (*ITNode)(nil)
func (n *ITNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
switch n.kind {
case 0:
// root folder
if name == "info" {
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: uint64(0),
}
operations := &ITNode{kind: nodeKindInfo, Ino: 0}
child := n.NewInode(ctx, operations, stable)
return child, 0
} else if name == "fs" {
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: uint64(1),
}
operations := &ITNode{kind: nodeKindFS, Ino: 1, path: ""}
child := n.NewInode(ctx, operations, stable)
return child, 0
}
case 1:
// info folder
for _, value := range properties {
if value.name == name {
stable := fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: uint64(value.Ino),
}
operations := &ITNode{kind: nodeKindReadOnly, Ino: value.Ino}
child := n.NewInode(ctx, operations, stable)
return child, 0
}
}
case 2:
// FS object
if len(n.lst) == 0 {
n.Readdir(ctx)
}
for _, file := range n.lst {
if file.path != n.path+"/"+name {
continue
}
log.Debug("FUSE Lookup successful").Str("path", file.path).Send()
if file.isDir {
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: inodemap[file.path],
}
operations := &ITNode{kind: nodeKindFS, path: file.path}
child := n.NewInode(ctx, operations, stable)
return child, 0
} else {
stable := fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: inodemap[file.path],
}
operations := &ITNode{
kind: nodeKindFS, path: file.path,
self: file,
}
child := n.NewInode(ctx, operations, stable)
return child, 0
}
}
log.Warn("FUSE Lookup failed").Str("path", n.path+"/"+name).Send()
}
return nil, syscall.ENOENT
}
type bytesFileReadHandle struct {
content []byte
}
var _ fs.FileReader = (*bytesFileReadHandle)(nil)
func (fh *bytesFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
log.Debug("FUSE Executing Read").Int("size", len(fh.content)).Send()
end := off + int64(len(dest))
if end > int64(len(fh.content)) {
end = int64(len(fh.content))
}
return fuse.ReadResultData(fh.content[off:end]), 0
}
type sensorFileReadHandle struct {
content []byte
}
var _ fs.FileReader = (*sensorFileReadHandle)(nil)
func (fh *sensorFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
log.Debug("FUSE Executing Read").Int("size", len(fh.content)).Send()
end := off + int64(len(dest))
if end > int64(len(fh.content)) {
end = int64(len(fh.content))
}
return fuse.ReadResultData(fh.content[off:end]), 0
}
var _ fs.FileFlusher = (*sensorFileReadHandle)(nil)
func (fh *sensorFileReadHandle) Flush(ctx context.Context) (errno syscall.Errno) {
return 0
}
type bytesFileWriteHandle struct {
content []byte
path string
}
var _ fs.FileWriter = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Write(ctx context.Context, data []byte, off int64) (written uint32, errno syscall.Errno) {
log.Debug("FUSE Executing Write").Str("path", fh.path).Int("prev_size", len(fh.content)).Int("next_size", len(data)).Send()
if off != int64(len(fh.content)) {
log.Error("FUSE Write file size changed unexpectedly").Int("expect", int(off)).Int("received", len(fh.content)).Send()
return 0, syscall.ENXIO
}
fh.content = append(fh.content[:], data[:]...)
return uint32(len(data)), 0
}
var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno) {
log.Debug("FUSE Attempting flush").Str("path", fh.path).Send()
fp, err := myfs.Create(fh.path, uint32(len(fh.content)))
if err != nil {
log.Error("FUSE Flush failed: create").Str("path", fh.path).Err(err).Send()
return syscallErr(err)
}
if len(fh.content) == 0 {
log.Debug("FUSE Flush no data to write").Str("path", fh.path).Send()
err = fp.Close()
if err != nil {
log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send()
return syscallErr(err)
}
return 0
}
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Debug("FUSE Flush progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send()
}
}()
r := bytes.NewReader(fh.content)
nread, err := io.Copy(fp, r)
if err != nil {
log.Error("FUSE Flush failed during write").Str("path", fh.path).Err(err).Send()
fp.Close()
return syscallErr(err)
}
if int(nread) != len(fh.content) {
log.Error("FUSE Flush failed during write").Str("path", fh.path).Int("expect", len(fh.content)).Int("got", int(nread)).Send()
fp.Close()
return syscall.EIO
}
err = fp.Close()
if err != nil {
log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send()
return syscallErr(err)
}
log.Debug("FUSE Flush done").Str("path", fh.path).Int("size", len(fh.content)).Send()
return 0
}
var _ fs.FileFsyncer = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) {
return fh.Flush(ctx)
}
var _ fs.NodeGetattrer = (*ITNode)(nil)
func (bn *ITNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
log.Debug("FUSE getattr").Str("path", bn.path).Send()
out.Ino = bn.Ino
out.Mtime = bn.self.modtime
out.Ctime = bn.self.modtime
out.Atime = bn.self.modtime
out.Size = uint64(bn.self.size)
return 0
}
var _ fs.NodeSetattrer = (*ITNode)(nil)
func (bn *ITNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
log.Debug("FUSE setattr").Str("path", bn.path).Send()
out.Size = 0
out.Mtime = 0
return 0
}
var _ fs.NodeOpener = (*ITNode)(nil)
func (f *ITNode) Open(ctx context.Context, openFlags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
switch f.kind {
case 2:
// FS file
if openFlags&syscall.O_RDWR != 0 {
log.Error("FUSE Open failed: RDWR").Str("path", f.path).Send()
return nil, 0, syscall.EROFS
}
if openFlags&syscall.O_WRONLY != 0 {
log.Debug("FUSE Opening for write").Str("path", f.path).Send()
fh = &bytesFileWriteHandle{
path: f.path,
content: make([]byte, 0),
}
return fh, fuse.FOPEN_DIRECT_IO, 0
} else {
log.Debug("FUSE Opening for read").Str("path", f.path).Send()
fp, err := myfs.Open(f.path)
if err != nil {
log.Error("FUSE: Opening failed").Str("path", f.path).Err(err).Send()
return nil, 0, syscallErr(err)
}
defer fp.Close()
b := &bytes.Buffer{}
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send()
}
}()
_, err = io.Copy(b, fp)
if err != nil {
log.Error("FUSE Read failed").Str("path", f.path).Err(err).Send()
fp.Close()
return nil, 0, syscallErr(err)
}
fh = &bytesFileReadHandle{
content: b.Bytes(),
}
return fh, fuse.FOPEN_DIRECT_IO, 0
}
case 3:
// Device file
// disallow writes
if openFlags&(syscall.O_RDWR|syscall.O_WRONLY) != 0 {
return nil, 0, syscall.EROFS
}
for _, value := range properties {
if value.Ino == f.Ino {
ans, err := value.gen()
if err != nil {
return nil, 0, syscallErr(err)
}
fh = &sensorFileReadHandle{
content: ans,
}
return fh, fuse.FOPEN_DIRECT_IO, 0
}
}
}
return nil, 0, syscall.EINVAL
}
var _ fs.NodeCreater = (*ITNode)(nil)
func (f *ITNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
if f.kind != 2 {
return nil, nil, 0, syscall.EROFS
}
path := f.path + "/" + name
ino := uint64(len(inodemap)) + 11
inodemap[path] = ino
stable := fs.StableAttr{
Mode: fuse.S_IFREG,
Ino: ino,
}
operations := &ITNode{
kind: nodeKindFS, Ino: ino,
path: path,
}
node = f.NewInode(ctx, operations, stable)
fh = &bytesFileWriteHandle{
path: path,
content: make([]byte, 0),
}
log.Debug("FUSE Creating file").Str("path", path).Send()
errno = 0
return node, fh, fuseFlags, 0
}
var _ fs.NodeMkdirer = (*ITNode)(nil)
func (f *ITNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
if f.kind != 2 {
return nil, syscall.EROFS
}
path := f.path + "/" + name
err := myfs.Mkdir(path)
if err != nil {
log.Error("FUSE Mkdir failed").
Str("path", path).
Err(err).
Send()
return nil, syscallErr(err)
}
ino := uint64(len(inodemap)) + 11
inodemap[path] = ino
stable := fs.StableAttr{
Mode: fuse.S_IFDIR,
Ino: ino,
}
operations := &ITNode{
kind: nodeKindFS, Ino: ino,
path: path,
}
node := f.NewInode(ctx, operations, stable)
log.Debug("FUSE Mkdir success").
Str("path", path).
Int("ino", int(ino)).
Send()
return node, 0
}
var _ fs.NodeRenamer = (*ITNode)(nil)
func (f *ITNode) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno {
if f.kind != 2 {
return syscall.EROFS
}
p1 := f.path + "/" + name
p2 := newParent.EmbeddedInode().Path(nil)[2:] + "/" + newName
err := myfs.Rename(p1, p2)
if err != nil {
log.Error("FUSE Rename failed").
Str("src", p1).
Str("dest", p2).
Err(err).
Send()
return syscallErr(err)
}
log.Debug("FUSE Rename sucess").
Str("src", p1).
Str("dest", p2).
Send()
ino := inodemap[p1]
delete(inodemap, p1)
inodemap[p2] = ino
return 0
}
var _ fs.NodeUnlinker = (*ITNode)(nil)
func (f *ITNode) Unlink(ctx context.Context, name string) syscall.Errno {
if f.kind != 2 {
return syscall.EROFS
}
delete(inodemap, f.path+"/"+name)
err := myfs.Remove(f.path + "/" + name)
if err != nil {
log.Error("FUSE Unlink failed").
Str("file", f.path+"/"+name).
Err(err).
Send()
return syscallErr(err)
}
log.Debug("FUSE Unlink success").
Str("file", f.path+"/"+name).
Send()
return 0
}
var _ fs.NodeRmdirer = (*ITNode)(nil)
func (f *ITNode) Rmdir(ctx context.Context, name string) syscall.Errno {
return f.Unlink(ctx, name)
}
+74
View File
@@ -0,0 +1,74 @@
package fusefs
import (
"syscall"
"go.elara.ws/infinitime/blefs"
)
func syscallErr(err error) syscall.Errno {
if err == nil {
return 0
}
switch err := err.(type) {
case blefs.FSError:
switch err.Code {
case 0x02: // filesystem error
return syscall.EIO // TODO
case 0x05: // read-only filesystem
return syscall.EROFS
case 0x03: // no such file
return syscall.ENOENT
case 0x04: // protocol error
return syscall.EPROTO
case -5: // input/output error
return syscall.EIO
case -84: // filesystem is corrupted
return syscall.ENOTRECOVERABLE // TODO
case -2: // no such directory entry
return syscall.ENOENT
case -17: // entry already exists
return syscall.EEXIST
case -20: // entry is not a directory
return syscall.ENOTDIR
case -39: // directory is not empty
return syscall.ENOTEMPTY
case -9: // bad file number
return syscall.EBADF
case -27: // file is too large
return syscall.EFBIG
case -22: // invalid parameter
return syscall.EINVAL
case -28: // no space left on device
return syscall.ENOSPC
case -12: // no more memory available
return syscall.ENOMEM
case -61: // no attr available
return syscall.ENODATA // TODO
case -36: // file name is too long
return syscall.ENAMETOOLONG
}
default:
switch err {
case blefs.ErrFileNotExists: // file does not exist
return syscall.ENOENT
case blefs.ErrFileReadOnly: // file is read only
return syscall.EACCES
case blefs.ErrFileWriteOnly: // file is write only
return syscall.EACCES
case blefs.ErrInvalidOffset: // invalid file offset
return syscall.EINVAL
case blefs.ErrOffsetChanged: // offset has already been changed
return syscall.ESPIPE
case blefs.ErrReadOpen: // only one file can be opened for reading at a time
return syscall.ENFILE
case blefs.ErrWriteOpen: // only one file can be opened for writing at a time
return syscall.ENFILE
case blefs.ErrNoRemoveRoot: // refusing to remove root directory
return syscall.EPERM
}
}
return syscall.EIO
}
+17
View File
@@ -0,0 +1,17 @@
package fusefs
import (
_ "unsafe"
"github.com/hanwen/go-fuse/v2/fuse"
)
func Unmount(mountPoint string) error {
return unmount(mountPoint, &fuse.MountOptions{DirectMount: false})
}
// Unfortunately, the FUSE library does not export its unmount function,
// so this is required until that changes
//
//go:linkname unmount github.com/hanwen/go-fuse/v2/fuse.unmount
func unmount(mountPoint string, opts *fuse.MountOptions) error
+3
View File
@@ -0,0 +1,3 @@
package rpc
//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-drpc_out=. --go-drpc_opt=paths=source_relative itd.proto
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
syntax = "proto3";
package rpc;
option go_package = "go.arsenm.dev/itd/internal/rpc";
message Empty {};
message IntResponse {
uint32 value = 1;
}
message StringResponse {
string value = 1;
}
message MotionResponse {
int32 x = 1;
int32 y = 2;
int32 z = 3;
}
message NotifyRequest {
string title = 1;
string body = 2;
}
message SetTimeRequest {
int64 unix_nano = 1;
}
message FirmwareUpgradeRequest {
enum Type {
Archive = 0;
Files = 1;
}
Type type = 1;
repeated string files = 2;
}
message DFUProgress {
int64 sent = 1;
int64 recieved = 2;
int64 total = 3;
}
service ITD {
rpc HeartRate(Empty) returns (IntResponse);
rpc WatchHeartRate(Empty) returns (stream IntResponse);
rpc BatteryLevel(Empty) returns (IntResponse);
rpc WatchBatteryLevel(Empty) returns (stream IntResponse);
rpc Motion(Empty) returns (MotionResponse);
rpc WatchMotion(Empty) returns (stream MotionResponse);
rpc StepCount(Empty) returns (IntResponse);
rpc WatchStepCount(Empty) returns (stream IntResponse);
rpc Version(Empty) returns (StringResponse);
rpc Address(Empty) returns (StringResponse);
rpc Notify(NotifyRequest) returns (Empty);
rpc SetTime(SetTimeRequest) returns (Empty);
rpc WeatherUpdate(Empty) returns (Empty);
rpc FirmwareUpgrade(FirmwareUpgradeRequest) returns (stream DFUProgress);
}
message PathRequest {
string path = 1;
}
message PathsRequest {
repeated string paths = 1;
}
message RenameRequest {
string from = 1;
string to = 2;
}
message TransferRequest {
string source = 1;
string destination = 2;
}
message FileInfo {
string name = 1;
int64 size = 2;
bool is_dir = 3;
}
message DirResponse {
repeated FileInfo entries = 1;
}
message TransferProgress {
uint32 sent = 1;
uint32 total = 2;
}
message ResourceLoadProgress {
enum Operation {
Upload = 0;
RemoveObsolete = 1;
}
string name = 1;
int64 total = 2;
int64 sent = 3;
Operation operation = 4;
}
service FS {
rpc RemoveAll(PathsRequest) returns (Empty);
rpc Remove(PathsRequest) returns (Empty);
rpc Rename(RenameRequest) returns (Empty);
rpc MkdirAll(PathsRequest) returns (Empty);
rpc Mkdir(PathsRequest) returns (Empty);
rpc ReadDir(PathRequest) returns (DirResponse);
rpc Upload(TransferRequest) returns (stream TransferProgress);
rpc Download(TransferRequest) returns (stream TransferProgress);
rpc LoadResources(PathRequest) returns (stream ResourceLoadProgress);
}
File diff suppressed because it is too large Load Diff
-150
View File
@@ -1,150 +0,0 @@
package types
import (
"fmt"
"strconv"
)
const (
ReqTypeHeartRate = iota
ReqTypeBattLevel
ReqTypeFwVersion
ReqTypeFwUpgrade
ReqTypeBtAddress
ReqTypeNotify
ReqTypeSetTime
ReqTypeWatchHeartRate
ReqTypeWatchBattLevel
ReqTypeMotion
ReqTypeWatchMotion
ReqTypeStepCount
ReqTypeWatchStepCount
ReqTypeCancel
ReqTypeFS
ReqTypeWeatherUpdate
)
const (
UpgradeTypeArchive = iota
UpgradeTypeFiles
)
const (
FSTypeWrite = iota
FSTypeRead
FSTypeMove
FSTypeDelete
FSTypeList
FSTypeMkdir
)
type ReqDataFS struct {
Type int `json:"type"`
Files []string `json:"files"`
Data string `json:"data,omitempty"`
}
type ReqDataFwUpgrade struct {
Type int
Files []string
}
type Response struct {
Type int `json:"type"`
Value interface{} `json:"value,omitempty"`
Message string `json:"msg,omitempty"`
ID string `json:"id,omitempty"`
Error bool `json:"error"`
}
type Request struct {
Type int `json:"type"`
Data interface{} `json:"data,omitempty"`
}
type ReqDataNotify struct {
Title string
Body string
}
type DFUProgress struct {
Received int64 `mapstructure:"recvd"`
Total int64 `mapstructure:"total"`
Sent int64 `mapstructure:"sent"`
}
type FSTransferProgress struct {
Type int `json:"type" mapstructure:"type"`
Total uint32 `json:"total" mapstructure:"total"`
Sent uint32 `json:"sent" mapstructure:"sent"`
Done bool `json:"done" mapstructure:"done"`
}
type MotionValues struct {
X int16
Y int16
Z int16
}
type FileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
IsDir bool `json:"isDir"`
}
func (fi FileInfo) String() string {
var isDirChar rune
if fi.IsDir {
isDirChar = 'd'
} else {
isDirChar = '-'
}
// Get human-readable value for file size
val, unit := bytesHuman(fi.Size)
prec := 0
// If value is less than 10, set precision to 1
if val < 10 {
prec = 1
}
// Convert float to string
valStr := strconv.FormatFloat(val, 'f', prec, 64)
// Return string formatted like so:
// - 10 kB file
// or:
// d 0 B .
return fmt.Sprintf(
"%c %3s %-2s %s",
isDirChar,
valStr,
unit,
fi.Name,
)
}
// bytesHuman returns a human-readable string for
// the amount of bytes inputted.
func bytesHuman(b int64) (float64, string) {
const unit = 1000
// Set possible units prefixes (PineTime flash is 4MB)
units := [2]rune{'k', 'M'}
// If amount of bytes is less than smallest unit
if b < unit {
// Return unchanged with unit "B"
return float64(b), "B"
}
div, exp := int64(unit), 0
// Get decimal values and unit prefix index
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
// Create string for full unit
unitStr := string([]rune{units[exp], 'B'})
// Return decimal with unit string
return float64(b) / float64(div), unitStr
}
+10 -6
View File
@@ -1,10 +1,14 @@
package main package utils
import "github.com/godbus/dbus/v5" import (
"context"
func newSystemBusConn() (*dbus.Conn, error) { "github.com/godbus/dbus/v5"
)
func NewSystemBusConn(ctx context.Context) (*dbus.Conn, error) {
// Connect to dbus session bus // Connect to dbus session bus
conn, err := dbus.SystemBusPrivate() conn, err := dbus.SystemBusPrivate(dbus.WithContext(ctx))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -19,9 +23,9 @@ func newSystemBusConn() (*dbus.Conn, error) {
return conn, nil return conn, nil
} }
func newSessionBusConn() (*dbus.Conn, error) { func NewSessionBusConn(ctx context.Context) (*dbus.Conn, error) {
// Connect to dbus session bus // Connect to dbus session bus
conn, err := dbus.SessionBusPrivate() conn, err := dbus.SessionBusPrivate(dbus.WithContext(ctx))
if err != nil { if err != nil {
return nil, err return nil, err
} }
+22
View File
@@ -1,6 +1,25 @@
[bluetooth]
adapter = "hci0"
[socket] [socket]
path = "/tmp/itd/socket" path = "/tmp/itd/socket"
[metrics]
enabled = false
[metrics.heartRate]
enabled = true
[metrics.stepCount]
enabled = true
[metrics.battLevel]
enabled = true
[metrics.motion]
# This may lower the battery life of the PineTime
enabled = false
[conn] [conn]
reconnect = true reconnect = true
@@ -29,3 +48,6 @@
[weather] [weather]
enabled = true enabled = true
location = "Los Angeles, CA" location = "Los Angeles, CA"
[logging]
level = "info"
+5
View File
@@ -0,0 +1,5 @@
[Desktop Entry]
Type=Application
Terminal=false
Exec=/usr/bin/itgui
Name=itgui
+85 -26
View File
@@ -19,26 +19,28 @@
package main package main
import ( import (
"context"
_ "embed" _ "embed"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"os/signal"
"strconv" "strconv"
"sync"
"syscall"
"time" "time"
"github.com/gen2brain/dlgs" "github.com/gen2brain/dlgs"
"github.com/knadh/koanf" "github.com/knadh/koanf"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/rs/zerolog" "go.elara.ws/infinitime"
"github.com/rs/zerolog/log" "go.elara.ws/logger"
"go.arsenm.dev/infinitime" "go.elara.ws/logger/log"
) )
var k = koanf.New(".") var k = koanf.New(".")
//go:embed version.txt
var version string
var ( var (
firmwareUpdating = false firmwareUpdating = false
// The FS must be updated when the watch is reconnected // The FS must be updated when the watch is reconnected
@@ -54,8 +56,13 @@ func main() {
return return
} }
level, err := logger.ParseLogLevel(k.String("logging.level"))
if err != nil {
level = logger.LogLevelInfo
}
// Initialize infinitime library // Initialize infinitime library
infinitime.Init() infinitime.Init(k.String("bluetooth.adapter"))
// Cleanly exit after function // Cleanly exit after function
defer infinitime.Exit() defer infinitime.Exit()
@@ -66,13 +73,15 @@ func main() {
Whitelist: k.Strings("conn.whitelist.devices"), Whitelist: k.Strings("conn.whitelist.devices"),
OnReqPasskey: onReqPasskey, OnReqPasskey: onReqPasskey,
Logger: log.Logger, Logger: log.Logger,
LogLevel: zerolog.WarnLevel, LogLevel: level,
} }
ctx := context.Background()
// Connect to InfiniTime with default options // Connect to InfiniTime with default options
dev, err := infinitime.Connect(opts) dev, err := infinitime.Connect(ctx, opts)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Error connecting to InfiniTime") log.Fatal("Error connecting to InfiniTime").Err(err).Send()
} }
// When InfiniTime reconnects // When InfiniTime reconnects
@@ -103,59 +112,109 @@ func main() {
// Get firmware version // Get firmware version
ver, err := dev.Version() ver, err := dev.Version()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error getting firmware version") log.Error("Error getting firmware version").Err(err).Send()
} }
// Log connection // Log connection
log.Info().Str("version", ver).Msg("Connected to InfiniTime") log.Info("Connected to InfiniTime").Str("version", ver).Send()
// If config specifies to notify on connect // If config specifies to notify on connect
if k.Bool("on.connect.notify") { if k.Bool("on.connect.notify") {
// Send notification to InfiniTime // Send notification to InfiniTime
err = dev.Notify("itd", "Successfully connected") err = dev.Notify("itd", "Successfully connected")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error sending notification to InfiniTime") log.Error("Error sending notification to InfiniTime").Err(err).Send()
} }
} }
// Set time to current time // Set time to current time
err = dev.SetTime(time.Now()) err = dev.SetTime(time.Now())
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error setting current time on connected InfiniTime") log.Error("Error setting current time on connected InfiniTime").Err(err).Send()
} }
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
sig := <-sigCh
log.Warn("Signal received, shutting down").Stringer("signal", sig).Send()
cancel()
}()
wg := WaitGroup{&sync.WaitGroup{}}
// Initialize music controls // Initialize music controls
err = initMusicCtrl(dev) err = initMusicCtrl(ctx, wg, dev)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error initializing music control") log.Error("Error initializing music control").Err(err).Send()
} }
// Start control socket // Start control socket
err = initCallNotifs(dev) err = initCallNotifs(ctx, wg, dev)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error initializing call notifications") log.Error("Error initializing call notifications").Err(err).Send()
} }
// Initialize notification relay // Initialize notification relay
err = initNotifRelay(dev) err = initNotifRelay(ctx, wg, dev)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error initializing notification relay") log.Error("Error initializing notification relay").Err(err).Send()
} }
// Initializa weather // Initializa weather
err = initWeather(dev) err = initWeather(ctx, wg, dev)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error initializing weather") log.Error("Error initializing weather").Err(err).Send()
}
// Initialize metrics collection
err = initMetrics(ctx, wg, dev)
if err != nil {
log.Error("Error intializing metrics collection").Err(err).Send()
}
// Initialize puremaps integration
err = initPureMaps(ctx, wg, dev)
if err != nil {
log.Error("Error intializing puremaps integration").Err(err).Send()
}
// Start fuse socket
if k.Bool("fuse.enabled") {
err = startFUSE(ctx, wg, dev)
if err != nil {
log.Error("Error starting fuse socket").Err(err).Send()
}
} }
// Start control socket // Start control socket
err = startSocket(dev) err = startSocket(ctx, wg, dev)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error starting socket") log.Error("Error starting socket").Err(err).Send()
} }
// Block forever wg.Wait()
select {} }
type x struct {
n int
*sync.WaitGroup
}
func (xy *x) Add(i int) {
xy.n += i
xy.WaitGroup.Add(i)
fmt.Println("add: counter:", xy.n)
}
func (xy *x) Done() {
xy.n -= 1
xy.WaitGroup.Done()
fmt.Println("done: counter:", xy.n)
} }
func onReqPasskey() (uint32, error) { func onReqPasskey() (uint32, error) {
+214
View File
@@ -0,0 +1,214 @@
package main
import (
"context"
"strings"
"github.com/godbus/dbus/v5"
"go.elara.ws/infinitime"
"go.elara.ws/itd/internal/utils"
"go.elara.ws/logger/log"
)
const (
interfaceName = "io.github.rinigus.PureMaps.navigator"
iconProperty = interfaceName + ".icon"
narrativeProperty = interfaceName + ".narrative"
manDistProperty = interfaceName + ".manDist"
progressProperty = interfaceName + ".progress"
)
func initPureMaps(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Connect to session bus. This connection is for method calls.
conn, err := utils.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 := utils.NewSessionBusConn(ctx)
if err != nil {
return err
}
// Define rules to listen for
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("Error setting all navigation fields").Err(err).Send()
}
}
wg.Add(1)
go func() {
defer wg.Done("pureMaps")
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("Error getting dbus member field").Err(err).Send()
continue
}
if !strings.HasSuffix(member, "Changed") {
continue
}
log.Debug("Signal received from PureMaps navigator").Str("member", member).Send()
// 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("Error getting property").Err(err).Str("property", member).Send()
continue
}
err = dev.Navigation.SetFlag(infinitime.NavFlag(icon))
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
}
case "narrative":
var narrative string
err = navigator.StoreProperty(narrativeProperty, &narrative)
if err != nil {
log.Error("Error getting property").Err(err).Str("property", member).Send()
continue
}
err = dev.Navigation.SetNarrative(narrative)
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
}
case "manDist":
var manDist string
err = navigator.StoreProperty(manDistProperty, &manDist)
if err != nil {
log.Error("Error getting property").Err(err).Str("property", member).Send()
continue
}
err = dev.Navigation.SetManDist(manDist)
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
}
case "progress":
var progress int32
err = navigator.StoreProperty(progressProperty, &progress)
if err != nil {
log.Error("Error getting property").Err(err).Str("property", member).Send()
continue
}
err = dev.Navigation.SetProgress(uint8(progress))
if err != nil {
log.Error("Error setting flag").Err(err).Str("property", member).Send()
continue
}
}
case <-ctx.Done():
return
}
}
}()
if exists {
log.Info("Sending PureMaps data to InfiniTime").Send()
}
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
}
+139
View File
@@ -0,0 +1,139 @@
package main
import (
"context"
"database/sql"
"path/filepath"
"time"
"go.elara.ws/infinitime"
"go.elara.ws/logger/log"
_ "modernc.org/sqlite"
)
func initMetrics(ctx context.Context, wg WaitGroup, 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,
)
}
}()
}
wg.Add(1)
go func() {
defer wg.Done("metrics")
<-ctx.Done()
db.Close()
}()
log.Info("Initialized metrics collection").Send()
return nil
}
+270
View File
@@ -0,0 +1,270 @@
package mpris
import (
"context"
"strings"
"sync"
"github.com/godbus/dbus/v5"
"go.elara.ws/itd/internal/utils"
)
var (
method, monitor *dbus.Conn
monitorCh chan *dbus.Message
onChangeOnce sync.Once
)
// Init makes required connections to DBus and
// initializes change monitoring channel
func Init(ctx context.Context) error {
// Connect to session bus for monitoring
monitorConn, err := utils.NewSessionBusConn(ctx)
if err != nil {
return err
}
// Add match rule for PropertiesChanged on media player
monitorConn.AddMatchSignal(
dbus.WithMatchObjectPath("/org/mpris/MediaPlayer2"),
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
dbus.WithMatchMember("PropertiesChanged"),
)
monitorCh = make(chan *dbus.Message, 10)
monitorConn.Eavesdrop(monitorCh)
// Connect to session bus for method calls
methodConn, err := utils.NewSessionBusConn(ctx)
if err != nil {
return err
}
method, monitor = methodConn, monitorConn
return nil
}
// Exit closes all connections and channels
func Exit() {
close(monitorCh)
method.Close()
monitor.Close()
}
// Play uses MPRIS to play media
func Play() error {
player, err := getPlayerObj()
if err != nil {
return err
}
if player != nil {
call := player.Call("org.mpris.MediaPlayer2.Player.Play", 0)
if call.Err != nil {
return call.Err
}
}
return nil
}
// Pause uses MPRIS to pause media
func Pause() error {
player, err := getPlayerObj()
if err != nil {
return err
}
if player != nil {
call := player.Call("org.mpris.MediaPlayer2.Player.Pause", 0)
if call.Err != nil {
return call.Err
}
}
return nil
}
// Next uses MPRIS to skip to next media
func Next() error {
player, err := getPlayerObj()
if err != nil {
return err
}
if player != nil {
call := player.Call("org.mpris.MediaPlayer2.Player.Next", 0)
if call.Err != nil {
return call.Err
}
}
return nil
}
// Prev uses MPRIS to skip to previous media
func Prev() error {
player, err := getPlayerObj()
if err != nil {
return err
}
if player != nil {
call := player.Call("org.mpris.MediaPlayer2.Player.Previous", 0)
if call.Err != nil {
return call.Err
}
}
return nil
}
func VolUp(percent uint) error {
player, err := getPlayerObj()
if err != nil {
return err
}
if player != nil {
currentVal, err := player.GetProperty("org.mpris.MediaPlayer2.Player.Volume")
if err != nil {
return err
}
newVal := currentVal.Value().(float64) + (float64(percent) / 100)
err = player.SetProperty("org.mpris.MediaPlayer2.Player.Volume", newVal)
if err != nil {
return err
}
}
return nil
}
func VolDown(percent uint) error {
player, err := getPlayerObj()
if err != nil {
return err
}
if player != nil {
currentVal, err := player.GetProperty("org.mpris.MediaPlayer2.Player.Volume")
if err != nil {
return err
}
newVal := currentVal.Value().(float64) - (float64(percent) / 100)
err = player.SetProperty("org.mpris.MediaPlayer2.Player.Volume", newVal)
if err != nil {
return err
}
}
return nil
}
type ChangeType int
const (
ChangeTypeTitle ChangeType = iota
ChangeTypeArtist
ChangeTypeAlbum
ChangeTypeStatus
)
func (ct ChangeType) String() string {
switch ct {
case ChangeTypeTitle:
return "Title"
case ChangeTypeAlbum:
return "Album"
case ChangeTypeArtist:
return "Artist"
case ChangeTypeStatus:
return "Status"
}
return ""
}
// OnChange runs cb when a value changes
func OnChange(cb func(ChangeType, string)) {
go onChangeOnce.Do(func() {
// For every message on channel
for msg := range monitorCh {
// Parse PropertiesChanged
iface, changed, ok := parsePropertiesChanged(msg)
if !ok || iface != "org.mpris.MediaPlayer2.Player" {
continue
}
// For every property changed
for name, val := range changed {
// If metadata changed
if name == "Metadata" {
// Get fields
fields := val.Value().(map[string]dbus.Variant)
// For every field
for name, val := range fields {
// Handle each field appropriately
if strings.HasSuffix(name, "title") {
title := val.Value().(string)
if title == "" {
title = "Unknown " + ChangeTypeTitle.String()
}
cb(ChangeTypeTitle, title)
} else if strings.HasSuffix(name, "album") {
album := val.Value().(string)
if album == "" {
album = "Unknown " + ChangeTypeAlbum.String()
}
cb(ChangeTypeAlbum, album)
} else if strings.HasSuffix(name, "artist") {
var artists string
switch artistVal := val.Value().(type) {
case string:
artists = artistVal
case []string:
artists = strings.Join(artistVal, ", ")
}
if artists == "" {
artists = "Unknown " + ChangeTypeArtist.String()
}
cb(ChangeTypeArtist, artists)
}
}
} else if name == "PlaybackStatus" {
// Handle status change
cb(ChangeTypeStatus, val.Value().(string))
}
}
}
})
}
// getPlayerNames gets all DBus MPRIS player bus names
func getPlayerNames(conn *dbus.Conn) ([]string, error) {
var names []string
err := conn.BusObject().Call("org.freedesktop.DBus.ListNames", 0).Store(&names)
if err != nil {
return nil, err
}
var players []string
for _, name := range names {
if strings.HasPrefix(name, "org.mpris.MediaPlayer2") {
players = append(players, name)
}
}
return players, nil
}
// GetPlayerObj gets the object corresponding to the first
// bus name found in DBus
func getPlayerObj() (dbus.BusObject, error) {
players, err := getPlayerNames(method)
if err != nil {
return nil, err
}
if len(players) == 0 {
return nil, nil
}
return method.Object(players[0], "/org/mpris/MediaPlayer2"), nil
}
// parsePropertiesChanged parses a DBus PropertiesChanged signal
func parsePropertiesChanged(msg *dbus.Message) (iface string, changed map[string]dbus.Variant, ok bool) {
if len(msg.Body) != 3 {
return "", nil, false
}
iface, ok = msg.Body[0].(string)
if !ok {
return
}
changed, ok = msg.Body[1].(map[string]dbus.Variant)
if !ok {
return
}
return
}
+84
View File
@@ -0,0 +1,84 @@
package mpris
import (
"reflect"
"testing"
"github.com/godbus/dbus/v5"
)
// TestParsePropertiesChanged checks the parsePropertiesChanged function to
// make sure it correctly parses a DBus PropertiesChanged signal.
func TestParsePropertiesChanged(t *testing.T) {
// Create a DBus message
msg := &dbus.Message{
Body: []interface{}{
"com.example.Interface",
map[string]dbus.Variant{
"Property1": dbus.MakeVariant(true),
"Property2": dbus.MakeVariant("Hello, world!"),
},
[]string{},
},
}
// Parse the message
iface, changed, ok := parsePropertiesChanged(msg)
if !ok {
t.Error("Expected parsePropertiesChanged to return true, but got false")
}
// Check the parsed values
expectedIface := "com.example.Interface"
if iface != expectedIface {
t.Errorf("Expected iface to be %q, but got %q", expectedIface, iface)
}
expectedChanged := map[string]dbus.Variant{
"Property1": dbus.MakeVariant(true),
"Property2": dbus.MakeVariant("Hello, world!"),
}
if !reflect.DeepEqual(changed, expectedChanged) {
t.Errorf("Expected changed to be %v, but got %v", expectedChanged, changed)
}
// Test a message with an invalid number of arguments
msg = &dbus.Message{
Body: []interface{}{
"com.example.Interface",
},
}
_, _, ok = parsePropertiesChanged(msg)
if ok {
t.Error("Expected parsePropertiesChanged to return false, but got true")
}
// Test a message with an invalid first argument
msg = &dbus.Message{
Body: []interface{}{
123,
map[string]dbus.Variant{
"Property1": dbus.MakeVariant(true),
"Property2": dbus.MakeVariant("Hello, world!"),
},
[]string{},
},
}
_, _, ok = parsePropertiesChanged(msg)
if ok {
t.Error("Expected parsePropertiesChanged to return false, but got true")
}
// Test a message with an invalid second argument
msg = &dbus.Message{
Body: []interface{}{
"com.example.Interface",
123,
[]string{},
},
}
_, _, ok = parsePropertiesChanged(msg)
if ok {
t.Error("Expected parsePropertiesChanged to return false, but got true")
}
}
+30 -19
View File
@@ -19,29 +19,32 @@
package main package main
import ( import (
"github.com/rs/zerolog/log" "context"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/infinitime/pkg/player"
"go.arsenm.dev/itd/translit" "go.elara.ws/infinitime"
"go.elara.ws/itd/mpris"
"go.elara.ws/itd/translit"
"go.elara.ws/logger/log"
) )
func initMusicCtrl(dev *infinitime.Device) error { func initMusicCtrl(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
player.Init() mpris.Init(ctx)
maps := k.Strings("notifs.translit.use") maps := k.Strings("notifs.translit.use")
translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom")) translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom"))
player.OnChange(func(ct player.ChangeType, val string) { mpris.OnChange(func(ct mpris.ChangeType, val string) {
newVal := translit.Transliterate(val, maps...) newVal := translit.Transliterate(val, maps...)
if !firmwareUpdating { if !firmwareUpdating {
switch ct { switch ct {
case player.ChangeTypeStatus: case mpris.ChangeTypeStatus:
dev.Music.SetStatus(val == "Playing") dev.Music.SetStatus(val == "Playing")
case player.ChangeTypeTitle: case mpris.ChangeTypeTitle:
dev.Music.SetTrack(newVal) dev.Music.SetTrack(newVal)
case player.ChangeTypeAlbum: case mpris.ChangeTypeAlbum:
dev.Music.SetAlbum(newVal) dev.Music.SetAlbum(newVal)
case player.ChangeTypeArtist: case mpris.ChangeTypeArtist:
dev.Music.SetArtist(newVal) dev.Music.SetArtist(newVal)
} }
} }
@@ -52,29 +55,37 @@ func initMusicCtrl(dev *infinitime.Device) error {
if err != nil { if err != nil {
return err return err
} }
wg.Add(1)
go func() { go func() {
defer wg.Done("musicCtrl")
// For every music event received // For every music event received
for musicEvt := range musicEvtCh { for {
select {
case musicEvt := <-musicEvtCh:
// Perform appropriate action based on event // Perform appropriate action based on event
switch musicEvt { switch musicEvt {
case infinitime.MusicEventPlay: case infinitime.MusicEventPlay:
player.Play() mpris.Play()
case infinitime.MusicEventPause: case infinitime.MusicEventPause:
player.Pause() mpris.Pause()
case infinitime.MusicEventNext: case infinitime.MusicEventNext:
player.Next() mpris.Next()
case infinitime.MusicEventPrev: case infinitime.MusicEventPrev:
player.Prev() mpris.Prev()
case infinitime.MusicEventVolUp: case infinitime.MusicEventVolUp:
player.VolUp(uint(k.Int("music.vol.interval"))) mpris.VolUp(uint(k.Int("music.vol.interval")))
case infinitime.MusicEventVolDown: case infinitime.MusicEventVolDown:
player.VolDown(uint(k.Int("music.vol.interval"))) mpris.VolDown(uint(k.Int("music.vol.interval")))
}
case <-ctx.Done():
return
} }
} }
}() }()
// Log completed initialization // Log completed initialization
log.Info().Msg("Initialized InfiniTime music controls") log.Info("Initialized InfiniTime music controls").Send()
return nil return nil
} }
+21 -9
View File
@@ -19,28 +19,32 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/rs/zerolog/log" "go.elara.ws/infinitime"
"go.arsenm.dev/infinitime" "go.elara.ws/itd/internal/utils"
"go.arsenm.dev/itd/translit" "go.elara.ws/itd/translit"
"go.elara.ws/logger/log"
) )
func initNotifRelay(dev *infinitime.Device) error { func initNotifRelay(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
// Connect to dbus session bus // Connect to dbus session bus
bus, err := newSessionBusConn() bus, err := utils.NewSessionBusConn(ctx)
if err != nil { if err != nil {
return err return err
} }
// Define rules to listen for // Define rules to listen for
var rules = []string{ rules := []string{
"type='method_call',member='Notify',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications'", "type='method_call',member='Notify',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications'",
} }
var flag uint = 0 var flag uint = 0
// Becode monitor for notifications // Becode monitor for notifications
call := bus.BusObject().Call("org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag) call := bus.BusObject().CallWithContext(
ctx, "org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, rules, flag,
)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
@@ -50,9 +54,13 @@ func initNotifRelay(dev *infinitime.Device) error {
// Send events to channel // Send events to channel
bus.Eavesdrop(notifCh) bus.Eavesdrop(notifCh)
wg.Add(1)
go func() { go func() {
defer wg.Done("notifRelay")
// For every event sent to channel // For every event sent to channel
for v := range notifCh { for {
select {
case v := <-notifCh:
// If firmware is updating, skip // If firmware is updating, skip
if firmwareUpdating { if firmwareUpdating {
continue continue
@@ -87,10 +95,14 @@ func initNotifRelay(dev *infinitime.Device) error {
} }
dev.Notify(sender, msg) dev.Notify(sender, msg)
case <-ctx.Done():
bus.Close()
return
}
} }
}() }()
log.Info().Msg("Relaying notifications to InfiniTime") log.Info("Relaying notifications to InfiniTime").Send()
return nil return nil
} }
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
git describe --tags > version.txt
+291 -522
View File
@@ -19,51 +19,32 @@
package main package main
import ( import (
"bufio" "context"
"encoding/json" "errors"
"fmt"
"io" "io"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/google/uuid" "go.elara.ws/drpc/muxserver"
"github.com/mitchellh/mapstructure" "go.elara.ws/infinitime"
"github.com/rs/zerolog/log" "go.elara.ws/infinitime/blefs"
"go.arsenm.dev/infinitime" "go.elara.ws/itd/internal/rpc"
"go.arsenm.dev/infinitime/blefs" "go.elara.ws/logger/log"
"go.arsenm.dev/itd/internal/types" "storj.io/drpc/drpcmux"
"go.arsenm.dev/itd/translit"
) )
type DoneMap map[string]chan struct{} var (
ErrDFUInvalidFile = errors.New("provided file is invalid for given upgrade type")
ErrDFUNotEnoughFiles = errors.New("not enough files provided for given upgrade type")
ErrDFUInvalidUpgType = errors.New("invalid upgrade type")
)
func (dm DoneMap) Exists(key string) bool { func startSocket(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
_, ok := dm[key]
return ok
}
func (dm DoneMap) Done(key string) {
ch := dm[key]
ch <- struct{}{}
}
func (dm DoneMap) Create(key string) {
dm[key] = make(chan struct{}, 1)
}
func (dm DoneMap) Remove(key string) {
close(dm[key])
delete(dm, key)
}
var done = DoneMap{}
func startSocket(dev *infinitime.Device) error {
// Make socket directory if non-existant // Make socket directory if non-existant
err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0755) err := os.MkdirAll(filepath.Dir(k.String("socket.path")), 0o755)
if err != nil { if err != nil {
return err return err
} }
@@ -82,610 +63,398 @@ func startSocket(dev *infinitime.Device) error {
fs, err := dev.FS() fs, err := dev.FS()
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Error getting BLE filesystem") log.Warn("Error getting BLE filesystem").Err(err).Send()
} }
go func() { mux := drpcmux.New()
for {
// Accept socket connection err = rpc.DRPCRegisterITD(mux, &ITD{dev})
conn, err := ln.Accept()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error accepting connection") return err
} }
// Concurrently handle connection err = rpc.DRPCRegisterFS(mux, &FS{dev, fs})
go handleConnection(conn, dev, fs) if err != nil {
return err
} }
log.Info("Starting control socket").Str("path", k.String("socket.path")).Send()
wg.Add(1)
go func() {
defer wg.Done("socket")
muxserver.New(mux).Serve(ctx, ln)
}() }()
// Log socket start
log.Info().Str("path", k.String("socket.path")).Msg("Started control socket")
return nil return nil
} }
func handleConnection(conn net.Conn, dev *infinitime.Device, fs *blefs.FS) { type ITD struct {
defer conn.Close() dev *infinitime.Device
}
// If an FS update is required (reconnect ocurred) func (i *ITD) HeartRate(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
if updateFS { hr, err := i.dev.HeartRate()
// Get new FS return &rpc.IntResponse{Value: uint32(hr)}, err
newFS, err := dev.FS() }
func (i *ITD) WatchHeartRate(_ *rpc.Empty, s rpc.DRPCITD_WatchHeartRateStream) error {
heartRateCh, err := i.dev.WatchHeartRate(s.Context())
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Error updating BLE filesystem") return err
} else {
// Set FS pointer to new FS
*fs = *newFS
// Reset updateFS
updateFS = false
}
} }
// Create new scanner on connection
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
var req types.Request
// Decode scanned message into types.Request
err := json.Unmarshal(scanner.Bytes(), &req)
if err != nil {
connErr(conn, req.Type, err, "Error decoding JSON input")
continue
}
// If firmware is updating, return error
if firmwareUpdating {
connErr(conn, req.Type, nil, "Firmware update in progress")
return
}
switch req.Type {
case types.ReqTypeHeartRate:
// Get heart rate from watch
heartRate, err := dev.HeartRate()
if err != nil {
connErr(conn, req.Type, err, "Error getting heart rate")
break
}
// Encode heart rate to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: heartRate,
})
case types.ReqTypeWatchHeartRate:
heartRateCh, cancel, err := dev.WatchHeartRate()
if err != nil {
connErr(conn, req.Type, err, "Error getting heart rate channel")
break
}
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every heart rate value
for heartRate := range heartRateCh { for heartRate := range heartRateCh {
select { err = s.Send(&rpc.IntResponse{Value: uint32(heartRate)})
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: heartRate,
})
}
}
}()
case types.ReqTypeBattLevel:
// Get battery level from watch
battLevel, err := dev.BatteryLevel()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting battery level") return err
break
} }
// Encode battery level to connection }
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type, return nil
Value: battLevel, }
})
case types.ReqTypeWatchBattLevel: func (i *ITD) BatteryLevel(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
battLevelCh, cancel, err := dev.WatchBatteryLevel() bl, err := i.dev.BatteryLevel()
return &rpc.IntResponse{Value: uint32(bl)}, err
}
func (i *ITD) WatchBatteryLevel(_ *rpc.Empty, s rpc.DRPCITD_WatchBatteryLevelStream) error {
battLevelCh, err := i.dev.WatchBatteryLevel(s.Context())
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting battery level channel") return err
break
} }
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every battery level value
for battLevel := range battLevelCh { for battLevel := range battLevelCh {
select { err = s.Send(&rpc.IntResponse{Value: uint32(battLevel)})
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: battLevel,
})
}
}
}()
case types.ReqTypeMotion:
// Get battery level from watch
motionVals, err := dev.Motion()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting motion values") return err
break
} }
// Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: motionVals,
})
case types.ReqTypeWatchMotion:
motionValCh, cancel, err := dev.WatchMotion()
if err != nil {
connErr(conn, req.Type, err, "Error getting heart rate channel")
break
} }
reqID := uuid.New().String()
go func() {
done.Create(reqID)
// For every motion event
for motionVals := range motionValCh {
select {
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return return nil
default: }
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{ func (i *ITD) Motion(_ context.Context, _ *rpc.Empty) (*rpc.MotionResponse, error) {
Type: req.Type, motionVals, err := i.dev.Motion()
ID: reqID, return &rpc.MotionResponse{
Value: motionVals, X: int32(motionVals.X),
}) Y: int32(motionVals.Y),
} Z: int32(motionVals.Z),
} }, err
}() }
case types.ReqTypeStepCount:
// Get battery level from watch func (i *ITD) WatchMotion(_ *rpc.Empty, s rpc.DRPCITD_WatchMotionStream) error {
stepCount, err := dev.StepCount() motionValsCh, err := i.dev.WatchMotion(s.Context())
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting step count") return err
break
} }
// Encode battery level to connection
json.NewEncoder(conn).Encode(types.Response{ for motionVals := range motionValsCh {
Type: req.Type, err = s.Send(&rpc.MotionResponse{
Value: stepCount, X: int32(motionVals.X),
Y: int32(motionVals.Y),
Z: int32(motionVals.Z),
}) })
case types.ReqTypeWatchStepCount:
stepCountCh, cancel, err := dev.WatchStepCount()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting heart rate channel") return err
break
} }
reqID := uuid.New().String() }
go func() {
done.Create(reqID) return nil
// For every step count value }
func (i *ITD) StepCount(_ context.Context, _ *rpc.Empty) (*rpc.IntResponse, error) {
sc, err := i.dev.StepCount()
return &rpc.IntResponse{Value: sc}, err
}
func (i *ITD) WatchStepCount(_ *rpc.Empty, s rpc.DRPCITD_WatchStepCountStream) error {
stepCountCh, err := i.dev.WatchStepCount(s.Context())
if err != nil {
return err
}
for stepCount := range stepCountCh { for stepCount := range stepCountCh {
select { err = s.Send(&rpc.IntResponse{Value: stepCount})
case <-done[reqID]:
// Stop notifications if done signal received
cancel()
done.Remove(reqID)
return
default:
// Encode response to connection if no done signal received
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
ID: reqID,
Value: stepCount,
})
}
}
}()
case types.ReqTypeFwVersion:
// Get firmware version from watch
version, err := dev.Version()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting firmware version") return err
break
} }
// Encode version to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: version,
})
case types.ReqTypeBtAddress:
// Encode bluetooth address to connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: dev.Address(),
})
case types.ReqTypeNotify:
// If no data, return error
if req.Data == nil {
connErr(conn, req.Type, nil, "Data required for notify request")
break
}
var reqData types.ReqDataNotify
// Decode data map to notify request data
err = mapstructure.Decode(req.Data, &reqData)
if err != nil {
connErr(conn, req.Type, err, "Error decoding request data")
break
}
maps := k.Strings("notifs.translit.use")
translit.Transliterators["custom"] = translit.Map(k.Strings("notifs.translit.custom"))
title := translit.Transliterate(reqData.Title, maps...)
body := translit.Transliterate(reqData.Body, maps...)
// Send notification to watch
err = dev.Notify(title, body)
if err != nil {
connErr(conn, req.Type, err, "Error sending notification")
break
}
// Encode empty types.Response to connection
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.ReqTypeSetTime:
// If no data, return error
if req.Data == nil {
connErr(conn, req.Type, nil, "Data required for settime request")
break
}
// Get string from data or return error
reqTimeStr, ok := req.Data.(string)
if !ok {
connErr(conn, req.Type, nil, "Data for settime request must be RFC3339 formatted time string")
break
} }
var reqTime time.Time return nil
if reqTimeStr == "now" { }
reqTime = time.Now()
} else { func (i *ITD) Version(_ context.Context, _ *rpc.Empty) (*rpc.StringResponse, error) {
// Parse time as RFC3339/ISO8601 v, err := i.dev.Version()
reqTime, err = time.Parse(time.RFC3339, reqTimeStr) return &rpc.StringResponse{Value: v}, err
if err != nil { }
connErr(conn, req.Type, err, "Invalid time format. Time string must be formatted as ISO8601 or the word `now`")
break func (i *ITD) Address(_ context.Context, _ *rpc.Empty) (*rpc.StringResponse, error) {
} return &rpc.StringResponse{Value: i.dev.Address()}, nil
} }
// Set time on watch
err = dev.SetTime(reqTime) func (i *ITD) Notify(_ context.Context, data *rpc.NotifyRequest) (*rpc.Empty, error) {
if err != nil { return &rpc.Empty{}, i.dev.Notify(data.Title, data.Body)
connErr(conn, req.Type, err, "Error setting device time") }
break
} func (i *ITD) SetTime(_ context.Context, data *rpc.SetTimeRequest) (*rpc.Empty, error) {
// Encode empty types.Response to connection return &rpc.Empty{}, i.dev.SetTime(time.Unix(0, data.UnixNano))
json.NewEncoder(conn).Encode(types.Response{Type: req.Type}) }
case types.ReqTypeFwUpgrade:
// If no data, return error func (i *ITD) WeatherUpdate(context.Context, *rpc.Empty) (*rpc.Empty, error) {
if req.Data == nil { sendWeatherCh <- struct{}{}
connErr(conn, req.Type, nil, "Data required for firmware upgrade request") return &rpc.Empty{}, nil
break }
}
var reqData types.ReqDataFwUpgrade func (i *ITD) FirmwareUpgrade(data *rpc.FirmwareUpgradeRequest, s rpc.DRPCITD_FirmwareUpgradeStream) error {
// Decode data map to firmware upgrade request data i.dev.DFU.Reset()
err = mapstructure.Decode(req.Data, &reqData)
if err != nil { switch data.Type {
connErr(conn, req.Type, err, "Error decoding request data") case rpc.FirmwareUpgradeRequest_Archive:
break
}
// Reset DFU to prepare for next update
dev.DFU.Reset()
switch reqData.Type {
case types.UpgradeTypeArchive:
// If less than one file, return error // If less than one file, return error
if len(reqData.Files) < 1 { if len(data.Files) < 1 {
connErr(conn, req.Type, nil, "Archive upgrade requires one file with .zip extension") return ErrDFUNotEnoughFiles
break
} }
// If file is not zip archive, return error // If file is not zip archive, return error
if filepath.Ext(reqData.Files[0]) != ".zip" { if filepath.Ext(data.Files[0]) != ".zip" {
connErr(conn, req.Type, nil, "Archive upgrade file must be a zip archive") return ErrDFUInvalidFile
break
} }
// Load DFU archive // Load DFU archive
err := dev.DFU.LoadArchive(reqData.Files[0]) err := i.dev.DFU.LoadArchive(data.Files[0])
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error loading archive file") return err
break
} }
case types.UpgradeTypeFiles: case rpc.FirmwareUpgradeRequest_Files:
// If less than two files, return error // If less than two files, return error
if len(reqData.Files) < 2 { if len(data.Files) < 2 {
connErr(conn, req.Type, nil, "Files upgrade requires two files. First with .dat and second with .bin extension.") return ErrDFUNotEnoughFiles
break
} }
// If first file is not init packet, return error // If first file is not init packet, return error
if filepath.Ext(reqData.Files[0]) != ".dat" { if filepath.Ext(data.Files[0]) != ".dat" {
connErr(conn, req.Type, nil, "First file must be a .dat file") return ErrDFUInvalidFile
break
} }
// If second file is not firmware image, return error // If second file is not firmware image, return error
if filepath.Ext(reqData.Files[1]) != ".bin" { if filepath.Ext(data.Files[1]) != ".bin" {
connErr(conn, req.Type, nil, "Second file must be a .bin file") return ErrDFUInvalidFile
break
} }
// Load individual DFU files // Load individual DFU files
err := dev.DFU.LoadFiles(reqData.Files[0], reqData.Files[1]) err := i.dev.DFU.LoadFiles(data.Files[0], data.Files[1])
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error loading firmware files") return err
break
} }
default:
return ErrDFUInvalidUpgType
} }
go func() { go func() {
// Get progress for event := range i.dev.DFU.Progress() {
progress := dev.DFU.Progress() _ = s.Send(&rpc.DFUProgress{
// For every progress event Sent: int64(event.Sent),
for event := range progress { Recieved: int64(event.Received),
// Encode event on connection Total: event.Total,
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: event,
}) })
} }
firmwareUpdating = false firmwareUpdating = false
}() }()
// Set firmwareUpdating // Set firmwareUpdating
firmwareUpdating = true firmwareUpdating = true
// Start DFU // Start DFU
err = dev.DFU.Start() err := i.dev.DFU.Start()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error performing upgrade")
firmwareUpdating = false firmwareUpdating = false
break return err
}
firmwareUpdating = false
case types.ReqTypeFS:
if fs == nil {
connErr(conn, req.Type, nil, "BLE filesystem is not available")
break
} }
// If no data, return error return nil
if req.Data == nil { }
connErr(conn, req.Type, nil, "Data required for filesystem operations")
break
}
var reqData types.ReqDataFS type FS struct {
// Decode data map to firmware upgrade request data dev *infinitime.Device
err = mapstructure.Decode(req.Data, &reqData) fs *blefs.FS
if err != nil { }
connErr(conn, req.Type, err, "Error decoding request data")
break
}
// Clean input filepaths func (fs *FS) RemoveAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
reqData.Files = cleanPaths(reqData.Files) fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.RemoveAll(path)
if err != nil {
return &rpc.Empty{}, err
}
}
return &rpc.Empty{}, nil
}
switch reqData.Type { func (fs *FS) Remove(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
case types.FSTypeDelete: fs.updateFS()
if len(reqData.Files) == 0 { for _, path := range req.Paths {
connErr(conn, req.Type, nil, "Remove FS command requires at least one file") err := fs.fs.Remove(path)
break
}
for _, file := range reqData.Files {
err := fs.Remove(file)
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error removing file") return &rpc.Empty{}, err
break
} }
} }
json.NewEncoder(conn).Encode(types.Response{Type: req.Type}) return &rpc.Empty{}, nil
case types.FSTypeMove: }
if len(reqData.Files) != 2 {
connErr(conn, req.Type, nil, "Move FS command requires an old path and new path in the files list") func (fs *FS) Rename(_ context.Context, req *rpc.RenameRequest) (*rpc.Empty, error) {
break fs.updateFS()
} return &rpc.Empty{}, fs.fs.Rename(req.From, req.To)
err := fs.Rename(reqData.Files[0], reqData.Files[1]) }
func (fs *FS) MkdirAll(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.MkdirAll(path)
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error moving file") return &rpc.Empty{}, err
break
} }
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.FSTypeMkdir:
if len(reqData.Files) == 0 {
connErr(conn, req.Type, nil, "Mkdir FS command requires at least one file")
break
} }
for _, file := range reqData.Files { return &rpc.Empty{}, nil
err := fs.Mkdir(file) }
func (fs *FS) Mkdir(_ context.Context, req *rpc.PathsRequest) (*rpc.Empty, error) {
fs.updateFS()
for _, path := range req.Paths {
err := fs.fs.Mkdir(path)
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error creating directory") return &rpc.Empty{}, err
break
} }
} }
json.NewEncoder(conn).Encode(types.Response{Type: req.Type}) return &rpc.Empty{}, nil
case types.FSTypeList: }
if len(reqData.Files) != 1 {
connErr(conn, req.Type, nil, "List FS command requires a path to list in the files list") func (fs *FS) ReadDir(_ context.Context, req *rpc.PathRequest) (*rpc.DirResponse, error) {
break fs.updateFS()
}
entries, err := fs.ReadDir(reqData.Files[0]) entries, err := fs.fs.ReadDir(req.Path)
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error reading directory") return nil, err
break
} }
var out []types.FileInfo var fileInfo []*rpc.FileInfo
for _, entry := range entries { for _, entry := range entries {
info, err := entry.Info() info, err := entry.Info()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting file info") return nil, err
break
} }
out = append(out, types.FileInfo{ fileInfo = append(fileInfo, &rpc.FileInfo{
Name: info.Name(), Name: info.Name(),
Size: info.Size(), Size: info.Size(),
IsDir: info.IsDir(), IsDir: info.IsDir(),
}) })
} }
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: out,
})
case types.FSTypeWrite:
if len(reqData.Files) != 2 {
connErr(conn, req.Type, nil, "Write FS command requires a path to the file to write")
break
}
localFile, err := os.Open(reqData.Files[1]) return &rpc.DirResponse{Entries: fileInfo}, nil
}
func (fs *FS) Upload(req *rpc.TransferRequest, s rpc.DRPCFS_UploadStream) error {
fs.updateFS()
localFile, err := os.Open(req.Source)
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error opening local file") return err
break
} }
defer localFile.Close()
localInfo, err := localFile.Stat() localInfo, err := localFile.Stat()
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error getting local file information") return err
break
} }
remoteFile, err := fs.Create(reqData.Files[0], uint32(localInfo.Size())) remoteFile, err := fs.fs.Create(req.Destination, uint32(localInfo.Size()))
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error creating remote file") return err
break
} }
go func() {
// For every progress event
for sent := range remoteFile.Progress() {
_ = s.Send(&rpc.TransferProgress{
Total: remoteFile.Size(),
Sent: sent,
})
}
}()
io.Copy(remoteFile, localFile)
localFile.Close()
remoteFile.Close()
return nil
}
func (fs *FS) Download(req *rpc.TransferRequest, s rpc.DRPCFS_DownloadStream) error {
fs.updateFS()
localFile, err := os.Create(req.Destination)
if err != nil {
return err
}
remoteFile, err := fs.fs.Open(req.Source)
if err != nil {
return err
}
defer localFile.Close()
defer remoteFile.Close() defer remoteFile.Close()
go func() { go func() {
// For every progress event // For every progress event
for sent := range remoteFile.Progress() { for sent := range remoteFile.Progress() {
// Encode event on connection _ = s.Send(&rpc.TransferProgress{
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: types.FSTransferProgress{
Type: types.FSTypeWrite,
Total: remoteFile.Size(), Total: remoteFile.Size(),
Sent: sent, Sent: sent,
},
}) })
} }
}() }()
json.NewEncoder(conn).Encode(types.Response{Type: req.Type}) _, err = io.Copy(localFile, remoteFile)
io.Copy(remoteFile, localFile)
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: types.FSTransferProgress{
Type: types.FSTypeWrite,
Total: remoteFile.Size(),
Sent: remoteFile.Size(),
Done: true,
},
})
case types.FSTypeRead:
if len(reqData.Files) != 2 {
connErr(conn, req.Type, nil, "Read FS command requires a path to the file to read")
break
}
localFile, err := os.Create(reqData.Files[0])
if err != nil { if err != nil {
connErr(conn, req.Type, err, "Error creating local file") return err
break
} }
defer localFile.Close()
remoteFile, err := fs.Open(reqData.Files[1]) return nil
if err != nil {
connErr(conn, req.Type, err, "Error opening remote file")
break
}
defer remoteFile.Close()
go func() {
// For every progress event
for rcvd := range remoteFile.Progress() {
// Encode event on connection
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: types.FSTransferProgress{
Type: types.FSTypeRead,
Total: remoteFile.Size(),
Sent: rcvd,
},
})
}
}()
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
io.Copy(localFile, remoteFile)
json.NewEncoder(conn).Encode(types.Response{
Type: req.Type,
Value: types.FSTransferProgress{
Type: types.FSTypeRead,
Total: remoteFile.Size(),
Sent: remoteFile.Size(),
Done: true,
},
})
}
case types.ReqTypeWeatherUpdate:
// Send weather update signal
sendWeatherCh <- struct{}{}
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
case types.ReqTypeCancel:
if req.Data == nil {
connErr(conn, req.Type, nil, "No data provided. Cancel request requires request ID string as data.")
continue
}
reqID, ok := req.Data.(string)
if !ok {
connErr(conn, req.Type, nil, "Invalid data. Cancel request required request ID string as data.")
}
// Stop notifications
done.Done(reqID)
json.NewEncoder(conn).Encode(types.Response{Type: req.Type})
default:
connErr(conn, req.Type, nil, fmt.Sprintf("Unknown request type %d", req.Type))
}
}
} }
func connErr(conn net.Conn, resType int, err error, msg string) { func (fs *FS) LoadResources(req *rpc.PathRequest, s rpc.DRPCFS_LoadResourcesStream) error {
var res types.Response resFl, err := os.Open(req.Path)
// If error exists, add to types.Response, otherwise don't
if err != nil { if err != nil {
log.Error().Err(err).Msg(msg) return err
res = types.Response{Message: fmt.Sprintf("%s: %s", msg, err)} }
progCh, err := infinitime.LoadResources(resFl, fs.fs)
if err != nil {
return err
}
for evt := range progCh {
err = s.Send(&rpc.ResourceLoadProgress{
Name: evt.Name,
Total: evt.Total,
Sent: evt.Sent,
Operation: rpc.ResourceLoadProgress_Operation(evt.Operation),
})
if err != nil {
return err
}
}
return nil
}
func (fs *FS) updateFS() {
if fs.fs == nil || updateFS {
// Get new FS
newFS, err := fs.dev.FS()
if err != nil {
log.Warn("Error updating BLE filesystem").Err(err).Send()
} else { } else {
log.Error().Msg(msg) // Set FS pointer to new FS
res = types.Response{Message: msg, Type: resType} fs.fs = newFS
// Reset updateFS
updateFS = false
} }
res.Error = true
// Encode error to connection
json.NewEncoder(conn).Encode(res)
}
// 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
} }
-2
View File
@@ -35,8 +35,6 @@ func (ct *ChineseTranslit) Transliterate(s string) string {
// Reset temporary buffer // Reset temporary buffer
tmpBuf.Reset() tmpBuf.Reset()
} }
// Write character to output
outBuf.WriteRune(char)
} }
} }
// If buffer contains characters // If buffer contains characters
+1 -1
View File
@@ -301,7 +301,7 @@ var Transliterators = map[string]Transliterator{
"Ð", "D", "Ð", "D",
"ð", "d", "ð", "d",
}, },
"Czeck": Map{ "Czech": Map{
"ř", "r", "ř", "r",
"ě", "e", "ě", "e",
"ý", "y", "ý", "y",
+47
View File
@@ -0,0 +1,47 @@
package translit
import "testing"
func TestTransliterate(t *testing.T) {
type testCase struct {
name string
input string
expected string
}
cases := []testCase{
{"eASCII", "œª°«»", `oeao""`},
{"Scandinavian", "ÆæØøÅå", "AeaeOeoeAaaa"},
{"German", "äöüÄÖÜßẞ", "aeoeueAeOeUessSS"},
{"Hebrew", "אבגדהוזחטיכלמנסעפצקרשתףץךםן", "abgdhuzkhtyclmns'ptskrshthftschmn"},
{"Greek", "αάβγδεέζηήθιίϊΐκλμνξοόπρσςτυύϋΰφχψωώΑΆΒΓΔΕΈΖΗΉΘΙΊΪΚΛΜΝΞΟΌΠΡΣΤΥΎΫΦΧΨΩΏ", "aavgdeeziithiiiiklmnksooprsstyyyyfchpsooAABGDEEZIIThIIIKLMNKsOOPRSTYYYFChPsOO"},
{"Russian", "Ёё", "Йoйo"},
{"Ukranian", "ґєіїҐЄІЇ", "ghjeijiGhJeIJI"},
{"Arabic", "ابتثجحخدذرزسشصضطظعغفقكلمنهويىﺓآئإؤأء٠١٢٣٤٥٦٧٨٩", "abtthj75dthrzssh99'66'33'fqklmnhwya2222220123456789"},
{"Farsi", "پچژکگی\u200c؟٪؛،۱۲۳۴۵۶۷۸۹۰»«َُِّ", "pchzhkgy ?%;:1234567890<>eao"},
{"Polish", "Łł", "Ll"},
{"Lithuanian", "ąčęėįšųūž", "aceeisuuz"},
{"Estonian", "äÄöõÖÕüÜ", "aAooOOuU"},
{"Icelandic", "ÞþÐð", "ThthDd"},
{"Czech", "řěýáíéóúůďťň", "reyaieouudtn"},
{"French", "àâéèêëùüÿç", "aaeeeeuuyc"},
{"Romanian", "ăĂâÂîÎșȘțȚşŞţŢ„”", `aAaAiIsStTsStT""`},
{
"Emoji",
"😂🤣😊☺️😌😃😁😋😛😜🙃😎😶😩😕😏💜💖💗❤️💕💞💘💓💚💙💟❣️💔😱😮😯😝🤔😔😍😘😚😙👍👌🤞✌️🌄🌞🤗🌻🥱🙄🔫🥔😬✨🌌💀😅😢💯🔥😉😴💤",
`XDXD:):):):D:D:P:P;P(:8):#-_-:(:J<3<3<3<3<3<3<3<3<3<3<3<3!</3D::O:OxP',:-|:|:*:*:*:*:thumbsup::ok_hand::crossed_fingers::victory_hand::sunrise_over_mountains::sun_with_face::hugging_face::sunflower::yawning_face::face_with_rolling_eyes::gun::potato::E******8-X':D:'(:100::fire:;):zzz::zzz:`,
},
{"Korean", "\ucc2c\ubbf8\ub97c \uc637\uc744 \uc5bc\ub9c8\ub098 \ud48d\ubd80\ud558\uac8c \uccad\ucd98\uc774 \uc5ed\uc0ac\ub97c", "chanmireul oteul eolmana pungbuhage cheongchuni yeoksareul"},
{"Chinese", "\u81e8\u8cc7\u601d\u7531\u554f\u805e\u907f\u6c5a\u81f3\u5c0e\u524d\u99ac\u59cb\u4e00\u79fb\u3002", "lin zi si you wen wen bi wu zhi dao qian ma shi yi yi"},
{"Armenian", "\u0531\u0532\u0533\u0534\u0535\u0536\u0537\u0538\u0539\u053a\u053b\u053c\u053d\u053e\u053f\u0540\u0541\u0542\u0543\u0544\u0545\u0546\u0547\u0548\u0549\u054a\u054b\u054c\u054d\u054e\u054f\u0550\u0551\u0552\u0553\u0554\u0555\u0556\u0561\u0562\u0563\u0564\u0565\u0566\u0567\u0568\u0569\u056a\u056b\u056c\u056d\u056e\u056f\u0570\u0571\u0572\u0573\u0574\u0575\u0576\u0577\u0578\u0579\u057a\u057b\u057c\u057d\u057e\u057f\u0580\u0581\u0582\u0583\u0584\u0585\u0586\u0587", "ABGDEZEYTJILXCKHDzXCMYNShVoChPJRSVTRCPQOFabgdezeytjilxckhdzxcmynsochpjrsvtrcpqofev"},
}
for _, tCase := range cases {
t.Run(tCase.name, func(t *testing.T) {
out := Transliterate(tCase.input, tCase.name)
if out != tCase.expected {
t.Errorf("Expected %q, got %q", tCase.expected, out)
}
})
}
}
+8
View File
@@ -0,0 +1,8 @@
package main
import _ "embed"
//go:generate scripts/gen-version.sh
//go:embed version.txt
var version string
-1
View File
@@ -1 +0,0 @@
unknown
+16
View File
@@ -0,0 +1,16 @@
package main
import (
"sync"
"go.elara.ws/logger/log"
)
type WaitGroup struct {
*sync.WaitGroup
}
func (wg WaitGroup) Done(c string) {
log.Info("Component stopped").Str("name", c).Send()
wg.WaitGroup.Done()
}
+41 -18
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
@@ -8,11 +9,12 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log" "go.elara.ws/infinitime"
"go.arsenm.dev/infinitime" "go.elara.ws/infinitime/weather"
"go.arsenm.dev/infinitime/weather" "go.elara.ws/logger/log"
) )
// METResponse represents a response from // METResponse represents a response from
@@ -60,27 +62,41 @@ type OSMData []struct {
var sendWeatherCh = make(chan struct{}, 1) var sendWeatherCh = make(chan struct{}, 1)
func initWeather(dev *infinitime.Device) error { func sleepCtx(ctx context.Context, d time.Duration) {
select {
case <-time.After(d):
case <-ctx.Done():
}
}
func initWeather(ctx context.Context, wg WaitGroup, dev *infinitime.Device) error {
if !k.Bool("weather.enabled") { if !k.Bool("weather.enabled") {
return nil return nil
} }
// Get location based on string in config // Get location based on string in config
lat, lon, err := getLocation(k.String("weather.location")) lat, lon, err := getLocation(ctx, k.String("weather.location"))
if err != nil { if err != nil {
return err return err
} }
timer := time.NewTimer(time.Hour) timer := time.NewTimer(time.Hour)
wg.Add(1)
go func() { go func() {
defer wg.Done("weather")
for { for {
_, ok := <-ctx.Done()
if !ok {
return
}
// Attempt to get weather // Attempt to get weather
data, err := getWeather(lat, lon) data, err := getWeather(ctx, lat, lon)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("Error getting weather data") log.Warn("Error getting weather data").Err(err).Send()
// Wait 15 minutes before retrying // Wait 15 minutes before retrying
time.Sleep(15 * time.Minute) sleepCtx(ctx, 15*time.Minute)
continue continue
} }
@@ -98,7 +114,7 @@ func initWeather(dev *infinitime.Device) error {
DewPoint: int16(round(currentData.DewPoint)), DewPoint: int16(round(currentData.DewPoint)),
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error adding temperature event") log.Error("Error adding temperature event").Err(err).Send()
} }
// Add precipitation event // Add precipitation event
@@ -111,7 +127,7 @@ func initWeather(dev *infinitime.Device) error {
Amount: uint8(round(current.Data.NextHour.Details.PrecipitationAmount)), Amount: uint8(round(current.Data.NextHour.Details.PrecipitationAmount)),
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error adding precipitation event") log.Error("Error adding precipitation event").Err(err).Send()
} }
// Add wind event // Add wind event
@@ -126,7 +142,7 @@ func initWeather(dev *infinitime.Device) error {
DirectionMax: uint8(round(currentData.WindDirection)), DirectionMax: uint8(round(currentData.WindDirection)),
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error adding wind event") log.Error("Error adding wind event").Err(err).Send()
} }
// Add cloud event // Add cloud event
@@ -138,7 +154,7 @@ func initWeather(dev *infinitime.Device) error {
Amount: uint8(round(currentData.CloudAreaFraction)), Amount: uint8(round(currentData.CloudAreaFraction)),
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error adding clouds event") log.Error("Error adding clouds event").Err(err).Send()
} }
// Add humidity event // Add humidity event
@@ -150,7 +166,7 @@ func initWeather(dev *infinitime.Device) error {
Humidity: uint8(round(currentData.RelativeHumidity)), Humidity: uint8(round(currentData.RelativeHumidity)),
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error adding humidity event") log.Error("Error adding humidity event").Err(err).Send()
} }
// Add pressure event // Add pressure event
@@ -162,7 +178,7 @@ func initWeather(dev *infinitime.Device) error {
Pressure: int16(round(currentData.AirPressure)), Pressure: int16(round(currentData.AirPressure)),
}) })
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error adding pressure event") log.Error("Error adding pressure event").Err(err).Send()
} }
// Reset timer to 1 hour // Reset timer to 1 hour
@@ -173,6 +189,8 @@ func initWeather(dev *infinitime.Device) error {
select { select {
case <-timer.C: case <-timer.C:
case <-sendWeatherCh: case <-sendWeatherCh:
case <-ctx.Done():
return
} }
} }
}() }()
@@ -181,10 +199,14 @@ func initWeather(dev *infinitime.Device) error {
// getLocation returns the latitude and longitude // getLocation returns the latitude and longitude
// given a location // given a location
func getLocation(loc string) (lat, lon float64, err error) { func getLocation(ctx context.Context, loc string) (lat, lon float64, err error) {
// Create request URL and perform GET request // Create request URL and perform GET request
reqURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search.php?q=%s&format=jsonv2", url.QueryEscape(loc)) reqURL := fmt.Sprintf("https://nominatim.openstreetmap.org/search.php?q=%s&format=jsonv2", url.QueryEscape(loc))
res, err := http.Get(reqURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return
}
res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return return
} }
@@ -218,9 +240,10 @@ func getLocation(loc string) (lat, lon float64, err error) {
} }
// getWeather gets weather data given a latitude and longitude // getWeather gets weather data given a latitude and longitude
func getWeather(lat, lon float64) (*METResponse, error) { func getWeather(ctx context.Context, lat, lon float64) (*METResponse, error) {
// Create new GET request // Create new GET request
req, err := http.NewRequest( req, err := http.NewRequestWithContext(
ctx,
http.MethodGet, http.MethodGet,
fmt.Sprintf( fmt.Sprintf(
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f", "https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=%.2f&lon=%.2f",