Skip to content

Repository files navigation

nas-mini-display

A graphical dashboard for the small LCD panel built into the AOOSTAR WTR MAX NAS.

Instead of scrolling numbers, the 960×376 panel shows bar charts with colour thresholds, five-hour area charts of CPU / GPU / RAM usage, and pages for network and storage. It runs as a systemd service, talks to the panel directly over its USB serial port, and needs nothing but Python 3 and two common libraries.

Temperature overview

  • Written in Python 3 — no compiler, no toolchain, no Rust at runtime
  • Two dependencies: Pillow (drawing) and pyserial (the panel)
  • Works on bare metal or from inside a Proxmox LXC container
  • Every page is optional and reorderable; pages with no data hide themselves
  • Screens are driven by one JSON file — colours, thresholds, labels, durations

Naming: the repository is nas-mini-display; the Python package and the systemd unit inside it are called aoo-display, from the AOOSTAR panel they were written for.


Screens

The rotation below is the default. Each page can be removed, reordered or re-timed in config.json, and any page whose data is unavailable is skipped automatically — no GPU sensor means no GPU page.

These images are produced by the renderer itself, from real readings on a WTR MAX. Only the multi-hour history behind the area charts is simulated: a screenshot is a single moment, so there is no other way to show a five-hour chart. Regenerate them any time with python3 tools/render_docs_screens.py.

Temperature overview · 5 s

Worst reading of each group, so one hot drive cannot hide behind three cool ones.

Overview

Per-device detail · 2 s each

One bar per device, so you can tell which drive is the warm one.

HDD detail NVMe detail

CPU, GPU and RAM show temperature and utilisation side by side. If your CPU exposes per-core temperatures, a per-core page appears as well.

CPU detail GPU detail RAM detail

Usage over the last five hours · 5 s each

The fill is a fixed vertical gradient — green at the bottom, red at the top — so the height of the curve can be read from its colour alone. History is written to disk, so the charts survive restarts instead of starting empty every time.

CPU chart GPU chart RAM chart

Network and storage · 5 s each

Network Storage

Colour thresholds

Group blue yellow red
HDD, NVMe, RAM ≤ 60 °C 60 – 80 °C > 80 °C
CPU, GPU ≤ 75 °C 75 – 88 °C > 88 °C

The change is abrupt at the threshold and gradual inside each band, so a glance tells you which band a value is in. All of it is redefinable — see docs/configuration.md.


Requirements

  • An AOOSTAR WTR MAX (or any machine wired to the same 0416:90A1 USB serial panel)
  • Linux with systemd, Python 3.10 or newer
  • python3-pil, python3-serial; smartmontools only if you need drive temperatures that the drivetemp kernel module cannot provide

Nothing needs to be compiled and no daemon other than this one is added.


Installation

Pick the path that matches where the USB panel is visible.

Path A — bare metal

The simplest case: the operating system that owns the disks also owns the panel.

git clone https://github.com/teoteo/nas-mini-display.git
cd nas-mini-display
sudo ./install.sh

install.sh installs the dependencies, copies the code to /opt/aoo-display, loads the drivetemp module (SATA drive temperatures) and makes it persistent, disables the stock aster-display.service if present — both cannot share the serial port — and starts aoo-display.service.

Then check what was detected and adjust the config:

python3 -m aoo_display.app --config /opt/aoo-display/config.json --diagnose
sudo nano /opt/aoo-display/config.json
sudo systemctl restart aoo-display

On bare metal you usually only need to fill in sensors.volumes with the mount points you want on the storage page, and rename drives in sensors.labels.

Path B — inside a Proxmox LXC container

Common on these boxes: Proxmox owns the hardware and a small container owns the panel, because that is where the USB device was passed through.

A container sees more than you might expect and less than you need. /sys is shared with the host, so temperatures are real — drives, CPU, GPU, memory modules all report correctly. But /proc is masked by lxcfs, so CPU and memory usage are the container's, not the machine's, and the host's storage pools are invisible.

Install exactly as in Path A, inside the container. Then close the two gaps by giving the container a read-only Proxmox API token — no software is installed on the host, no bind mount, no container restart:

# on the Proxmox host, once
pveum user add display@pve --comment "LCD display (read-only)"
pveum acl modify /nodes/$(hostname) --users display@pve --roles PVEAuditor
pveum acl modify /storage           --users display@pve --roles PVEAuditor
pveum user token add display@pve lcd --privsep 0

The last command prints the secret once. Put it in the container's config:

"sensors": {
  "proxmox": {
    "enabled": true,
    "host": "192.168.1.10",
    "node": "proxmox",
    "token_id": "display@pve!lcd",
    "token_secret": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "use_for": ["volumes", "cpu_usage", "ram_usage", "ram_bytes",
                "uptime", "load", "network", "hostname"]
  }
}
chmod 600 /opt/aoo-display/config.json
python3 -m aoo_display.app --config /opt/aoo-display/config.json --diagnose
systemctl restart aoo-display

use_for decides which values come from the API; drop an entry and that value stays local. A ready-made file is in examples/config.proxmox-lxc.json, and the details — including two alternatives that do not use the API — are in docs/proxmox.md.

The token sits in a plain file, so keep it at chmod 600 and give it no role beyond PVEAuditor, which is read-only.


Checking that it works

# what the system can see, with hints about anything missing
python3 -m aoo_display.app --config config.json --diagnose

# solid colours, a test card, then a partial update — isolates the panel from the app
python3 -m aoo_display.app --test-pattern

# one PNG per page, without touching the panel
python3 -m aoo_display.app --config config.json --preview /tmp/screens

# turn the panel off and on
python3 -m aoo_display.app --off
python3 -m aoo_display.app --on

journalctl -u aoo-display -f

--diagnose is the first thing to run when something is missing: it lists hwmon chips, block devices, available tools and the Proxmox connection, then prints concrete suggestions for whatever it could not read.

Common problems

Nothing on the panel, no errors in the log. Another process owns the serial port. systemctl stop aster-display.service, or whatever else talks to the panel.

The panel keeps showing an old image while the log reports chunks being sent. The firmware needs one command per USB packet. chunk_batch must be 1 unless you have tested otherwise with --test-pattern.

No drive temperatures. modprobe drivetemp, then check --diagnose again. On drives the module cannot read, set "smartctl": true — but list USB drives in sensors.hide first, or polling will keep waking them up.

Drives, CPU or GPU missing inside a container. /sys may not be fully visible. --diagnose says which one is missing and what to do about it.


Configuration

The whole dashboard is one JSON file. The keys you are most likely to touch:

Key Effect
pages[] which screens appear, in what order, for how long (seconds)
display_on_hour / display_off_hour switch the panel off overnight
graph.window_hours width of the area charts (default 5)
sensors.volumes mount points on the storage page
sensors.labels rename drives: {"sda": "HDD1", "nvme0": "System"}
sensors.hide exclude devices entirely
scales thresholds and colours
decimals, decimal_comma number format (52.7 or 52,7)

Full reference: docs/configuration.md.


How it works

Roughly 2,600 lines of Python in eleven modules:

lcd.py the panel protocol: RGB565, 47-byte chunks, differential updates
sensors.py readings from /sys, /proc, smartctl, external files
pve.py Proxmox API client, on its own thread
draw.py drawing primitives: rounded gradient bars, area charts
pages.py the screens
theme.py palette, threshold scales, geometry
history.py persistent history for the charts
app.py configuration, scheduler, main loop

Two decisions shaped the rest. Frames are drawn on a 1920×752 canvas and scaled down to 960×376, which is the cheapest way to get antialiased curves and rounded corners out of Pillow, at about 40 ms per frame. And RGB565 conversion goes through Image.merge("LA", …), which packs two bytes per pixel at C speed: 7 ms, byte-identical to the numpy version, without the numpy dependency.

After the first full frame only changed chunks are retransmitted, so a typical update is a few kB instead of 900, and a full refresh is forced every five minutes in case the panel's framebuffer drifts out of sync. See docs/protocol.md for what we learned about the wire format.


Portability

Tested only on the AOOSTAR WTR MAX, in the configuration described above. That is the honest scope: one machine, one panel, verified in daily use.

Nothing in the design is specific to that model, though. Hardware discovery is automatic — the serial port is found by USB VID:PID, drives, temperatures and GPU come from /sys without any model knowledge — so another WTR MAX should work with no changes beyond labels and mount points.

The GEM12+ PRO uses the same panel according to upstream, so it is the most likely next candidate; it has not been tested here.

For a panel from another vendor, the display-specific parts are the constants at the top of lcd.py: resolution, VID:PID, baud rate and the command bytes. Everything above that layer is independent of the screen.

Reports from other hardware are welcome — a --diagnose output is the most useful thing to attach.


Credits

The USB protocol of this panel is not documented by the manufacturer. It was reverse engineered by Markus Zehnder and Gabriel Max in aoostar-rs (documentation), whose docs/lcd_protocol.md and asterctl-lcd crate are where the command bytes, the 47-byte chunking and the framing in this project come from. Without that work this dashboard would not exist, and their asterctl remains the reference implementation.

This is an independent Python implementation, not a port of their code, and it runs without any part of aoostar-rs installed. It does keep optional support for their aster-sysinfo collector as an alternative data source.

License

Dual licensed, matching upstream, at your option:

Unless you state otherwise, any contribution you submit shall be dual licensed as above, without additional terms or conditions.

About

Graphical dashboard for the AOOSTAR WTR MAX LCD panel

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages