A native Windows app for discovering, streaming, plotting, logging and configuring Twinleaf quantum sensors.
This is the Windows sibling of the macOS/iPadOS
twinleaf-app,
built the same way: a native platform UI over the same Rust core, with a
C ABI between them.
| macOS / iPadOS | Windows | |
|---|---|---|
| UI | SwiftUI | WinUI 3 (Windows App SDK), C# |
| Core | libtwinleaf_core.dylib / .a |
twinleaf_core.dll |
| Boundary | C ABI (twinleaf_core.h) |
the same C ABI |
| Drawing | SwiftUI Canvas |
Win2D (Direct2D) |
Keeping the boundary identical is the point: the core is developed once, and the two apps stay behaviourally identical because they consume the same events.
twinleaf-app-win/
rust/tio-bridge/ # Rust core — copied from the macOS app, unmodified
src/lib.rs # C ABI shim (twinleaf_runtime_*)
src/main.rs # runtime + CLI harness
include/twinleaf_core.h
src/Twinleaf/ # WinUI 3 app
Interop/ # P/Invoke + binary event decoding
Models/ # device, stream, column, RPC model
Core/BridgeClient.cs # observable app model
Core/TioDocument.cs # .tio recording + save
Models/Diagnostics.cs# firmware + stream-health events
Controls/ # plot canvas, sidebar rows, splitter
vendor/twinleaf-rust/ # submodule, pinned to the macOS app's commit
The window follows the GTK app: one sidebar, grouped per device, carrying that device's streams and channels and its settings — settings sit inline with the streams they belong to rather than in a panel of their own. The header keeps the sidebar toggle as its one leading control, with everything else right-aligned and file commands (Open / Save / Save As / Export) in a menu.
Recording always writes to a temporary .tio, and Save snapshots that file to
the chosen path — the same model the macOS app uses. That ordering is what lets
an untitled window start recording immediately and keep recording across the
save: the core appends to a path that never moves. Opening a log copies it to the
document's temporary file first, so inspecting a saved log can never write to it.
The core owns everything hardware-facing: device I/O through twinleaf-rust,
FPCS decimation, Welch FFT, .tio packet logging, CSV/HDF5 export, playback,
firmware updates and stream health. The UI drives it by calling
twinleaf_runtime_* and consuming events through one callback.
Events arrive in three shapes, discriminated by the callback's kind:
| kind | transport | used for |
|---|---|---|
| 0 | UTF-8 JSON | events with no binary encoding: upgradeStatus, upgradeProgress, health |
| 1 | binary plot frame | the streaming hot path — already-decimated series |
| 2 | binary typed event | the sixteen common events (device list, metadata, RPC results, …) |
Interop/EventDecoder.cs is the mirror image of the Rust encoders in
rust/tio-bridge/src/main.rs. The wire format carries no field names, so
field order there is load-bearing — reordering silently misreads every later
field. When the core's encoders change, that file changes with them.
Threading: the core calls back from its own worker threads and the payload
pointer is valid only for that call, so events are decoded synchronously on the
Rust thread and the immutable result is marshalled to the UI thread by
BridgeClient.
Prerequisites:
- Rust (MSVC toolchain) — https://rustup.rs. The core links against MSVC,
so this must be
x86_64-pc-windows-msvc, not an MSYS2/MinGW Rust. - .NET 9 SDK — https://dot.net.
- Visual Studio Build Tools 2022 with the MSVC toolset and a Windows 10/11 SDK, which is what Rust's MSVC target links through.
- The vendored core:
git submodule update --init --recursive.
dotnet build src/Twinleaf/Twinleaf.csproj -r win-x64The build compiles the Rust core first and stages twinleaf_core.dll beside the
app, the same way the macOS app's Xcode run-script phase embeds the dylib. Pass
-p:TwinleafSkipRustBuild=true to reuse the last Rust build.
To produce a runnable, self-contained app folder:
dotnet publish src/Twinleaf/Twinleaf.csproj -c Release -r win-x64That output is unpackaged and self-contained — no MSIX install, no separately installed Windows App SDK or .NET runtime — so it can be zipped and run anywhere.
twinleaf-rust ships a simulator. Run it, then connect the app to
udp://127.0.0.1:7855 (use 127.0.0.1, not localhost — the simulator binds
IPv4):
cargo run --manifest-path vendor/twinleaf-rust/Cargo.toml --bin tio -- simulateThe Rust core also builds a CLI harness (tio-bridge) that speaks the same
events over stdout, which is useful for isolating core behaviour from the UI.
The app itself can be driven unattended through environment hooks, which is how the milestones above are verified without a human at the keyboard:
| variable | effect |
|---|---|
TWINLEAF_SELFTEST_CONNECT=<url> |
connect on launch and plot the first column |
TWINLEAF_SELFTEST_RPC=name=value[,…] |
write settings once metadata lands, logging each reply |
TWINLEAF_SELFTEST_OPEN=<path> |
open a saved .tio for inspection |
TWINLEAF_SELFTEST_EXPORT=<path> |
export before quitting; format from the extension |
TWINLEAF_SELFTEST_QUIT=<seconds> |
exit after N seconds with a summary |
Diagnostics go to stderr. TWINLEAF_DEBUG=1 additionally enables the core's own
tracing, matching the macOS app and tio-bridge.
Working end to end:
-
Rust core builds unmodified on Windows as
twinleaf_core.dll, exporting all sixteentwinleaf_runtime_*entry points. -
Full P/Invoke surface, binary typed-event decoder, and binary plot-frame decoder (frame version 5, with the older version gates the Swift app carries).
-
Live device discovery, connect/disconnect, stream + column metadata, channel sidebar with live value readout, and a Win2D plot with linear/log axes, nice ticks, legend and crosshair readout.
-
Settings sidebar: every device RPC with the control its type implies — value field, switch, action button or capture trigger — plus search, live readback and a refresh affordance when the device reports its cached values stale.
-
.tiodocuments: record to a temporary log, Save/Save As to snapshot it, and Open an existing log for inspection with a playback scrubber. A recording made by the app reads back cleanly intio log inspect. -
Export to CSV or HDF5, and copy the plotted window to the clipboard. The format follows the extension you choose. Export covers the whole log, not just the plotted channels — it reads the
.tio, not the view. -
Stream-health popover: per-stream rate, jitter, clock error and dropped counts, refreshed about twice a second. The toolbar button appears only once the core reports stream data and flags itself when a stream goes stale or starts losing samples.
-
Firmware popover: available updates with a per-device Flash button and live flash progress. The update arrow stays hidden unless something is actually available — its appearance is the notification — so the check also lives in the menu where it is always reachable.
HDF5 needs the core built with its feature (cargo build --features hdf5,
which wants cmake); the default build reports plainly that HDF5 is unavailable
rather than failing silently.
Not yet ported from the macOS app: multi-pane layout with drag-and-drop, RPC sliders, favourites, the capture viewer (the capture trigger works), and preferences.
Pushing a vX.Y.Z tag runs release.yml: CI
builds the self-contained app with HDF5 enabled, signs Twinleaf.exe and
twinleaf_core.dll through the org's Azure Trusted Signing profile, and drafts
a GitHub release carrying the zip — published after a human looks at it. The
workflow can also be dispatched from the Actions tab, which stops at an
uploaded artifact (unsigned if signing secrets are absent) without touching
releases.
After a session starts streaming, the core checks whether newer published
firmware exists for each connected device, against the public catalog at
github.com/twinleaf/twinleaf-firmware-updates via api.github.com. The app
does not repeat that check on its own — doing so just spends a second round trip
on the same answer — it only re-checks when asked. Firmware images download over
HTTPS when an upgrade starts. Apart from this, the app makes no connections
other than the device and proxy URLs you configure.
Apache-2.0. Vendored dependencies retain their own licenses; vendor/twinleaf-rust
is MIT/Apache-2.0.
docs/shared-core.md records which parts of the core are
app code and which are library code sitting in the wrong crate — including a
measured case for moving spectral out of twinleaf-tools, which would roughly
halve the app's dependency graph. It applies to the macOS app equally, since
both build the same core.