PC-side companion to the STM32 dynamometer firmware, which lives in this repo under
firmware/. It connects to the dynamometer's STM32 over USB-CDC,
decodes its telemetry/error streams, and can send framed commands back. The UI is Avalonia
(cross-platform .NET); the device logic lives in a UI-agnostic Dyno.Core library.
The USB wire protocol is not hand-maintained: the firmware defines it once as a YAML schema and the C# message types are generated from that same schema (see Regenerating message types), so the host and firmware can't drift.
| Path | What |
|---|---|
src/Dyno.Core/ |
Device layer: serial, protocol parser + frame builder, generated message types. See README. |
src/Dyno.App/ |
Avalonia desktop UI. See README. |
tests/Dyno.Core.Tests/ |
xUnit tests (struct sizes, CRC, error decode, parser). |
tools/message_gen/ |
YAML → C# codegen + drift guard. See README. |
firmware/ |
STM32 firmware — the wire-protocol source of truth. See README. |
firmware/third_party/STM32CubeH7 |
ST's firmware pack, vendored as a submodule. Only needed to regenerate the firmware's HAL — never to build. See README. |
A plain clone is all you need — for the app and for the firmware alike:
git clone <repository-url>The repo has exactly one submodule, firmware/third_party/STM32CubeH7 (ST's HAL
package). It is needed only to regenerate the firmware from its .ioc; the
generated HAL is committed, so nothing here requires it to build. It is also ~2 GB,
and firmware/Scripts/regen-cube.sh initialises it on demand — so don't clone
with --recurse-submodules, and never run
git submodule update --init --recursive, which drags in the pack's own ~40
eval-board BSP sub-submodules that nothing uses. Details:
firmware/README.md.
| Tool | Purpose | Install (Fedora) | Install (Ubuntu/Debian) | Install (Windows) |
|---|---|---|---|---|
| .NET SDK 10 | Build / run / test | sudo dnf install dotnet-sdk-10.0 |
packages.microsoft.com | winget install Microsoft.DotNet.SDK.10 |
| Python 3 + venv | Only to regenerate message types | sudo dnf install python3 |
sudo apt install python3 python3-venv |
winget install Python.Python.3.13 |
On Linux, opening a serial port needs the user in the dialout group, and ModemManager has to
be told to leave the board alone:
sudo usermod -aG dialout "$USER" # re-login afterwards
sudo install -m 0644 scripts/udev/99-dyno-cdc.rules /etc/udev/rules.d/
sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=ttySkip the rule and connects are intermittent rather than broken: ModemManager probes each
newly-appeared ttyACM with AT commands for ~20-30 s, holding the port open, so whether a connect
succeeds depends on how long after plugging in you click. The board appears flaky, not busy.
The rule also adds a stable /dev/dyno symlink, so the port keeps its name across replugs.
On Windows no setup is needed: the board's USB-CDC interface enumerates as a
COMx port with the inbox usbser.sys driver.
The board's USB-CDC link comes from the STM32's own USB connector, not from an attached ST-Link. An ST-Link/V2 (
0483:3748) carries no virtual COM port at all, so a board wired up for debugging only will flash fine and never appear in the app's port list. Confirm withlsusb | grep 0483— the data link is the device that reports0483:5740.
The same commands work on Linux, macOS and Windows:
dotnet build Dyno.slnx # build everything (Debug)
dotnet test Dyno.slnx # run the unit tests
dotnet build Dyno.slnx -c Release # release build./scripts/run.sh # Linux/macOS
scripts\run.ps1 # Windows (PowerShell)The GUI needs a display (X11/Wayland on Linux) and a connected board to show live data. End users get native, per-RID self-contained builds:
dotnet publish src/Dyno.App/Dyno.App.csproj -c Release -r linux-x64 --self-contained
dotnet publish src/Dyno.App/Dyno.App.csproj -c Release -r win-x64 --self-containedThe committed Messages.cs, SysConfigCatalog.cs and ErrorCatalog.cs under src/Dyno.Core/
are all generated from the firmware's messages_public.yaml — the wire contract, every runtime
parameter, and every fault the board can report with the sentence the app shows for it. After
changing the schema:
./scripts/generate.sh # Linux/macOS — runs every generator
scripts\generate.ps1 # Windows (PowerShell)CI fails if any committed file is out of sync (python tools/message_gen/check.py). Details:
tools/message_gen/README.md.
.github/workflows/build.yml:
- codegen — verifies
Messages.csmatches the schema (drift guard). - build & test —
dotnet build/teston Ubuntu and Windows, then publishes the app per RID (linux-x64,win-x64) as workflow artifacts.
.github/workflows/firmware.yml:
- generated-headers — verifies the firmware's committed headers match the schema.
- build — Docker-toolchain firmware builds (Debug and Release), uploaded as workflow artifacts.
- On connect the firmware announces itself and the host replies with a version-checked
USB_CMD_ACKhandshake; no telemetry streams until it completes, and aUSB_PROTOCOL_VERSIONmismatch refuses the link rather than mis-decoding the stream. - Sensor data streams only while the dyno is running a session. The device announces each session start/stop (and re-states it after every ack), so the app can show whether a session is on and hide the readouts when it is not — an idle board and a dead one otherwise look identical.
Dyno.Corehas no UI dependency, so it can be driven headlessly or from another front-end.- Target framework is
net10.0; the solution uses the.slnx(XML) solution format.