flex-sim
June 22, 2026 · View on GitHub
Status: design draft v0.1 · 2026-06-15 · Nigel (G0JKN) + Claude (research laptop, on the road)
Working name: flex-sim (placeholder — see Open Decisions)
Relationship: standalone sibling to tci-monitor; together they form a closed-loop test rig.
1. Why
AetherSDR's waterfall/spectrum code churns constantly (#3578, #3586, #3457, #3182, #3031…) with no synthetic test coverage — every fix is eyeballed against whatever live signal happens to be on the air. Known waterfall bugs sit open and hard to reproduce: #2126 (waterfall blanks/flickers during TX) and #1916 (waterfall disappears after CW TX).
We need a controllable spectrum source that injects known patterns — min→max level ramps, calibration tones, sweeps, impulses, TX-blank repro — so waterfall behaviour becomes reproducible and assertable instead of "looks wrong to me."
2. What it is (and isn't)
- IS: a standalone app that emulates a FlexRadio's data plane — answers SmartSDR discovery + control, then streams synthetic VITA-49 panadapter/FFT/meter data to AetherSDR, driving AE's own waterfall from a programmable signal engine.
- ISN'T: a TCI tool. TCI is AE's spectrum output to clients; this drives AE's input — a different plane.
tci-monitorstays the passive observer;flex-simis the active stimulus. - Closed loop:
flex-simstimulates AE's input →tci-monitorobserves AE's output (TCI spectrum, meters) → assert. Stimulus + measurement = a real test rig.
3. Goals / non-goals
Goals: deterministic, repeatable spectrum patterns; accurate dB levels (a calibrated test card); programmable rate / span / bin-count; reproduce known waterfall bugs on demand; eventually CI-able (golden-image or numeric readback). Non-goals (v1): real IQ/audio streams, TX, full radio-control fidelity, being a usable "fake radio" to actually operate. Only enough fidelity to drive the spectrum/waterfall pipeline.
4. The protocol reality (the hard part — read this first)
To make AE render a waterfall, flex-sim must impersonate a Flex 6000 closely enough that AE will:
- Discover it — periodic UDP discovery broadcast (Flex discovery datagram). Exact port/format: CAPTURE from AE's discovery decoder or a real radio — do not guess.
- Connect — AE opens TCP :4992;
flex-simruns the control server speaking the FlexLib command grammar (Cn|…commands,Rn|…replies,Sn|…status). Must handle enough to: report version/handle, accept subscriptions, create a panadapter (display pan create …→ returns a stream id), accept slice/meter subs, and signal data is flowing. - Stream data — VITA-49/UDP packets: panadapter/FFT frames (bin amplitudes → pan + waterfall) and meter packets, framed per Flex's VITA-49 usage (stream id, class id, packet type, payload), at the negotiated rate to AE's data port.
⚠️ #1 risk / critical path: the exact command set + VITA-49 framing. Lift it from AE's own source — AE already has the decoder, which is the precise encode spec — and/or capture a real AE↔Flex 6300 session (Nigel has the 6300/6700; do when home). Phase 0 exists to retire this risk before anything else is built.
5. Architecture (layers)
- Discovery responder (UDP) — advertise "a radio" so AE lists/auto-connects it.
- Control server (TCP 4992) — FlexLib command/reply/status; stream-id allocation; pan/slice/meter lifecycle.
- VITA-49 emitter (UDP) — packetise FFT frames + meters at the negotiated rate to AE's data port.
- Signal engine — produces FFT frames per the Test-Signal Spec (§7). Parameterised: span, center, bins, frame-rate, level (dBFS/dBm), pattern, animation. Deterministic / seedable.
- UI (Qt) — pattern picker, level/rate/span controls, presets, start/stop, live subscription status.
- (Phase 3) readback/assert hook — capture AE's TCI spectrum output via
tci-monitor, or screen-grab, for golden-image / numeric assertion.
6. Tech stack
Recommend C++/Qt + CMake — same as tci-monitor: shared suite, build/release pipeline, UI components, and protocol-struct reuse with AE. High-rate VITA-49 streaming wants native perf.
De-risk: do a throwaway Python protocol spike first (sockets + struct) to nail the discovery+control handshake cheaply, then port to C++. Protocol-first.
7. Test-signal spec (the reusable gold)
Each pattern parameterised by span / center / bins / frame-rate; deterministic; serializable as a preset.
noise_floor(level)— flat floor at dBFS L. Baseline.ramp(min,max,period)— whole-span level swept min→max. Tests level→colour mapping, auto-black (#3586), clipping, floor consistency (#3483).cal_tones([(freq,dBm)])— fixed carriers at known levels → verify the dB scale is accurate (the literal test card).swept_carrier(start,stop,rate)— single tone across the span → frequency mapping + tile decode (#3457, the 1 GHz ceiling).comb(n,spacing,level)— multi-tone → dynamic range / simultaneous bins.step(low,high,dt)/impulse(level,width)— temporal/dynamic response: scroll timing, averaging, history reprojection (#3578), paced fallback (#3182).tx_blank(pattern,blank_ms)— deliberately drive the gap/zero-fill that mimics TX, to reproduce #2126 / #1916 on demand.staircase— center signal stepping floor→max in N even amplitude steps (renders as a colour ladder — reads AE's colormap/black-threshold live);noise— random noise band, tunable white→pink tilt. Signal width is set in kHz (--width-khz, tracks AE's span).carrier(added 2026-06-15) — steady tone on the VFO at a settable dBm; paired with the meter plane (below) it makes AE's S-meter read the injected level → a calibrated S-meter test card. Floor/level controls are now in dBm with the S-unit shown (S9 = −73 dBm, 6 dB/S-unit).- Meter plane (PCC 0x8002), added 2026-06-15: the sim defines the slice S-meter (
meter … src=SLC nam=LEVEL unit=dBm) and streams the dBm at the VFO each frame (), so AE's S-meter tracks the injected level for every pattern. Closes the dB-accuracy goal end-to-end (panadapter scale + S-meter). SeePROTOCOL.md§5 / §8.6. - TX meter plane, added 2026-06-15: keying the sim (panel TX toggle, or AE's own
transmit set mox=1/tune=1) makes it emitinterlock state=TRANSMITTING+transmitstatus and stream FWDPWR (dBm→W,100 W = 50 dBm) + SWR meters, so AE's TX power/SWR readouts move — no RF, emulated radio only. Meters stream continuously (~0 W / 1.0 SWR when not keyed) so the readout decays back on de-key. Settable W + SWR from the panel. SeePROTOCOL.md§8.7. - CW + CWX keying, added 2026-06-15:
cwpattern (panel CW normal / CW full break-in buttons) sim-keysCQ CQ CQ TST…at 20 wpm; and the sim handles AE's own CWX keyer (cwx send/wpm/qsk_enabled/clear) so the keying originates from AE (authentic CW-TX path). Keying runs at 50 fps for sub-dit timing, holds a 0.6 s tail, follows AE's QSK, and reportscwx queue=0on drain. Finding: AE handles the waterfall across CW-TX gaps robustly (no #2126/#1916); the audio breaks are AE's localCwxLocalKeyersidetone (the sim sends no audio) — a known AE area (PRs #3202/#2754/#2181), still reproducing on Windows v26.6.3. Also fixed a real sim concurrency bug (unserialized TCP writes from the stream + command threads). SeePROTOCOL.md§8.8. - Interim live control (web), added 2026-06-15: the spike serves a browser control panel (
--ctl-port, default 8731) — pattern picker + live sliders for noise floor / signal level (dBm + S-unit) / signal width / noise-color, driven from the host browser, no restarts. (The full Qt GUI is still Phase 1; this is the interim bench.) The picker shows a per-pattern "what to look for" hint (incl. the relevant AE issue #) so the bench is self-documenting. - Faithful waterfall
AutoBlackLevel(2026-06-16): each waterfall tile now carries the frame's measured noise-floor raw intensity (min(intens)) in theAutoBlackLevelfield, instead of a token value — so AE's radio-auto-black path (PR #3586) gets a real low/black point. Makes flex-sim a deterministic bench for verifying #3586 (drive the floor, confirm the waterfall floor tracks it evenly).
8. Phased plan
- Phase 0 — protocol spike (retire the #1 risk): minimal discovery + control so AE connects and opens a pan; emit ONE static flat frame; confirm anything renders in AE's waterfall. Python is fine here. Proves the handshake before we invest.
- Status (2026-06-15): ✅ LIVE-VALIDATED against real AetherSDR v26.6.3. Sim in WSL2, AE on the Windows host (separate network stacks via
localhostForwarding=false). Confirmed working: discovery, full handshake, panadapter + waterfall both rendering, VFO 3.705/LSB, coherent frequency, S-meter reading the injected dBm, TX power/SWR meters (key → 100 W/1.2, de-key → decays to 0/1.0), and CW/CWX keying (sim-keyed buttons + AE-driven CWX, normal & break-in).loopback_test.py(mock-AE) also passes offline across all patterns (now asserting the meter def + meter packets too). Findings on the CW-TX waterfall (robust, no #2126/#1916) and the AE-local CWX sidetone stutter are inPROTOCOL.md§8.8. Four AE quirks pinned along the way — seePROTOCOL.md§8 (RF_frequency status key; ±0.25 MHz waterfall XVTR tolerance;blackThresh=160−black_levelcolormap; plain-Hz tile freq).
- Status (2026-06-15): ✅ LIVE-VALIDATED against real AetherSDR v26.6.3. Sim in WSL2, AE on the Windows host (separate network stacks via
- Phase 1 — MVP: C++/Qt;
noise_floor+ramp+cal_tones; level/rate/span UI. Enough to exercise auto-black (#3586) and level accuracy. - Phase 2 — full pattern library + dynamics:
swept_carrier,comb,step,impulse,tx_blank. - Phase 3 — closed loop + CI:
tci-monitorreadback / golden-image asserts; package as a regression fixture; candidate upstream contribution to AE.
9. Strategic upside
AE has zero synthetic spectrum test coverage for a fast-churning waterfall codebase. Scoped as a QA/regression fixture (CI-able with golden images), flex-sim becomes the waterfall test rig the project doesn't have — a high-value, non-disruptive thing to bring upstream once it's solid.
"1 Aether, many radios" (Nigel, 2026-06-15): because each emulated radio is just a discovery + control + VITA stream with its own serial/IP/port/stream-ids, the sim can advertise N radios at once (multiple instances, or one process serving many). That makes it a multi-radio test bench (#3445) with zero hardware — nobody can exercise AE's multi-radio support today without a rack of physical rigs. Design discovery + stream-id allocation to allow several radios to coexist on one host when we move past Phase 0.
9.1 What flex-sim tests — and what it can't (the data/render boundary)
flex-sim is a data-correctness rig: it injects known spectrum data (bins, levels, frequencies) and verifies AE decodes and maps it faithfully — the dB scale, frequency mapping, S-meter, auto-black, level→colour. The ruler suite (run_tests.py) certifies exactly this class.
It is not a render-correctness rig. Bugs that live downstream of the data — in compositing, rasterization, GPU-vs-software parity, z-order, anti-aliasing, line-width drawing — are invisible to flex-sim, because they don't depend on what the signal is. Issue #3731 is the canonical example: a GPU-path z-order bug painted the panadapter grid on top of the FFT trace ("comb of vertical lines on the peaks"). flex-sim would send a perfect two-tone and the comb would appear identically — the defect ignores the data entirely. That bug needed a golden-image (pixel visual-diff) check, not a data check.
So the clean line: flex-sim answers "is the data right?"; a golden-image rig answers "is the picture right?" They're complementary, not the same tool. This is the boundary to keep in mind when scoping coverage — and it's why Phase 3's "golden-image asserts" is a distinct piece of work, not just more ruler patterns.
Where flex-sim still helps the render rig: it's the ideal deterministic signal source for capturing golden images. A known two-tone / noise_cal bed / test_card is a far better visual-diff subject than whatever live band noise happens to be on air — reproducible frame in, reproducible picture out. So flex-sim is the stimulus for a render-diff harness even though it can't do the diffing itself: data-rig and image-rig share flex-sim as their common, trustworthy input.
10. Open decisions (need Nigel)
- Name:
flex-sim/flex-sim/spectrum-bench/vita-forge/ other? - Stack: confirm C++/Qt for the real tool, with a Python protocol-spike first? (recommended)
- Emulation target: Flex 6000 specifically (matches AE's primary data plane) — or does AE have a simpler/generic SDR ingest we could target with less protocol surface? (Check AE source.)
- Repo: standalone
nigelfenton/<name>when we cut Phase 1. - Capture plan: grab a real AE↔Flex 6300 session when home to pin the exact handshake + VITA-49 framing, and/or read AE's decoder source.
11. When-home dependencies
- Read AE source (the VITA-49 decoder = encode spec, plus the FlexLib command handling) — needs hub/aurora reachable.
- Optional real-traffic capture from the 6300 — when home, when the Comcast/Lusby outage clears.