From d98cd89c48dc62edde9a82c56badd2ad74e925c6 Mon Sep 17 00:00:00 2001 From: Markus Zehnder Date: Thu, 28 Aug 2025 09:03:30 +0200 Subject: [PATCH] refactor: project structure (#9) Split up project into multiple crates and use a Cargo workspace. --- .idea/misc.xml | 1 - .idea/runConfigurations/Run_demo.xml | 2 +- .idea/runConfigurations/Run_demo_DEV.xml | 2 +- .idea/runConfigurations/Run_sensor_panel.xml | 2 +- .../Run_sensor_panel_DEV.xml | 2 +- .idea/runConfigurations/Run_sysinfo.xml | 2 +- .../runConfigurations/Run_sysinfo_repeat.xml | 2 +- .idea/runConfigurations/clippy_sysinfo.xml | 19 -- .idea/vcs.xml | 2 +- AOOstar-rs.iml | 10 +- CHANGELOG.md | 4 + Cargo.lock | 117 ++++---- Cargo.toml | 49 +--- README.md | 24 +- cfg/sensors/values.txt | 6 +- crates/asterctl-lcd/Cargo.toml | 18 ++ crates/asterctl-lcd/LICENSE-APACHE | 1 + crates/asterctl-lcd/LICENSE-MIT | 1 + crates/asterctl-lcd/README.md | 17 ++ .../asterctl-lcd/src/aoo_screen.rs | 7 +- .../asterctl-lcd/src/fake_serialport.rs | 18 +- crates/asterctl-lcd/src/lib.rs | 53 ++++ crates/asterctl/Cargo.toml | 30 ++ crates/asterctl/LICENSE-APACHE | 1 + crates/asterctl/LICENSE-MIT | 1 + crates/asterctl/README.md | 3 + {img => crates/asterctl/src/bin}/aybabtu.png | Bin crates/asterctl/src/bin/demo.rs | 262 ++++++++++++++++++ {src => crates/asterctl/src}/cfg.rs | 0 {src => crates/asterctl/src}/font.rs | 2 +- {src => crates/asterctl/src}/format_value.rs | 2 +- {src => crates/asterctl/src}/img.rs | 42 +-- crates/asterctl/src/lib.rs | 14 + {src => crates/asterctl/src}/main.rs | 197 +------------ {src => crates/asterctl/src}/render.rs | 2 +- {src => crates/asterctl/src}/sensors.rs | 0 crates/sysinfo/Cargo.toml | 19 ++ crates/sysinfo/LICENSE-APACHE | 1 + crates/sysinfo/LICENSE-MIT | 1 + crates/sysinfo/README.md | 47 ++++ .../sysinfo.rs => crates/sysinfo/src/main.rs | 3 + {doc => docs}/README.md | 0 {doc => docs}/asterctl.md | 37 ++- {doc => docs}/img/lcd_off.png | Bin {doc => docs}/img/lcd_on.png | Bin {doc => docs}/img/mode4_pic.png | Bin {doc => docs}/img/progress.png | Bin {doc => docs}/img/progress_circle.png | Bin {doc => docs}/img/send_image.png | Bin {doc => docs}/img/sensor_mode1.png | Bin {doc => docs}/img/sensor_mode1_background.png | Bin {doc => docs}/img/sensor_mode2.jpg | Bin {doc => docs}/img/sensor_mode2_background.jpg | Bin {doc => docs}/img/sensor_mode3.png | Bin {doc => docs}/img/sensor_mode3_background.png | Bin {doc => docs}/img/sensor_mode4.png | Bin {doc => docs}/img/sensor_mode4_background.png | Bin {doc => docs}/img/sensor_panel-01.png | Bin {doc => docs}/img/sensor_panel-02.png | Bin {doc => docs}/lcd_protocol.md | 0 {doc => docs}/sensor_custom_panel.md | 0 {doc => docs}/sensor_data_shell.md | 0 {doc => docs}/sensor_data_sysinfo.md | 8 +- {doc => docs}/sensor_data_txt_file.md | 0 {doc => docs}/sensor_mode1_text.md | 0 {doc => docs}/sensor_mode2_fan.md | 0 {doc => docs}/sensor_mode3_progress.md | 0 {doc => docs}/sensor_mode4_pointer.md | 0 {doc => docs}/sensor_panels.md | 0 {doc => docs}/shell_commands.md | 0 linux/lcd-on.service | 42 +++ 71 files changed, 672 insertions(+), 401 deletions(-) delete mode 100644 .idea/runConfigurations/clippy_sysinfo.xml create mode 100644 crates/asterctl-lcd/Cargo.toml create mode 120000 crates/asterctl-lcd/LICENSE-APACHE create mode 120000 crates/asterctl-lcd/LICENSE-MIT create mode 100644 crates/asterctl-lcd/README.md rename src/display.rs => crates/asterctl-lcd/src/aoo_screen.rs (98%) rename src/dummy_serialport.rs => crates/asterctl-lcd/src/fake_serialport.rs (92%) create mode 100644 crates/asterctl-lcd/src/lib.rs create mode 100644 crates/asterctl/Cargo.toml create mode 120000 crates/asterctl/LICENSE-APACHE create mode 120000 crates/asterctl/LICENSE-MIT create mode 100644 crates/asterctl/README.md rename {img => crates/asterctl/src/bin}/aybabtu.png (100%) create mode 100644 crates/asterctl/src/bin/demo.rs rename {src => crates/asterctl/src}/cfg.rs (100%) rename {src => crates/asterctl/src}/font.rs (95%) rename {src => crates/asterctl/src}/format_value.rs (98%) rename {src => crates/asterctl/src}/img.rs (77%) create mode 100644 crates/asterctl/src/lib.rs rename {src => crates/asterctl/src}/main.rs (60%) rename {src => crates/asterctl/src}/render.rs (99%) rename {src => crates/asterctl/src}/sensors.rs (100%) create mode 100644 crates/sysinfo/Cargo.toml create mode 120000 crates/sysinfo/LICENSE-APACHE create mode 120000 crates/sysinfo/LICENSE-MIT create mode 100644 crates/sysinfo/README.md rename src/bin/sysinfo.rs => crates/sysinfo/src/main.rs (99%) rename {doc => docs}/README.md (100%) rename {doc => docs}/asterctl.md (93%) rename {doc => docs}/img/lcd_off.png (100%) rename {doc => docs}/img/lcd_on.png (100%) rename {doc => docs}/img/mode4_pic.png (100%) rename {doc => docs}/img/progress.png (100%) rename {doc => docs}/img/progress_circle.png (100%) rename {doc => docs}/img/send_image.png (100%) rename {doc => docs}/img/sensor_mode1.png (100%) rename {doc => docs}/img/sensor_mode1_background.png (100%) rename {doc => docs}/img/sensor_mode2.jpg (100%) rename {doc => docs}/img/sensor_mode2_background.jpg (100%) rename {doc => docs}/img/sensor_mode3.png (100%) rename {doc => docs}/img/sensor_mode3_background.png (100%) rename {doc => docs}/img/sensor_mode4.png (100%) rename {doc => docs}/img/sensor_mode4_background.png (100%) rename {doc => docs}/img/sensor_panel-01.png (100%) rename {doc => docs}/img/sensor_panel-02.png (100%) rename {doc => docs}/lcd_protocol.md (100%) rename {doc => docs}/sensor_custom_panel.md (100%) rename {doc => docs}/sensor_data_shell.md (100%) rename {doc => docs}/sensor_data_sysinfo.md (82%) rename {doc => docs}/sensor_data_txt_file.md (100%) rename {doc => docs}/sensor_mode1_text.md (100%) rename {doc => docs}/sensor_mode2_fan.md (100%) rename {doc => docs}/sensor_mode3_progress.md (100%) rename {doc => docs}/sensor_mode4_pointer.md (100%) rename {doc => docs}/sensor_panels.md (100%) rename {doc => docs}/shell_commands.md (100%) create mode 100644 linux/lcd-on.service diff --git a/.idea/misc.xml b/.idea/misc.xml index dd8986a..e5db904 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/.idea/runConfigurations/Run_demo.xml b/.idea/runConfigurations/Run_demo.xml index 6eeb8af..7e05cc7 100644 --- a/.idea/runConfigurations/Run_demo.xml +++ b/.idea/runConfigurations/Run_demo.xml @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/AOOstar-rs.iml b/AOOstar-rs.iml index 2fecef3..94c628b 100644 --- a/AOOstar-rs.iml +++ b/AOOstar-rs.iml @@ -1,12 +1,16 @@ - + - + + + + + + - \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a482e78..bc1b0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ _Changes in the next release_ ### Added - Simple sensor panel with a file-based data source (#6) +- Initial support for fan-, progress-, & pointer-sensors (#8) + +### Changed +- Project structure using a Cargo workspace --- diff --git a/Cargo.lock b/Cargo.lock index 5774350..a912d6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,31 +98,6 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" -[[package]] -name = "aoostar-rs" -version = "0.1.0" -dependencies = [ - "ab_glyph", - "anyhow", - "bytes", - "clap", - "env_logger", - "image", - "imageproc", - "itertools 0.14.0", - "log", - "notify", - "once_cell", - "regex", - "rstest", - "serde", - "serde_json", - "serde_repr", - "serialport", - "sysinfo", - "tempfile", -] - [[package]] name = "approx" version = "0.5.1" @@ -155,6 +130,37 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asterctl" +version = "0.1.0" +dependencies = [ + "ab_glyph", + "anyhow", + "asterctl-lcd", + "clap", + "env_logger", + "image", + "imageproc", + "log", + "notify", + "once_cell", + "rstest", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "asterctl-lcd" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "image", + "log", + "serialport", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -198,9 +204,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bitstream-io" @@ -240,9 +246,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", @@ -651,9 +657,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -665,7 +671,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "inotify-sys", "libc", ] @@ -756,9 +762,9 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", @@ -986,7 +992,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "fsevent-sys", "inotify", "kqueue", @@ -1104,7 +1110,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] @@ -1384,9 +1390,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -1396,9 +1402,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -1407,9 +1413,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "relative-path" @@ -1467,7 +1473,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys", @@ -1570,11 +1576,11 @@ dependencies = [ [[package]] name = "serialport" -version = "4.7.2" +version = "4.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb0bc984f6af6ef8bab54e6cf2071579ee75b9286aa9f2319a0d220c28b0a2b" +checksum = "2acaf3f973e8616d7ceac415f53fc60e190b2a686fbcf8d27d0256c741c5007b" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", "cfg-if", "core-foundation", "core-foundation-sys", @@ -1650,6 +1656,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "itertools 0.14.0", + "log", + "regex", + "sysinfo 0.37.0", + "tempfile", +] + [[package]] name = "sysinfo" version = "0.37.0" @@ -2221,9 +2240,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -2234,7 +2253,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.9.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1df2530..0c6be52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,45 +1,10 @@ -[package] -name = "aoostar-rs" -version = "0.1.0" -edition = "2024" +[workspace] +members = ["crates/*"] +resolver = "3" + +[workspace.package] rust-version = "1.88" +edition = "2024" authors = ["Markus Zehnder"] license = "MIT or Apache-2.0" - -[profile.release] -strip = true # Automatically strip symbols from the binary. - -[[bin]] -name = "asterctl" -path = "src/main.rs" - -[[bin]] -name = "sysinfo" -path = "src/bin/sysinfo.rs" -required-features = ["sysinfo"] - -[features] -sysinfo = ["dep:sysinfo"] - -[dependencies] -anyhow = "1.0.98" -bytes = "1.10.1" -clap = { version = "4.5.42", features = ["derive"] } -serialport = "4.7.2" -image = "0.25.6" -imageproc = { version = "0.25.0", default-features = false } -ab_glyph = { version = "0.2.31", default-features = false, features = ["std"] } -log = "0.4.27" -env_logger = "0.11.8" -notify = "8.2.0" -regex = "1.11" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.142" -serde_repr = "0.1.20" -once_cell = "1.21.3" -sysinfo = { version = "0.37.0", optional = true } -itertools = "0.14" -tempfile = "3" - -[dev-dependencies] -rstest = "0.26" +repository = "https://github.com/zehnm/aoostar-rs" diff --git a/README.md b/README.md index eb61424..fbba75d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# AOOSTAR WTR MAX Screen Control +# AOOSTAR WTR MAX / GEM12+ PRO Screen Control Reverse engineering the [AOOSTAR WTR MAX](https://aoostar.com/products/aoostar-wtr-max-amd-r7-pro-8845hs-11-bays-mini-pc) display protocol, with a proof-of-concept application written in Rust. -This project should also support the GEM12+ PRO device. +It has only been tested on the WTR MAX, but should also support the GEM12+ PRO device. **Disclaimer: ‼️ EXPERIMENTAL — use at your own risk ‼️** @@ -10,6 +10,8 @@ This project should also support the GEM12+ PRO device. > There is no official documentation available; > all display control commands have been reverse engineered from the original AOOSTAR-X software. +Even though this software works fine **for me**, I cannot guarantee that it is risk-free: + - It may or may not work. - It could crash the display firmware, requiring a power cycle. - It could even brick the display firmware. @@ -20,14 +22,14 @@ Note: Multiple attempts to contact the manufacturer for documentation have recei With that out of the way, on to the fun stuff! -**See [Linux shell commands](doc/shell_commands.md) on how to switch off the display with standard Linux commands!** +**See [Linux shell commands](docs/shell_commands.md) on how to switch off the display with standard Linux commands!** See [releases](https://github.com/zehnm/aoostar-rs/releases) for binary Linux x64 releases and [Linux systemd Service](linux/) on how to automatically switch off the LCD at boot up. A Debian package for easy installation is planned for the future! ## Reverse Engineering -Reverse engineered LCD commands: [doc/lcd_protocol.md](doc/lcd_protocol.md) +Reverse engineered LCD commands: [docs/lcd_protocol.md](docs/lcd_protocol.md) ### Motivation @@ -50,7 +52,7 @@ The display remains on continuously (24×7) if the official software is not runn - [x] Reverse engineer the LCD serial protocol to provide open screen software. - Utilize the official AOOSTAR-X display software by sniffing USB communication, using `strace`, and decompiling the Python app. - [x] Document known commands so clients in other programming languages can be written. -- [ ] Eventually, create a Rust crate for easy integration into other Rust applications. +- [ ] Eventually, publish a Rust crate for easy integration into other Rust applications. **Out of scope:** @@ -63,7 +65,9 @@ The display remains on continuously (24×7) if the official software is not runn - Control the AOOSTAR WTR MAX and GEM12+ PRO second screen from Linux. - Switch the display on or off. - Display images (with automatic scaling and partial update support). -- Proof-of-concept demo for drawing shapes and text. +- Render dynamic sensor panels defined from the AOOSTAR-X software. + - Update sensor values from simple text files. + - Rotate through multiple panels in a defined interval. - USB device/serial port selection. ## Setup @@ -87,21 +91,19 @@ cd aoostar-rs ### Build -A release build is highly recommended, as it significantly improves graphics performance: +A release build is highly recommended, as it significantly improves graphic rendering performance: ```shell -cargo build --release --bins --all-features +cargo build --release ``` -The `--bins` option builds the main `asterctl` app and all other tools. - ### Install See [Linux systemd Service](linux/) on how to automatically switch off the LCD at boot up. ## Usage -See [asterctl documentation](doc/README.md) for more information or run `asterctl --help` for available command line options. +See [asterctl documentation](docs/README.md) for more information or run `asterctl --help` for available command line options. ## Contributing diff --git a/cfg/sensors/values.txt b/cfg/sensors/values.txt index beab7fc..306811a 100644 --- a/cfg/sensors/values.txt +++ b/cfg/sensors/values.txt @@ -1,5 +1,5 @@ cpu_temperature: 65 -cpu_percent: 98 +cpu_percent: 47.7 memory_usage: 77 memory_Temperature: 48 net_ip_address: 146.56.182.244 @@ -17,9 +17,9 @@ storage_ssd[1]['used']: 18 storage_ssd[2]['temperature']: 33 storage_ssd[2]['used']: 19 storage_ssd[3]['temperature']: 34 -storage_ssd[3]['used']: 20 +storage_ssd[3]['used']: 35 storage_ssd[4]['temperature']: 35 -storage_ssd[4]['used']: 21 +storage_ssd[4]['used']: 80 storage_hdd[0]['temperature']: 36 storage_hdd[0]['used']: 22 storage_hdd[1]['temperature']: 37 diff --git a/crates/asterctl-lcd/Cargo.toml b/crates/asterctl-lcd/Cargo.toml new file mode 100644 index 0000000..4e9897c --- /dev/null +++ b/crates/asterctl-lcd/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "asterctl-lcd" +version = "0.1.0" +description = "AOOSTAR WTR MAX / GEM12+ PRO screen protocol" + +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +anyhow = "1.0.98" +bytes = "1.10.1" +# TODO make image an optional feature +image = "0.25.6" +log = "0.4.27" +serialport = "4.7.3" diff --git a/crates/asterctl-lcd/LICENSE-APACHE b/crates/asterctl-lcd/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/asterctl-lcd/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/asterctl-lcd/LICENSE-MIT b/crates/asterctl-lcd/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/asterctl-lcd/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/asterctl-lcd/README.md b/crates/asterctl-lcd/README.md new file mode 100644 index 0000000..73e591c --- /dev/null +++ b/crates/asterctl-lcd/README.md @@ -0,0 +1,17 @@ +# AOOSTAR WTR MAX / GEM12+ PRO UART Screen Protocol + +Reverse engineered [AOOSTAR WTR MAX](https://aoostar.com/products/aoostar-wtr-max-amd-r7-pro-8845hs-11-bays-mini-pc) +UART display protocol, written in Rust. +This project should also support the GEM12+ PRO device. + +- [LCD Protocol](../../docs/lcd_protocol.md) +- See [README](../../README.md) for more information about the `asterctl` screen control tool. + +## Display Information + +- **Resolution:** 960 × 376 +- **Manufacturer:** Synwit +- **Connected over USB UART** with a proprietary serial communication protocol: + - **USB device ID:** `416:90A1` (as shown by `lsusb`) + - **Linux device (example on Debian):** `/dev/ttyACM0` + - **1,500,000 baud**, 8N1 (likely ignored; actual USB transfer speed is much higher) diff --git a/src/display.rs b/crates/asterctl-lcd/src/aoo_screen.rs similarity index 98% rename from src/display.rs rename to crates/asterctl-lcd/src/aoo_screen.rs index bbd6a2f..df09e10 100644 --- a/src/display.rs +++ b/crates/asterctl-lcd/src/aoo_screen.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder -use crate::dummy_serialport::DummySerialPort; -use crate::img::ToRgb565; +use crate::FakeSerialPort; +use crate::ToRgb565; + use anyhow::{Context, anyhow}; use bytes::{BufMut, BytesMut}; use log::{debug, error, info, warn}; @@ -68,7 +69,7 @@ impl AooScreenBuilder { /// Simulate the LCD device. No real device or serial port is required. pub fn simulate(self) -> anyhow::Result { Ok(AooScreen { - port: Some(Box::new(DummySerialPort::new())), + port: Some(Box::new(FakeSerialPort::new())), enable_cache: self.enable_cache.unwrap_or(true), prev_frame: None, no_init_check: self.no_init_check.unwrap_or(false), diff --git a/src/dummy_serialport.rs b/crates/asterctl-lcd/src/fake_serialport.rs similarity index 92% rename from src/dummy_serialport.rs rename to crates/asterctl-lcd/src/fake_serialport.rs index 849d79b..c7ee3be 100644 --- a/src/dummy_serialport.rs +++ b/crates/asterctl-lcd/src/fake_serialport.rs @@ -5,7 +5,7 @@ use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPort, StopBit use std::thread::sleep; use std::time::Duration; -pub struct DummySerialPort { +pub struct FakeSerialPort { baud_rate: u32, data_bits: DataBits, flow_control: FlowControl, @@ -14,8 +14,14 @@ pub struct DummySerialPort { timeout: Duration, } -impl DummySerialPort { - pub fn new() -> DummySerialPort { +impl Default for FakeSerialPort { + fn default() -> Self { + Self::new() + } +} + +impl FakeSerialPort { + pub fn new() -> FakeSerialPort { Self { baud_rate: 1_500_000, data_bits: DataBits::Eight, @@ -27,14 +33,14 @@ impl DummySerialPort { } } -impl std::io::Read for DummySerialPort { +impl std::io::Read for FakeSerialPort { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { buf[0] = b'A'; Ok(1) } } -impl std::io::Write for DummySerialPort { +impl std::io::Write for FakeSerialPort { fn write(&mut self, buf: &[u8]) -> std::io::Result { // just some approximation, additional overhead like flushing etc is not considered let byte_rate = @@ -49,7 +55,7 @@ impl std::io::Write for DummySerialPort { } } -impl SerialPort for DummySerialPort { +impl SerialPort for FakeSerialPort { fn name(&self) -> Option { Some("Dummy Serial".into()) } diff --git a/crates/asterctl-lcd/src/lib.rs b/crates/asterctl-lcd/src/lib.rs new file mode 100644 index 0000000..c6fb9c6 --- /dev/null +++ b/crates/asterctl-lcd/src/lib.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder + +#![forbid(non_ascii_idents)] +#![deny(unsafe_code)] + +use bytes::{BufMut, BytesMut}; +use image::{RgbImage, RgbaImage}; + +mod aoo_screen; +mod fake_serialport; + +pub use aoo_screen::{AooScreen, AooScreenBuilder, DISPLAY_SIZE}; +pub use fake_serialport::FakeSerialPort; + +/// Trait definition to get a RGB 565 representation from a source image. +pub trait ToRgb565 { + /// Get an RGB 565 representation of the image in little endian format. + fn to_rgb565_le(&self) -> BytesMut; + + /// Convert a single RGB 888 pixel to 16 bit RGB 565 format. + fn convert_rgb(&self, r: u8, g: u8, b: u8) -> u16 { + ((r & 248) as u16) << 8 | ((g & 252) as u16) << 3 | ((b as u16) >> 3) + } +} + +// TODO quick & dirty approach for converting RgbImage & RgbaImage to RGB 565. +// There should be a more generic way, maybe with PixelEnumerator... +impl ToRgb565 for &RgbImage { + fn to_rgb565_le(&self) -> BytesMut { + let mut img_rgb565 = + BytesMut::with_capacity(self.width() as usize * self.height() as usize * 2); + + for (_x, _y, pixel) in self.enumerate_pixels() { + img_rgb565.put_u16_le(self.convert_rgb(pixel.0[0], pixel.0[1], pixel.0[2])); + } + + img_rgb565 + } +} + +impl ToRgb565 for &RgbaImage { + fn to_rgb565_le(&self) -> BytesMut { + let mut img_rgb565 = + BytesMut::with_capacity(self.width() as usize * self.height() as usize * 2); + + for (_x, _y, pixel) in self.enumerate_pixels() { + img_rgb565.put_u16_le(self.convert_rgb(pixel.0[0], pixel.0[1], pixel.0[2])); + } + + img_rgb565 + } +} diff --git a/crates/asterctl/Cargo.toml b/crates/asterctl/Cargo.toml new file mode 100644 index 0000000..58e414f --- /dev/null +++ b/crates/asterctl/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "asterctl" +version = "0.1.0" +description = "AOOSTAR WTR MAX Screen Control tool" +readme = "../../README.md" + +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +asterctl-lcd = { path = "../asterctl-lcd", version = "0.1.0" } + +anyhow = "1.0.98" +clap = { version = "4.5.42", features = ["derive"] } +image = "0.25.6" +imageproc = { version = "0.25.0", default-features = false } +ab_glyph = { version = "0.2.31", default-features = false, features = ["std"] } +log = "0.4.27" +env_logger = "0.11.8" +notify = "8.2.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.142" +serde_repr = "0.1.20" +once_cell = "1.21.3" + +[dev-dependencies] +rstest = "0.26" diff --git a/crates/asterctl/LICENSE-APACHE b/crates/asterctl/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/asterctl/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/asterctl/LICENSE-MIT b/crates/asterctl/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/asterctl/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/asterctl/README.md b/crates/asterctl/README.md new file mode 100644 index 0000000..510bb03 --- /dev/null +++ b/crates/asterctl/README.md @@ -0,0 +1,3 @@ +# Screen control tool for AOOSTAR WTR MAX / GEM12+ PRO + +See [README](../../README.md) in root directory for more information. diff --git a/img/aybabtu.png b/crates/asterctl/src/bin/aybabtu.png similarity index 100% rename from img/aybabtu.png rename to crates/asterctl/src/bin/aybabtu.png diff --git a/crates/asterctl/src/bin/demo.rs b/crates/asterctl/src/bin/demo.rs new file mode 100644 index 0000000..9e73ca9 --- /dev/null +++ b/crates/asterctl/src/bin/demo.rs @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder + +use asterctl::cfg; +use asterctl::font::FontHandler; +use asterctl::render::PanelRenderer; +use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE}; + +use ab_glyph::PxScale; +use clap::Parser; +use env_logger::Env; +use image::imageops::FilterType; +use image::{ImageReader, Rgb, RgbImage}; +use imageproc::drawing::{draw_line_segment_mut, draw_text_mut}; +use log::{error, info}; +use std::collections::HashMap; +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::thread::sleep; +use std::time::{Duration, Instant}; + +/// AOOSTAR WTR MAX and GEM12+ PRO screen control demo. +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Serial device, for example "/dev/cu.usbserial-AB0KOHLS". Takes priority over --usb option. + #[arg(short, long)] + device: Option, + + /// USB serial UART "vid:pid" in hex notation (lsusb output). Default: 416:90A1 + #[arg(short, long)] + usb: Option, + + /// AOOSTAR-X json configuration file to parse. + /// + /// The configuration file will be loaded from the `config_dir` directory if no full path is + /// specified. + #[arg(short, long)] + config: Option, + + /// Configuration directory containing configuration files and background images + /// specified in the `config` file. Default: `./cfg` + #[arg(long)] + config_dir: Option, + + /// Font directory for fonts specified in the `config` file. Default: `./fonts` + #[arg(long)] + font_dir: Option, + + /// Switch off display n seconds after loading image. + #[arg(short, long)] + off_after: Option, + + /// Test mode: only write to the display without checking response. + #[arg(short, long)] + write_only: bool, + + /// Test mode: save changed images in ./out folder. + #[arg(short, long)] + save: bool, + + /// Simulate serial port for testing and development, `--device` and `--usb` options are ignored. + #[arg(long)] + simulate: bool, +} + +fn main() -> anyhow::Result<()> { + env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + + let args = Args::parse(); + + // initialize display with given UART port parameter + let mut builder = AooScreenBuilder::new(); + builder.no_init_check(args.write_only); + let mut screen = if args.simulate { + builder.simulate()? + } else if let Some(device) = args.device { + builder.open_device(&device)? + } else if let Some(usb) = args.usb { + builder.open_usb_id(&usb)? + } else { + builder.open_default()? + }; + + info!("Loading and displaying demo..."); + run_demo( + &mut screen, + args.config.as_deref(), + args.config_dir.unwrap_or_else(|| "cfg".into()), + args.font_dir.unwrap_or_else(|| "fonts".into()), + args.save, + )?; + + if let Some(off) = args.off_after { + info!("Switching off display in {off}s"); + sleep(Duration::from_secs(off as u64)); + screen.off()?; + } + + info!("Bye bye!"); + Ok(()) +} + +fn run_demo( + screen: &mut AooScreen, + config: Option<&Path>, + config_dir: PathBuf, + font_dir: PathBuf, + save_images: bool, +) -> anyhow::Result<()> { + let rgb_img = demo_image()?; + + // fill left and right side of the loaded image with neighboring pixel color + const WIDTH: u32 = 108; + let rgb_img = demo_blinds(screen, &rgb_img, WIDTH, save_images)?; + + // print demo text over background image + demo_text(screen, &rgb_img, save_images)?; + + if let Some(config) = config { + let mut cfg = if config.is_absolute() { + cfg::load_cfg(config)? + } else { + cfg::load_cfg(config_dir.join(config))? + }; + + if let Some(panel) = cfg.get_next_active_panel() { + info!("Displaying demo panel..."); + + // get sensor values from panel configuration + let mut demo_values = HashMap::new(); + for sensor in &panel.sensor { + demo_values.insert( + sensor.label.clone(), + sensor.value.clone().unwrap_or_default(), + ); + } + + let mut renderer = PanelRenderer::new(DISPLAY_SIZE, &font_dir, &config_dir); + renderer.set_save_render_img(save_images); + renderer.set_save_processed_pic(save_images); + renderer.set_save_progress_layer(save_images); + + match renderer.render(panel, &demo_values) { + Ok(image) => screen.send_image(&image)?, + Err(e) => error!("Error rendering panel '{}': {e:?}", panel.friendly_name()), + } + } else { + error!("No active panel found"); + } + } + + Ok(()) +} + +fn demo_image() -> anyhow::Result { + let reader = ImageReader::new(Cursor::new(include_bytes!("aybabtu.png"))) + .with_guessed_format() + .expect("Cursor io never fails"); + + Ok(reader + .decode()? + .resize_exact(DISPLAY_SIZE.0, DISPLAY_SIZE.1, FilterType::Lanczos3) + .to_rgb8()) +} + +fn demo_text( + screen: &mut AooScreen, + background: &RgbImage, + save_images: bool, +) -> anyhow::Result<()> { + let text = "ALL YOUR BASE ARE BELONG TO US."; + let text_upd_delay = Duration::from_millis(0); + let font = FontHandler::default_font(); + let height = 36.0; + let scale = PxScale { + x: height, + y: height, + }; + + if save_images { + fs::create_dir_all("out")?; + } + + for text_idx in 0..text.len() { + info!("Printing: {}", &text[0..text_idx + 1]); + let text_upd = Instant::now(); + let mut rgb_img = background.clone(); + draw_text_mut( + &mut rgb_img, + Rgb([118u8, 118u8, 97u8]), + 4 * 47, + 300, + scale, + &font, + &text[0..text_idx + 1], + ); + + if save_images { + rgb_img.save_with_format( + format!("out/demo_text-{text_idx}.png"), + image::ImageFormat::Png, + )?; + } + + screen.send_image(&rgb_img)?; + + let elapsed = text_upd.elapsed(); + if elapsed < text_upd_delay { + sleep(text_upd_delay - elapsed); + } + } + + Ok(()) +} + +// CPU intensive! Release build is ~ 5x faster on M1 Max +fn demo_blinds( + screen: &mut AooScreen, + background: &RgbImage, + width: u32, + save_images: bool, +) -> anyhow::Result { + let mut rgb_img = background.clone(); + + info!("Masking {width} pixels of left & right image..."); + + if save_images { + fs::create_dir_all("out")?; + } + + for y in 0..DISPLAY_SIZE.1 { + let color = *rgb_img.get_pixel(width + 1, y); + draw_line_segment_mut( + &mut rgb_img, + (0.0, y as f32), + (width as f32, y as f32), + color, + ); + let color = *rgb_img.get_pixel(DISPLAY_SIZE.0 - width - 1, y); + draw_line_segment_mut( + &mut rgb_img, + ((DISPLAY_SIZE.0 - width) as f32, y as f32), + (DISPLAY_SIZE.0 as f32, y as f32), + color, + ); + + if y % 5 == 0 { + screen.send_image(&rgb_img)?; + } + + if save_images { + rgb_img + .save_with_format(format!("out/demo_blinds-{y}.png"), image::ImageFormat::Png)?; + } + } + + screen.send_image(&rgb_img)?; + + Ok(rgb_img) +} diff --git a/src/cfg.rs b/crates/asterctl/src/cfg.rs similarity index 100% rename from src/cfg.rs rename to crates/asterctl/src/cfg.rs diff --git a/src/font.rs b/crates/asterctl/src/font.rs similarity index 95% rename from src/font.rs rename to crates/asterctl/src/font.rs index 44ec4b3..4933f3f 100644 --- a/src/font.rs +++ b/crates/asterctl/src/font.rs @@ -13,7 +13,7 @@ use std::path::PathBuf; static DEFAULT_TTF_FONT: Lazy = Lazy::new(|| { FontArc::new( - FontRef::try_from_slice(include_bytes!("../fonts/DejaVuSans.ttf")) + FontRef::try_from_slice(include_bytes!("../../../fonts/DejaVuSans.ttf")) .expect("Failed to load default font"), ) }); diff --git a/src/format_value.rs b/crates/asterctl/src/format_value.rs similarity index 98% rename from src/format_value.rs rename to crates/asterctl/src/format_value.rs index 39be26e..b3d553d 100644 --- a/src/format_value.rs +++ b/crates/asterctl/src/format_value.rs @@ -47,7 +47,7 @@ impl From> for IntegerDigits { /// # Examples /// /// ``` -/// let value = format_value("123.456", IntegerDigits::Auto, 0, "foobar"); +/// let value = asterctl::format_value("123.456", asterctl::IntegerDigits::Auto, 0, "foobar"); /// assert_eq!(value, "123foobar"); /// ``` pub fn format_value( diff --git a/src/img.rs b/crates/asterctl/src/img.rs similarity index 77% rename from src/img.rs rename to crates/asterctl/src/img.rs index 5b42507..b52986e 100644 --- a/src/img.rs +++ b/crates/asterctl/src/img.rs @@ -3,9 +3,8 @@ //! Image helper functions. -use bytes::{BufMut, BytesMut}; use image::imageops::FilterType; -use image::{DynamicImage, GenericImageView, ImageBuffer, ImageReader, RgbImage, Rgba, RgbaImage}; +use image::{DynamicImage, GenericImageView, ImageBuffer, ImageReader, Rgba, RgbaImage}; use imageproc::geometric_transformations::{Interpolation, rotate}; use log::{debug, warn}; use std::collections::HashMap; @@ -40,45 +39,6 @@ where } } -/// Trait definition to get a RGB 565 representation from a source image. -pub trait ToRgb565 { - /// Get an RGB 565 representation of the image in little endian format. - fn to_rgb565_le(&self) -> BytesMut; - - /// Convert a single RGB 888 pixel to 16 bit RGB 565 format. - fn convert_rgb(&self, r: u8, g: u8, b: u8) -> u16 { - ((r & 248) as u16) << 8 | ((g & 252) as u16) << 3 | ((b as u16) >> 3) - } -} - -// TODO quick & dirty approach for converting RgbImage & RgbaImage to RGB 565. -// There should be a more generic way, maybe with PixelEnumerator... -impl ToRgb565 for &RgbImage { - fn to_rgb565_le(&self) -> BytesMut { - let mut img_rgb565 = - BytesMut::with_capacity(self.width() as usize * self.height() as usize * 2); - - for (_x, _y, pixel) in self.enumerate_pixels() { - img_rgb565.put_u16_le(self.convert_rgb(pixel.0[0], pixel.0[1], pixel.0[2])); - } - - img_rgb565 - } -} - -impl ToRgb565 for &RgbaImage { - fn to_rgb565_le(&self) -> BytesMut { - let mut img_rgb565 = - BytesMut::with_capacity(self.width() as usize * self.height() as usize * 2); - - for (_x, _y, pixel) in self.enumerate_pixels() { - img_rgb565.put_u16_le(self.convert_rgb(pixel.0[0], pixel.0[1], pixel.0[2])); - } - - img_rgb565 - } -} - /// Cache for loaded images to avoid repeated file I/O pub struct ImageCache { img_path: PathBuf, diff --git a/crates/asterctl/src/lib.rs b/crates/asterctl/src/lib.rs new file mode 100644 index 0000000..fdc19c2 --- /dev/null +++ b/crates/asterctl/src/lib.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder + +#![forbid(non_ascii_idents)] +#![deny(unsafe_code)] + +pub mod cfg; +pub mod font; +mod format_value; +pub mod img; +pub mod render; +pub mod sensors; + +pub use format_value::*; diff --git a/src/main.rs b/crates/asterctl/src/main.rs similarity index 60% rename from src/main.rs rename to crates/asterctl/src/main.rs index 0219683..cf68fa0 100644 --- a/src/main.rs +++ b/crates/asterctl/src/main.rs @@ -1,31 +1,21 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder -mod cfg; -mod display; -mod dummy_serialport; -mod font; -mod format_value; -mod img; -mod render; -mod sensors; +#![forbid(non_ascii_idents)] +#![deny(unsafe_code)] + +use asterctl::cfg::{MonitorConfig, Panel, load_custom_panel}; +use asterctl::render::PanelRenderer; +use asterctl::sensors::start_file_slurper; +use asterctl::{cfg, img}; +use asterctl_lcd::{AooScreen, AooScreenBuilder, DISPLAY_SIZE}; -use crate::cfg::{MonitorConfig, Panel, load_custom_panel}; -use crate::display::{AooScreen, AooScreenBuilder, DISPLAY_SIZE}; -use crate::font::FontHandler; -use crate::render::PanelRenderer; -use crate::sensors::start_file_slurper; -use ab_glyph::PxScale; use anyhow::anyhow; use clap::Parser; use env_logger::Env; -use image::imageops::FilterType; -use image::{ImageReader, Rgb, RgbImage}; -use imageproc::drawing::{draw_line_segment_mut, draw_text_mut}; use log::{debug, error, info}; use std::collections::HashMap; use std::fs; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::{Arc, RwLock}; @@ -83,10 +73,6 @@ struct Args { #[arg(long)] sensor_path: Option, - /// Run a demo - #[arg(long)] - demo: bool, - /// Switch off display n seconds after loading image or running demo. #[arg(short, long)] off_after: Option, @@ -134,9 +120,7 @@ fn main() -> anyhow::Result<()> { // switch on screen for remaining commands screen.init()?; - if !args.demo - && let Some(config) = args.config - { + if let Some(config) = args.config { info!("Starting sensor panel mode"); let img_save_path = if args.save { let img_save_path = PathBuf::from("out"); @@ -167,17 +151,6 @@ fn main() -> anyhow::Result<()> { debug!("Image sent in {}ms", timestamp.elapsed().as_millis()); } - if args.demo { - info!("Loading and displaying demo..."); - run_demo( - &mut screen, - args.config.as_deref(), - args.config_dir.unwrap_or_else(|| "cfg".into()), - args.font_dir.unwrap_or_else(|| "fonts".into()), - args.save, - )?; - } - if let Some(off) = args.off_after { info!("Switching off display in {off}s"); sleep(Duration::from_secs(off as u64)); @@ -298,155 +271,3 @@ fn update_panel( Ok(()) } - -fn run_demo( - screen: &mut AooScreen, - config: Option<&Path>, - config_dir: PathBuf, - font_dir: PathBuf, - save_images: bool, -) -> anyhow::Result<()> { - let rgb_img = demo_image()?; - - // fill left and right side of the loaded image with neighboring pixel color - const WIDTH: u32 = 108; - let rgb_img = demo_blinds(screen, &rgb_img, WIDTH, save_images)?; - - // print demo text over background image - demo_text(screen, &rgb_img, save_images)?; - - if let Some(config) = config { - let mut cfg = load_configuration(config, &config_dir, None)?; - - if let Some(panel) = cfg.get_next_active_panel() { - info!("Displaying demo panel..."); - - // get sensor values from panel configuration - let mut demo_values = HashMap::new(); - for sensor in &panel.sensor { - demo_values.insert( - sensor.label.clone(), - sensor.value.clone().unwrap_or_default(), - ); - } - - let mut renderer = PanelRenderer::new(DISPLAY_SIZE, &font_dir, &config_dir); - renderer.set_save_render_img(save_images); - renderer.set_save_processed_pic(save_images); - renderer.set_save_progress_layer(save_images); - - update_panel(screen, &mut renderer, panel, &demo_values)?; - } else { - error!("No active panel found"); - } - } - - Ok(()) -} - -fn demo_image() -> anyhow::Result { - let reader = ImageReader::new(Cursor::new(include_bytes!("../img/aybabtu.png"))) - .with_guessed_format() - .expect("Cursor io never fails"); - - Ok(reader - .decode()? - .resize_exact(DISPLAY_SIZE.0, DISPLAY_SIZE.1, FilterType::Lanczos3) - .to_rgb8()) -} - -fn demo_text( - screen: &mut AooScreen, - background: &RgbImage, - save_images: bool, -) -> anyhow::Result<()> { - let text = "ALL YOUR BASE ARE BELONG TO US."; - let text_upd_delay = Duration::from_millis(0); - let font = FontHandler::default_font(); - let height = 36.0; - let scale = PxScale { - x: height, - y: height, - }; - - if save_images { - fs::create_dir_all("out")?; - } - - for text_idx in 0..text.len() { - info!("Printing: {}", &text[0..text_idx + 1]); - let text_upd = Instant::now(); - let mut rgb_img = background.clone(); - draw_text_mut( - &mut rgb_img, - Rgb([118u8, 118u8, 97u8]), - 4 * 47, - 300, - scale, - &font, - &text[0..text_idx + 1], - ); - - if save_images { - rgb_img.save_with_format( - format!("out/demo_text-{text_idx}.png"), - image::ImageFormat::Png, - )?; - } - - screen.send_image(&rgb_img)?; - - let elapsed = text_upd.elapsed(); - if elapsed < text_upd_delay { - sleep(text_upd_delay - elapsed); - } - } - - Ok(()) -} - -// CPU intensive! Release build is ~ 5x faster on M1 Max -fn demo_blinds( - screen: &mut AooScreen, - background: &RgbImage, - width: u32, - save_images: bool, -) -> anyhow::Result { - let mut rgb_img = background.clone(); - - info!("Masking {width} pixels of left & right image..."); - - if save_images { - fs::create_dir_all("out")?; - } - - for y in 0..DISPLAY_SIZE.1 { - let color = *rgb_img.get_pixel(width + 1, y); - draw_line_segment_mut( - &mut rgb_img, - (0.0, y as f32), - (width as f32, y as f32), - color, - ); - let color = *rgb_img.get_pixel(DISPLAY_SIZE.0 - width - 1, y); - draw_line_segment_mut( - &mut rgb_img, - ((DISPLAY_SIZE.0 - width) as f32, y as f32), - (DISPLAY_SIZE.0 as f32, y as f32), - color, - ); - - if y % 5 == 0 { - screen.send_image(&rgb_img)?; - } - - if save_images { - rgb_img - .save_with_format(format!("out/demo_blinds-{y}.png"), image::ImageFormat::Png)?; - } - } - - screen.send_image(&rgb_img)?; - - Ok(rgb_img) -} diff --git a/src/render.rs b/crates/asterctl/src/render.rs similarity index 99% rename from src/render.rs rename to crates/asterctl/src/render.rs index c22cbdd..86c310a 100644 --- a/src/render.rs +++ b/crates/asterctl/src/render.rs @@ -5,7 +5,7 @@ use crate::cfg::{Panel, Sensor, SensorDirection, SensorMode, TextAlign}; use crate::font::FontHandler; -use crate::format_value::format_value; +use crate::format_value; use crate::img::{ImageCache, Size, rotate_image}; use ab_glyph::Font; use image::{ImageBuffer, Rgba, RgbaImage}; diff --git a/src/sensors.rs b/crates/asterctl/src/sensors.rs similarity index 100% rename from src/sensors.rs rename to crates/asterctl/src/sensors.rs diff --git a/crates/sysinfo/Cargo.toml b/crates/sysinfo/Cargo.toml new file mode 100644 index 0000000..81b72f1 --- /dev/null +++ b/crates/sysinfo/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sysinfo" +version = "0.1.0" +description = "System sensor provider for asterctl" + +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +clap = { version = "4.5.42", features = ["derive"] } +sysinfo = "0.37.0" +itertools = "0.14" +tempfile = "3" +log = "0.4.27" +env_logger = "0.11.8" +regex = "1.11" diff --git a/crates/sysinfo/LICENSE-APACHE b/crates/sysinfo/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/sysinfo/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/sysinfo/LICENSE-MIT b/crates/sysinfo/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/sysinfo/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/sysinfo/README.md b/crates/sysinfo/README.md new file mode 100644 index 0000000..e36c5c6 --- /dev/null +++ b/crates/sysinfo/README.md @@ -0,0 +1,47 @@ +# System Sensor Provider for asterctl + +This tool gathers system sensor values with the help of the [sysinfo](https://github.com/GuillaumeGomez/sysinfo) crate +and writes them into a text file. + +See [README](../../README.md) in root directory for more information. + +``` +Proof of concept sensor value collection for the asterctl screen control tool + +Usage: sysinfo [OPTIONS] + +Options: + -o, --out + Output sensor file + + -t, --temp-dir + Temporary directory for preparing the output sensor file. + + The system temp directory is used if not specified. + The temp directory must be on the same file system for atomic rename operation! + + --console + Print values in console + + -r, --refresh + System sensor refresh interval in seconds + + --disk-refresh + Enable individual disk refresh logic as used in AOOSTAR-X. Refresh interval in seconds + + --smartctl + Retrieve drive temperature if `disk-update` option is enabled. + + Requires smartctl and password-less sudo! +``` + +Single test run with printing all sensors in the console: +```shell +sysinfo --console +``` + +Normal mode providing sensor values for `asterctl` in `/tmp/sensors/sysinfo.txt` every 3 seconds: + +```shell +sysinfo --refresh 3 --out /tmp/sensors/sysinfo.txt +``` diff --git a/src/bin/sysinfo.rs b/crates/sysinfo/src/main.rs similarity index 99% rename from src/bin/sysinfo.rs rename to crates/sysinfo/src/main.rs index c4170de..8727c58 100644 --- a/src/bin/sysinfo.rs +++ b/crates/sysinfo/src/main.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder +#![forbid(non_ascii_idents)] +#![deny(unsafe_code)] + use clap::Parser; use env_logger::Env; use itertools::Itertools; diff --git a/doc/README.md b/docs/README.md similarity index 100% rename from doc/README.md rename to docs/README.md diff --git a/doc/asterctl.md b/docs/asterctl.md similarity index 93% rename from doc/asterctl.md rename to docs/asterctl.md index f13e0eb..f25bf17 100644 --- a/doc/asterctl.md +++ b/docs/asterctl.md @@ -2,9 +2,6 @@ > Aster: Greek for star and similar to AOOSTAR. -Currently, the project includes a proof-of-concept demo application that loads an image, draws rectangles, and writes -text over the image. - A work-in-progress "panel-mode" mimics the AOOSTAR-X software and uses the same configuration files for rendering sensor panels with dynamic sensor values. @@ -51,9 +48,6 @@ Options: --sensor-path Single sensor value input file or directory for multiple sensor input files. Default: `./cfg/sensors` - --demo - Run a demo - -o, --off-after Switch off display n seconds after loading image or running demo @@ -73,21 +67,6 @@ Options: Print version ``` -## Demo Mode - -```shell -cargo run --release -- --demo --config monitor.json -``` - -The `--config` parameter is optional. It loads the official configuration file and displays the defined sensors in the -first panel. - -### Parameters - -- `--device /dev/ttyACM0` — Specify the serial device. -- `--usb 0403:6001` — Specify the USB UART device by USB **VID:PID** (hexadecimal, as shown by `lsusb`). -- `--help` — Show all options. - ## Sensor Panel Mode ```shell @@ -122,3 +101,19 @@ asterctl --image img/aybabtu.png This expects a 960 × 376 image (other sizes are automatically scaled and the aspect ratio is ignored). See Rust image crate for [supported image formats](https://github.com/image-rs/image?tab=readme-ov-file#supported-image-formats). + +## Demo app + +```shell +cargo run --release --bin demo -- --config monitor.json +``` + +The `--config` parameter is optional. It loads the official configuration file and displays the defined sensors in the +first panel. + +### Parameters + +- `--device /dev/ttyACM0` — Specify the serial device. +- `--usb 0403:6001` — Specify the USB UART device by USB **VID:PID** (hexadecimal, as shown by `lsusb`). +- `--help` — Show all options. + diff --git a/doc/img/lcd_off.png b/docs/img/lcd_off.png similarity index 100% rename from doc/img/lcd_off.png rename to docs/img/lcd_off.png diff --git a/doc/img/lcd_on.png b/docs/img/lcd_on.png similarity index 100% rename from doc/img/lcd_on.png rename to docs/img/lcd_on.png diff --git a/doc/img/mode4_pic.png b/docs/img/mode4_pic.png similarity index 100% rename from doc/img/mode4_pic.png rename to docs/img/mode4_pic.png diff --git a/doc/img/progress.png b/docs/img/progress.png similarity index 100% rename from doc/img/progress.png rename to docs/img/progress.png diff --git a/doc/img/progress_circle.png b/docs/img/progress_circle.png similarity index 100% rename from doc/img/progress_circle.png rename to docs/img/progress_circle.png diff --git a/doc/img/send_image.png b/docs/img/send_image.png similarity index 100% rename from doc/img/send_image.png rename to docs/img/send_image.png diff --git a/doc/img/sensor_mode1.png b/docs/img/sensor_mode1.png similarity index 100% rename from doc/img/sensor_mode1.png rename to docs/img/sensor_mode1.png diff --git a/doc/img/sensor_mode1_background.png b/docs/img/sensor_mode1_background.png similarity index 100% rename from doc/img/sensor_mode1_background.png rename to docs/img/sensor_mode1_background.png diff --git a/doc/img/sensor_mode2.jpg b/docs/img/sensor_mode2.jpg similarity index 100% rename from doc/img/sensor_mode2.jpg rename to docs/img/sensor_mode2.jpg diff --git a/doc/img/sensor_mode2_background.jpg b/docs/img/sensor_mode2_background.jpg similarity index 100% rename from doc/img/sensor_mode2_background.jpg rename to docs/img/sensor_mode2_background.jpg diff --git a/doc/img/sensor_mode3.png b/docs/img/sensor_mode3.png similarity index 100% rename from doc/img/sensor_mode3.png rename to docs/img/sensor_mode3.png diff --git a/doc/img/sensor_mode3_background.png b/docs/img/sensor_mode3_background.png similarity index 100% rename from doc/img/sensor_mode3_background.png rename to docs/img/sensor_mode3_background.png diff --git a/doc/img/sensor_mode4.png b/docs/img/sensor_mode4.png similarity index 100% rename from doc/img/sensor_mode4.png rename to docs/img/sensor_mode4.png diff --git a/doc/img/sensor_mode4_background.png b/docs/img/sensor_mode4_background.png similarity index 100% rename from doc/img/sensor_mode4_background.png rename to docs/img/sensor_mode4_background.png diff --git a/doc/img/sensor_panel-01.png b/docs/img/sensor_panel-01.png similarity index 100% rename from doc/img/sensor_panel-01.png rename to docs/img/sensor_panel-01.png diff --git a/doc/img/sensor_panel-02.png b/docs/img/sensor_panel-02.png similarity index 100% rename from doc/img/sensor_panel-02.png rename to docs/img/sensor_panel-02.png diff --git a/doc/lcd_protocol.md b/docs/lcd_protocol.md similarity index 100% rename from doc/lcd_protocol.md rename to docs/lcd_protocol.md diff --git a/doc/sensor_custom_panel.md b/docs/sensor_custom_panel.md similarity index 100% rename from doc/sensor_custom_panel.md rename to docs/sensor_custom_panel.md diff --git a/doc/sensor_data_shell.md b/docs/sensor_data_shell.md similarity index 100% rename from doc/sensor_data_shell.md rename to docs/sensor_data_shell.md diff --git a/doc/sensor_data_sysinfo.md b/docs/sensor_data_sysinfo.md similarity index 82% rename from doc/sensor_data_sysinfo.md rename to docs/sensor_data_sysinfo.md index 4709098..feacc4b 100644 --- a/doc/sensor_data_sysinfo.md +++ b/docs/sensor_data_sysinfo.md @@ -1,6 +1,6 @@ # sysinfo Tool -The Rust based [/src/bin/sysinfo.rs](../src/bin/sysinfo.rs) tool gathers many more system sensor values with the help of +The Rust based [sysinfo](../crates/sysinfo/src/main.rs) tool gathers many more system sensor values with the help of the [sysinfo](https://github.com/GuillaumeGomez/sysinfo) crate. It supports FreeBSD, Linux, macOS, Windows and other OSes, but it has only been tested on Linux so far. @@ -35,15 +35,15 @@ Options: Requires smartctl and password-less sudo! ``` -Single test run with printing all sensors on the console: +Single test run with printing all sensors in the console: ```shell sysinfo --console ``` -Normal mode providing sensor values for `asterctl` in `/tmp/sensors/sysinfo.txt`: +Normal mode providing sensor values for `asterctl` in `/tmp/sensors/sysinfo.txt` every 3 seconds: ```shell -sysinfo --refresh 3 --out /tmp/sensor/sysinfo.txt +sysinfo --refresh 3 --out /tmp/sensors/sysinfo.txt ``` Note: the lower the refresh rate, the more resources are used! diff --git a/doc/sensor_data_txt_file.md b/docs/sensor_data_txt_file.md similarity index 100% rename from doc/sensor_data_txt_file.md rename to docs/sensor_data_txt_file.md diff --git a/doc/sensor_mode1_text.md b/docs/sensor_mode1_text.md similarity index 100% rename from doc/sensor_mode1_text.md rename to docs/sensor_mode1_text.md diff --git a/doc/sensor_mode2_fan.md b/docs/sensor_mode2_fan.md similarity index 100% rename from doc/sensor_mode2_fan.md rename to docs/sensor_mode2_fan.md diff --git a/doc/sensor_mode3_progress.md b/docs/sensor_mode3_progress.md similarity index 100% rename from doc/sensor_mode3_progress.md rename to docs/sensor_mode3_progress.md diff --git a/doc/sensor_mode4_pointer.md b/docs/sensor_mode4_pointer.md similarity index 100% rename from doc/sensor_mode4_pointer.md rename to docs/sensor_mode4_pointer.md diff --git a/doc/sensor_panels.md b/docs/sensor_panels.md similarity index 100% rename from doc/sensor_panels.md rename to docs/sensor_panels.md diff --git a/doc/shell_commands.md b/docs/shell_commands.md similarity index 100% rename from doc/shell_commands.md rename to docs/shell_commands.md diff --git a/linux/lcd-on.service b/linux/lcd-on.service new file mode 100644 index 0000000..b803b2c --- /dev/null +++ b/linux/lcd-on.service @@ -0,0 +1,42 @@ +[Unit] +Description=Switch on embedded LCD + +[Service] +Type=oneshot +RemainAfterExit=no +DynamicUser=true +# tailored to Debian: adapt for other Linux flavours! RW access to /dev/ttyACM0 is required +Group=dialout + +ExecStart=/usr/bin/asterctl --on + +# lock down service +CapabilityBoundingSet= +LockPersonality=true +RestrictNamespaces=true +ProtectHome=true +ProtectSystem=strict +NoNewPrivileges=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true +ProtectControlGroups=true +MemoryDenyWriteExecute=true +RestrictSUIDSGID=true +KeyringMode=private +ProtectClock=true +ProtectProc=invisible +ProcSubset=pid +RestrictRealtime=true +PrivateNetwork=true +PrivateTmp=true +PrivateUsers=true +ProtectHostname=true +RestrictAddressFamilies=none +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources +SystemCallErrorNumber=EPERM +UMask=0177 + +# that's all we need access to +DeviceAllow=/dev/ttyACM0 rw