Repository navigation
Releases: nigelfenton/flex-sim
Release list
v0.3.6 — several clients at once (MultiFlex + bound controllers)
The Flex radio sim now serves several API clients at once, as a real FlexRadio does. A controller can sit beside AetherSDR and drive its slices, for example a CTR2 through AetherSDR's CTR2 Relay. With only AetherSDR connected, nothing changes.
About the binaries below: they are the Flex radio sim (
flex_sim.py). The KPA1500, ANAN and station sims run from source.
Added: several clients at once (#24)
- Each connection is served on its own thread, with its own client handle (
V/Hgreeting) and its own send lock. The radio's slices, panadapters and transmit state stay shared. - GUI and non-GUI clients: a client that sends
client guiis a GUI client.sub client allreportsclient 0x<handle> connected … client_id=<uuid> program=… station=…for each one, anddisconnectedwhen it leaves. client bind client_id=<uuid>: a non-GUI controller binds to a GUI client, then sees and drives that client's slices (slice tune,slice set … mode=,audio_level=, filter,xmit/transmit set mox=). Status reaches every client that should see it.- Disconnects are per client: only the leaving client's own slices and pans are removed, and TX is dropped only if that client keyed it.
Fixed: autopan=1 re-announces the pan (#25)
slice tune <n> <MHz> autopan=1 now re-sends the panadapter status with its new centre, as display pan set center= already did. Before, AetherSDR kept the old centre while the sim framed the spectrum on the new one, so the carrier pattern appeared away from the slice flag.
Added: sim get / sim set level= (#25)
Flex sim-only API verbs, so a controller without access to the web control panel can read and set the signal level (the same value as the panel's signal-level slider):
sim getreplieslevel=<dBm> floor=<dBm> pattern=<name>.sim set level=<dBm>sets it, clamped like the slider.
A real radio rejects them. Documented in PROTOCOL.md as a flex-sim extension.
Tested with
AetherSDR's CTR2 Relay and a CTR2-Max running the AetherPanel firmware:
- the controller binds to AetherSDR's session
- the VFO tunes with
autopan=1and the pan follows, with the carrier on the flag - push-and-turn sets the signal level
- PTT and TUNE key the sim only
v0.3.5 — Flex sim SmartSDR v4.3 firmware (--firmware 4.3)
The Flex radio sim can now behave like a radio on SmartSDR v4.3.16, FlexRadio's release of 2026-10-13. Start it with --firmware 4.3 to test AetherSDR's 4.3 work without upgrading a radio. aethersdr/AetherSDR#6437 maps the wire changes this follows. Without the option nothing changes: the sim reports and behaves as before.
About the binaries below: they are the Flex radio sim (
flex_sim.py). The KPA1500, ANAN and station sims run from source.
Added: --firmware 4.3 (#19)
- Version: discovery and the connect greeting report
4.3.16.42034. Presets are3.3,4.1,4.2and4.3, or give a full version. - PTT Override (#6442): the transmit status reports
ptt_mic_override. A client can set it to 0 or 1; any other value is refused. Older firmware never reports it. - What a footswitch sends. On 4.3, a hardware PTT (MIC, ACC or RCA) transmits the selected source, DAX included. Before 4.3 it always forced the analog mic. PTT Override brings the old behaviour back.
- Simulate a footswitch with
http://<sim>:8731/hwptt?src=MIC&on=1. /stateshows what the radio would put on the air.
- Simulate a footswitch with
- Interlock sources: every latched source is listed, hardware first (
MIC,VOX,MIC,VOX,SW), and VOX is reported asVOX. - DSP levels: reported as
nrl_level,anfl_levelandnrs_level. NRS runs 0–50, andnrf_levelis ignored. - 48 and 96 kHz slice audio:
slice set N sample_rate=48000|96000streams with the new class codes (0x03E4/0x03E5, reduced bandwidth0x0124/0x0125). It switches live. - APD:
state=OFF|IDLE|ACTIVE, plus the new equalizer line with its antenna, frequency and power. - UDP keepalives:
udp_registerandpingare counted in/state. - DAX TX rate: the TX chain check gains stage 3b: TX audio rate must match the TX slice, which 4.3 firmware enforces.
- CWX: the
0x1Estand-in for a quote insidecwx sendis decoded.
Fixed
UDP keepalives were counted as DAX TX audio. AetherSDR sends its UDP keepalives to the radio's port 4991, which is also where the sim watches for DAX TX audio. So a client ping every 5 s could make the TX chain's "audio arriving" stage flicker true with no audio flowing. Keepalives are now recognised and counted separately, whatever the firmware setting.
Tested
tests/test_firmware_43.py: 44 checks, each behaviour run against an old-firmware sim and a 4.3 one.- The full suite and the offline loopback self-test pass.
- Against a real AetherSDR: the #6292 build connected to a
--firmware 4.3sim, showed version 4.3.16.42034, and took the new PTT Override key without complaint.
Documented in PROTOCOL.md §12.
One assumption: whether real 4.3 firmware ignores the dropped nrf_level command or refuses it isn't known. The sim ignores it.
Not covered: SmartLink WAN sessions, APD's TRAINING/HOLD/FAULT states, and the FLEX-8000/Aurora-only features (AutoDX, NTP, Noise Floor Range).
v0.3.4 — Flex sim licensing options: subscription, features, DVK refusal, live changes
The Flex radio sim can now report any licence a real radio might: SmartSDR+ subscription, licensed features and DVK refusal. You set it at startup or change it live while AetherSDR is connected. This was asked for by K5PTB, to test AetherSDR against different licences without a radio for each one.
About the binaries below: they are the Flex radio sim (
flex_sim.py). The KPA1500, ANAN and station sims run from source.
Added (#17)
-
The full licence report.
sub license allis answered with the three licence objects a real radio sends:licenselicense subscription- one
license featureline per feature
By default the sim reports an active SmartSDR+ subscription with the Digital Voice Keyer licensed.
-
Startup flags:
--subscription none|smartsdr+|early-access--subscription-expires YYYY-MM-DD(a past date gives an expired subscription)--license-feature NAME=0|1[:REASON], repeatable, for any feature--dvk-refuses
--no-dvk-licensestill works. -
Live changes, no reconnect.
http://<sim>:8731/licensechanges the licence while AetherSDR is connected and pushes the new status straight to it. For example,/license?feature=digital_voice_keyer&enabled=0or/license?subscription=none. With no query it shows the current licence.- Bad input is refused and changes nothing.
- Rack mode takes
&radio=N.
-
DVK refusal. With
--dvk-refuses(or?dvk_refuses=1), everydvkcommand is refused with50004001. A real radio does this when it won't store recordings, sometimes even while it reports the feature as enabled.
tests/test_licensing.py covers all of this (20 checks), and PROTOCOL.md §11 documents it.
Checked end to end with the aethersdr/AetherSDR#6292 build. With the voice keyer source on Automatic:
- AetherSDR uses the Radio keyer while the licence is on.
- Turning the licence off live switches it to Local and removes Radio DVK from the menu.
- Turning the licence back on switches it back to Radio.
Testing tips:
- Start from Automatic. A saved Local or Radio choice overrides the licence.
- AetherSDR only upgrades its subscription label during a session, so reconnect to test a downgrade.
v0.3.3 — Flex sim TX audio state: mic source, DVK licence, reconnect and client-drop
Five fixes to the Flex radio sim's transmit-audio state. These are behaviours that AetherSDR's client-side voice keyer (aethersdr/AetherSDR#6292) depends on. Before this release, testing that PR meant patching the sim first. Anyone testing AetherSDR's mic source, DVK or TX recovery against the sim should update.
About the binaries below: they are the Flex radio sim (
flex_sim.py). The KPA1500, ANAN and station sims run from source.
Fixed (#15)
- Mic source.
mic listnow includes PC like a real 6000.mic input <src>selects the source, and the transmit status reportsmic_selection=anddax=so a client can read them back. AetherSDR's voice keyer refuses to record unless the radio reports PC, so recording could not be tested before. - DVK licence. The sim now answers
sub license allwithdigital_voice_keyerenabled. The new--no-dvk-licenseflag makes it a radio without SmartSDR+. AetherSDR assumes the licence is present when it hears nothing, so every sim used to look licensed, and the unlicensed path (the one the client-side keyer exists for) could not be reached. remote_audio_txgets its own stream id. It used to sharenetcw's, so AetherSDR held one id for two streams.- Reconnecting while keyed. The sim answers
sub tx allwith its current TX state, so a client that reconnects mid-transmission learns that the radio is keyed. - A client that disconnects while keyed is unkeyed, as on a real radio. The sim used to stay keyed, so the next client inherited a carrier nobody owned.
tests/test_tx_audio_state.py covers all five (11 checks), and PROTOCOL.md §10 documents them. Checked end to end with the #6292 build and no patches:
- with
--no-dvk-license, AetherSDR picks its Local keyer by itself - the mic switches to PC and reads back from the radio
- a client dropping while keyed leaves the sim unkeyed
v0.3.2 — Flex sim TX routing for TCI; KPA1500 amplifier sim
Fixes to how the Flex radio sim handles transmit routing. This release also brings the new KPA1500 amplifier simulator. Anyone driving the Flex sim through TCI, or from an API client other than AetherSDR's GUI, should update.
About the binaries below: they are the Flex radio sim (
flex_sim.py). The KPA1500, ANAN and station sims run from source.
Flex sim: transmit routing (#13)
Found while driving AetherSDR 26.10.1's TCI server from CAT4OM, a third-party CAT hub, with flex-sim as the radio. Each gap made a correct client look broken:
xmit 1/xmit 0now key the radio. AetherSDR turns a TCItrx:0,true;intoxmit 1, and so do other API clients. The sim ignored it, so the radio never reported keyed and AetherSDR correctly dropped the PTT after its confirmation timeout. A malformedxmitkeys nothing.- A new slice is announced before the
slice createreply. AetherSDR's TCI VFO-B route looks the new slice up when the reply arrives. It found nothing, abandoned the slice and created another on the next request, so slices leaked (A, B, C …). slice set N tx=1moves the transmit slice. The sim ignored it, so TCI split never reached AetherSDR.
tests/test_tx_routing.py covers all three (19 checks; 10 fail on v0.3.1), and PROTOCOL.md §9 documents them. With these fixes, AetherSDR + CAT4OM passed 19/19 end-to-end checks against the sim. These covered frequency, mode, VFO B, split, PTT, and releasing TX when the controlling client drops.
New: KPA1500 amplifier simulator (kpa_sim.py)
An Elecraft KPA1500 built against Elecraft's Programming Reference V3. It is for aethersdr/AetherSDR#4097: can a KPA1500 be keyed safely over the LAN?
- Network PTT (#9):
^TX;/^TXnn;(keyed with a timeout) /^RX;/^TQ;on TCP and UDP:1500, with a T/R interlock bench. Runflex_sim.py --rf-sense 127.0.0.1:1510so the radio tells the amp when its RF is on. ^NIand the ACC INHIBIT line (#10).- The internal ATU (#12):
^AM ^AI ^AE ^FT ^FE ^TP, for aethersdr/AetherSDR#5905.
Also
- Run the sims as always-on services on Linux (#11): example systemd units in
deploy/systemd/and a README section.
v0.3.1 — ANAN sim: real I/Q handedness
A correctness fix for the ANAN-G2 / openHPSDR Protocol 2 simulator. Anyone using anan_sim.py from v0.3.0 should update.
About the binaries below: they are the Flex radio sim, rebuilt with only a version bump. The ANAN / Protocol 2 tools run from source.
The fix
- The sim's I/Q handedness was mirrored. On the first run against AetherSDR's ANAN-G2 backend (aethersdr/AetherSDR#5143), the "+1 kHz" test tone was drawn 1 kHz below the dial and was audible only in LSB. AetherSDR's polarity had been measured on a real G2 against WWV and cross-checked with an RSP1B running SDR++, so the sim was the one in error: the real HPSDR wire is the conjugate of the textbook convention. A correct client now shows the tone just above the dial and hears it in USB (#8).
- Why it matters: a mirrored sim would push a correct client towards a "fix" that breaks it on real hardware.
- Caught by a test now.
tests/test_anan_p2.pyasserts the tone's signed frequency; nothing checked it before, because every frame was the right size.
Also
- High Priority frequencies are decoded as phase words (Hz = word × 122.88 MHz / 2³²), so a client tuned to 10.000 MHz is no longer logged as "349.525333 MHz". Discovery advertises it (byte 21 = 1).
- The README's "not yet run against AetherSDR's ANAN backend" warning is retired — it now has been, end to end.
v0.3.0 — ANAN / Protocol 2, a whole station, and a noise bench
The biggest update since the test bench. flex-sim grows a second radio family — an Apache Labs ANAN-G2 / openHPSDR Protocol 2 receiver simulator — plus a whole station of accessory simulators, an HF noise bench for testing noise reduction, radio-side notch filters, a TX slice, and a real CI suite.
About the binaries below: they are the Flex radio sim (
flex_sim.py). The ANAN / Protocol 2 tools and the accessory simulators run from source — pure Python standard library, no install. See the README.
ANAN-G2 / openHPSDR Protocol 2 (new)
anan_sim.py— answers discovery, completes the General → DDC-Specific → DUC-Specific → High-Priority handshake, and streams DDC I/Q at the sample rate the client commands. Enough for a Protocol 2 client to find the radio and paint a panadapter and waterfall. Receive only.- Checked against more than itself. RX geometry confirmed against real ANAN-G2 captures (16 B header + 1428 B = 238 samples — the widely-quoted 1440 B / 240 is the TX layout, #3); an independent probe (
tools/p2stream.c) built from piHPSDR's offsets; and an end-to-end session with NereusSDR 0.5.2, which found four sim defects the documentation-derived checks could not — session-port streaming, the DDC enable mask, commanded re-rating, and Windows pacing (#4, #6; reported in #5). p2verify.py— a stdlib pcapng reader that reports what a real Protocol 2 radio puts on the wire, and tells DDC (RX) from DUC (TX) by each frame's own declared fields rather than by port. It guards against nanosecond pcapng timestamps and multicast traffic hiding the session (#7).- ⚠ Not yet run against AetherSDR's ANAN backend (aethersdr/AetherSDR#5143) — which is exactly what it is for. Reports welcome. One observation remains open: an RX-audio click comb in NereusSDR with a clean wire (#5).
- The scoping doc is corrected: it had the RX packet geometry wrong (#7).
A whole station, no hardware
- SPE Expert amplifier sim (
spe_sim.py, :4531), including the ser2net / RFC 2217 power-on path, and a scripted wire bench (spe_bench.py). - ACOM 600S amplifier sim (
acom_sim.py, :9600). station.pynow hosts SPE and ACOM alongside the Antenna Genius, Power Genius XL and Tuner Genius XL — five accessories, one process, one prompt — plus--radio-portand--patternfor the radio it spawns.- Amplifier meters published, with per-unit meter scaling fixed.
HF noise bench
- Additive HF noise for testing AetherSDR's noise reduction, with text-to-speech voice fixtures so there is something realistic to recover.
- The noise scene renders on the waterfall, tones sit at their true frequency via the live span, and noise rises from the floor rather than floating as a band above it.
Radio-side emulation
- Tracking Notch Filter and Auto-Notch Filter, emulated end to end.
- A TX slice (
tx=) and 2-SCU diversity RX. - Radio-side two-tone generation and
--txlog. - DAX TX chain observer — names the first missing stage rather than the symptom — with multiFLEX contention modelled.
Models, CLI and wire
- FLEX-8400 added and the FLEX-8600 slice count corrected — thanks @Ozy311 (#1).
--modelsselects the model for a single radio too; the real model is logged.--serialprefix, and the serial now defaults to the hostname, so two sims on one network are distinguishable.- Waterfall tiles encode VitaFrequency (Hz × 2²⁰), following AetherSDR #4412's removal of the plain-Hz fallback.
Fixes
sig_cwkeys real Morse — duration and key state had been conflated.- SPE: a partial sync run is carried across receive boundaries.
- The accessory sims now ship, so
station.pyruns from a clean clone. - The control panel's noise controls were dead; rebuilt.
Docker and CI
- The Docker image ships every sim, not just the radio.
- The test suite actually runs now. It was previously uncollectable —
pytestreported "no tests ran" and exited 0. It runs on every push and PR on Linux and Windows, and CI fails if it ever collects fewer than four tests again.
v0.2.0 — test bench + realism
A big usability + realism update. flex-sim's control panel is now a proper test bench, and the sim can drive AetherSDR's RADE/digital RX path.
Test bench (control panel)
- Run calibration rulers from the panel — per-ruler checkboxes + a Run button; results stream into the page and reports are listed in-browser (no more SSHing in). The fast runner uses its own mock-AE loopback and coexists with a live sim.
- "Live test on real AE" — a guided walkthrough that drives YOUR connected AetherSDR through each ruler (~5 s each, pausable), so you watch each signal appear on the panadapter/waterfall while the measured-vs-expected numbers stream alongside. Seeing is believing.
- Fixtures dropdown — pick a WAV from
fixtures/(size + duration shown) instead of typing a server path. - Grouped patterns — dropdown organised into Realistic / Calibration rulers / Diagnostic / Keying.
Real Stop/Go + status
- Stop/Go button — pauses ALL output (spectrum + audio) while staying connected to AE, so the panadapter and waterfall go dead; Go resumes. (The old "Stop" only changed the pattern.)
- Live connection-status bar — waiting-for-AE / connected (with peer IP) / streaming
<pattern>· dBm, or a clear "output STOPPED".
Realism
- SSB voice lookalike pattern — a ~2.4 kHz USB-offset band with a syllabic envelope, word-gap pauses and drifting formants, so the waterfall reads like someone talking on HF (zoom AE's span in to see the detail). Sits alongside the geometric calibration patterns.
RADE / digital RX
- dax_rx stream support — the sim now answers AE's
stream create type=dax_rx, registers the stream, and streams full-bandwidth float32 so AE's RADE/digital decoder gets a valid signal. Modem decode proven (CRC-valid EOO callsign).
Other
- Audio defaults to silence — the sim no longer blasts a 440 Hz tone on connect; Tone/Noise/WAV are opt-in.
Prebuilt binaries for windows-x64 / linux-x64 / linux-arm64 / macos-arm64 are attached once CI finishes.
v0.1.1
flex-sim v0.1.1 (beta) — a synthetic FlexRadio-6000 emulator / hardware-free test bench for AetherSDR. No radio required.
New in 0.1.1: Linux ARM64 binary — runs on a Raspberry Pi and other ARM boxes.
Download & run
Grab the binary for your machine, run it on a computer on your network (ideally not the same one as AetherSDR — see the README's one networking rule), and AetherSDR discovers it as a FLEX-6600. Then open the control panel at http://<that-ip>:8731/.
- Windows:
flex-sim-windows-x64.exe - Linux (PC / x86-64):
flex-sim-linux-x64 - Linux (Raspberry Pi / ARM64):
flex-sim-linux-arm64 - macOS (Apple Silicon):
flex-sim-macos-arm64
⚠️ Antivirus may flag the .exe
It's unsigned, so SmartScreen / Norton / Defender may warn or quarantine it. This is a false positive — PyInstaller bundles Python into one file and trips AV heuristics. The full source is in this repo (pure-stdlib Python, zero deps); allow the file, or run from Python to skip the binary entirely.
See the README for full setup, rack mode, and the control panel.
v0.1.0
flex-sim v0.1.0 (beta) — a synthetic FlexRadio-6000 emulator / hardware-free test bench for AetherSDR. No radio required.
Download & run
Grab the binary for your OS below, run it on a computer on your network (ideally not the same one as AetherSDR — see the README's one networking rule), and AetherSDR will discover it as a FLEX-6600. Then open the control panel at http://<that-ip>:8731/.
- Windows:
flex-sim-windows-x64.exe - Linux:
flex-sim-linux-x64 - macOS (Apple Silicon):
flex-sim-macos-arm64
⚠️ Antivirus may flag the .exe
It's unsigned, so SmartScreen / Norton / Defender may warn or quarantine it. This is a false positive — PyInstaller bundles Python into one .exe and that trips AV heuristics. The full source is in this repo (pure-stdlib Python, zero deps); allow the file, or run from Python to skip the binary entirely.
See the README for full setup.