Streacom VU1 dial controller - Linux + Windows, self-updating
  • Python 55.7%
  • JavaScript 20.4%
  • CSS 9.3%
  • PowerShell 7.5%
  • Shell 3.8%
  • Other 3.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
kadir 4aeb46bb51 Don't fail the udev setup on image-based distros (Bazzite)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:47:18 +02:00
assets Game mode: show a running game's FPS on a dial, on a face made for the game 2026-10-13 19:15:44 +02:00
images Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
install Don't fail the udev setup on image-based distros (Bazzite) 2026-10-13 20:47:18 +02:00
static Game mode: fixed FPS scale with large numbers instead of the adaptive one 2026-10-13 20:19:24 +02:00
vendor/VU-Server Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
.gitattributes Readable versions and a double-click Windows installer 2026-10-13 18:53:40 +02:00
.gitignore Readable versions and a double-click Windows installer 2026-10-13 18:53:40 +02:00
games.py Game mode: fixed FPS scale with large numbers instead of the adaptive one 2026-10-13 20:19:24 +02:00
launcher.py Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
README.md Game mode: fixed FPS scale with large numbers instead of the adaptive one 2026-10-13 20:19:24 +02:00
requirements.txt Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
run.sh Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
sensors.py Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
supervisor.py Readable versions and a double-click Windows installer 2026-10-13 18:53:40 +02:00
updater.py Readable versions and a double-click Windows installer 2026-10-13 18:53:40 +02:00
vu1.py Make VU1 cross-platform and self-updating 2026-10-13 17:27:41 +02:00
web_app.py Game mode: fixed FPS scale with large numbers instead of the adaptive one 2026-10-13 20:19:24 +02:00

VU1

Controller for Streacom VU1 dials: a web UI (:8765) that shows CPU/GPU load, temperatures and fan speed on the dials, with reactive backlights. Runs on Linux and Windows, and every installed machine updates itself when this repo's main branch changes.

Install

Linux

curl -fsSL https://git.ihatenetflix.com/kadir/vu1/raw/branch/main/install/install.sh | bash

Windows 10/11 — in a normal (not elevated) PowerShell window:

irm https://git.ihatenetflix.com/kadir/vu1/raw/branch/main/install/install.ps1 | iex

Or download and double-click Install-VU1.cmd, which runs the same command.

Both are safe to re-run; settings and dial data are kept. Then open http://localhost:8765.

Linux Windows
Install dir ~/.local/share/vu1 %LOCALAPPDATA%\VU1
Starts at login via systemd user unit vu1.service scheduled tasks VU1 and VU1 Sensors
Temperatures / fans from kernel (hwmon, sysfs, nvidia-smi) LibreHardwareMonitor on 127.0.0.1:8085
Web UI listens on all interfaces localhost only (set VU1_HOST to change)
Needs admin sudo once, for the serial-port udev rule one UAC prompt
Uninstall install/uninstall.sh [--purge] install\uninstall.ps1 [-Purge]

What the Windows installer adds

Windows can't report CPU/GPU temperatures or fan speeds by itself, so the installer also sets up:

  • LibreHardwareMonitor (latest release, into %LOCALAPPDATA%\VU1\LibreHardwareMonitor) with its web server switched on and start-minimised-to-tray. It runs elevated — that is what lets it read the sensors — from the VU1 Sensors scheduled task.
  • PawnIO, the signed kernel driver LibreHardwareMonitor reads hardware through.
  • Python 3.13 via winget, only if no Python 3.10+ is present.

If LibreHardwareMonitor is already running with its web server on :8085, that one is used instead. -NoSensors skips all of it (temperature and fan dials then read 0).

Alternating faces

A dial can take turns between several metrics. Under Assign Metric, pick what to Alternate with and how many seconds each face stays up; the dial then cycles through its assigned metric and the chosen ones, swapping the face and the needle together.

  • The minimum is 10 seconds, because the e-ink display needs a few seconds to redraw.
  • The dial's custom scale applies to its assigned metric; the others use their default scale.
  • While a game is on a dial in game mode, the cycle pauses and picks up again afterwards.

Game mode

Switch a dial to Show game FPS in the web UI and, while a game runs, that dial shows the game's frame rate on a face made for the game, with the backlight in the game's colour. When the game exits, the dial gets its previous face, metric and backlight back.

  • Face: the game's logo from Steam, reduced to black and white for the e-ink (falls back to the name in plain lettering when Steam has no logo or the game isn't on Steam), above an FPS scale.
  • Scale: fixed at 0–500 FPS by default, printed in large numbers. Change both ends with the FPS scale boxes in the Game mode bar; it applies to every game.
  • Tuned games: Dota 2, PUBG, FC 26, FC 27, Factorio, Oxygen Not Included, Counter-Strike 2 and DayZ have hand-picked colours (PROFILES in games.py). Any other game gets its Steam logo and a colour taken from its Steam banner, or just its name.
  • Preview: the Game mode bar in the UI can fake a running game for 60 seconds.

Where the frame rate comes from:

Source What it needs
Linux / Bazzite MangoHud's live log MangoHud active for the game. The installer adds ~/.config/environment.d/vu1-mangohud.conf so MangoHud logs whenever it runs (log out and in once). In Steam desktop mode add mangohud %command% to a game's launch options, or enable MangoHud globally; in Bazzite's Game Mode keep the performance overlay at level 1 or higher. Flatpak Steam can't write to the log folder without extra permissions.
Windows Intel PresentMon (tools\PresentMon.exe) Membership of the "Performance Log Users" group, which the installer adds (sign out and in once). The game is the fullscreen or borderless window in front.

On Linux a Steam game is recognised even without MangoHud (the face and colour change, the needle stays at 0), which is the sign that MangoHud isn't active for that game.

How updates work

launcher.py      the one process the OS starts; swaps versions in and rolls them back
└─ supervisor.py   keeps the two programs below alive, checks the repo every 5 minutes
   ├─ vu-server      vendored VU-Server, talks to the hub over USB serial (:5340)
   └─ web_app.py     web UI + metrics loop (:8765)
  1. git push to main.
  2. Within 5 minutes each machine sees the new commit, downloads it, checks that it compiles and installs any new packages from requirements.txt.
  3. The dials pause for a few seconds while the new version is swapped in.
  4. The new version must answer /api/health within 2 minutes. If it doesn't, the previous version is put back and that commit is skipped until a newer one is pushed.

The web UI header shows the running version as v<build> · <date>: the build number is the count of commits on main up to the installed one, the date is when that commit was made (hover for the commit hash and message). state.json has the full hash; logs/supervisor.log in the install dir has the history. instance.json holds the repo URL, branch and check_interval (seconds). For a private repo, pass --token / -Token to the installer.

Not covered by self-update: launcher.py itself (new code loads at next login) and the service/task definitions (re-run the installer).

Per-machine state lives outside the code and survives updates: data/settings.json (dial assignments, colours, ranges) and vu-server/ (dial database, config, uploaded faces).

Development

./run.sh web     # web UI from this checkout, state kept next to the code
./run.sh         # terminal UI

install/install.sh --source . installs the working copy instead of downloading it.

vendor/VU-Server is an unmodified snapshot of SasaKaranovic/VU-Server at 62a4059.