refactor: project structure (#9)
Split up project into multiple crates and use a Cargo workspace.
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||
<entry key="Project Default">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run demo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --package aoostar-rs --bin asterctl -- --demo -c monitor.json" />
|
||||
<option name="command" value="run --bin asterctl -- --demo -c monitor.json" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run demo DEV" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="release" />
|
||||
<option name="command" value="run --package aoostar-rs --bin asterctl -- --demo -c monitor.json --save --simulate" />
|
||||
<option name="command" value="run --bin demo -- -c monitor.json --save --simulate" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run sensor panel" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --package aoostar-rs --bin asterctl -- -c monitor.json" />
|
||||
<option name="command" value="run --bin asterctl -- -c monitor.json" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run sensor panel DEV" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --package aoostar-rs --bin asterctl -- -c monitor.json --save --simulate" />
|
||||
<option name="command" value="run --bin asterctl -- -c monitor.json --save --simulate" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run sysinfo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --package aoostar-rs --bin sysinfo -- --console --out ./cfg/sensors/sysinfo.txt" />
|
||||
q <option name="command" value="run --bin sysinfo -- --console --out ./cfg/sensors/sysinfo.txt" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run sysinfo repeat" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run --package aoostar-rs --bin sysinfo -- --console --out ./cfg/sensors/sysinfo.txt --refresh 3" />
|
||||
<option name="command" value="run --bin sysinfo -- --console --out ./cfg/sensors/sysinfo.txt --refresh 3" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="clippy sysinfo" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="command" value="clippy --bin sysinfo --features=sysinfo" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -8,6 +8,6 @@
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,12 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="RUST_MODULE" version="4">
|
||||
<module version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/asterctl-lcd/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/asterctl/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/asterctl/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/crates/sysinfo/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/crates/asterctl-lcd/target" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/crates/sysinfo/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -0,0 +1 @@
|
||||
../../LICENSE-MIT
|
||||
@@ -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)
|
||||
@@ -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<AooScreen> {
|
||||
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),
|
||||
@@ -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<usize> {
|
||||
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<usize> {
|
||||
// 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<String> {
|
||||
Some("Dummy Serial".into())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -0,0 +1 @@
|
||||
../../LICENSE-MIT
|
||||
@@ -0,0 +1,3 @@
|
||||
# Screen control tool for AOOSTAR WTR MAX / GEM12+ PRO
|
||||
|
||||
See [README](../../README.md) in root directory for more information.
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -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<String>,
|
||||
|
||||
/// USB serial UART "vid:pid" in hex notation (lsusb output). Default: 416:90A1
|
||||
#[arg(short, long)]
|
||||
usb: Option<String>,
|
||||
|
||||
/// 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<PathBuf>,
|
||||
|
||||
/// Configuration directory containing configuration files and background images
|
||||
/// specified in the `config` file. Default: `./cfg`
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
/// Font directory for fonts specified in the `config` file. Default: `./fonts`
|
||||
#[arg(long)]
|
||||
font_dir: Option<PathBuf>,
|
||||
|
||||
/// Switch off display n seconds after loading image.
|
||||
#[arg(short, long)]
|
||||
off_after: Option<u32>,
|
||||
|
||||
/// 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<RgbImage> {
|
||||
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<RgbImage> {
|
||||
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)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use std::path::PathBuf;
|
||||
|
||||
static DEFAULT_TTF_FONT: Lazy<FontArc> = 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"),
|
||||
)
|
||||
});
|
||||
@@ -47,7 +47,7 @@ impl From<Option<i32>> 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(
|
||||
@@ -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,
|
||||
@@ -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::*;
|
||||
@@ -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<PathBuf>,
|
||||
|
||||
/// Run a demo
|
||||
#[arg(long)]
|
||||
demo: bool,
|
||||
|
||||
/// Switch off display n seconds after loading image or running demo.
|
||||
#[arg(short, long)]
|
||||
off_after: Option<u32>,
|
||||
@@ -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<RgbImage> {
|
||||
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<RgbImage> {
|
||||
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)
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -0,0 +1 @@
|
||||
../../LICENSE-MIT
|
||||
@@ -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 <OUT>
|
||||
Output sensor file
|
||||
|
||||
-t, --temp-dir <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 <REFRESH>
|
||||
System sensor refresh interval in seconds
|
||||
|
||||
--disk-refresh <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
|
||||
```
|
||||
@@ -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;
|
||||
@@ -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 <SENSOR_PATH>
|
||||
Single sensor value input file or directory for multiple sensor input files. Default: `./cfg/sensors`
|
||||
|
||||
--demo
|
||||
Run a demo
|
||||
|
||||
-o, --off-after <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.
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 259 KiB |
@@ -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!
|
||||
@@ -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
|
||||