Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
Breaking changes¶
- The
commandsCW-pitch and key-speed builders take the raw level (MOR-2473, MOR-2481).rigplane.commands.set_cw_pitchandrigplane.commands.set_key_speednow take the raw 0-255 level instead of Hz/WPM; a caller still passing Hz getsValueError(Hz 300-900 is outside the 0-255 level band), but a caller still passing WPM does not: 6-48 are valid levels and are sent as raw levels. The Hz/WPM conversion moved intoCoreRadio.set_cw_pitch/CoreRadio.set_key_speed, which encode through the active profile's[controls.cw_pitch]/[controls.key_speed]band and its newencode_roundingkey ("ceil"/"nearest_half_down") viarigplane.profiles.control_domain.encode_legacy_control. A CI-V profile control withoutencode_roundingmakes theCoreRadiosetter raise. The X6100's CW pitch and key speed move to its documented 400-1200 Hz (10 Hz step) and 5-50 WPM bands, and the X6200 now writes its documented 400-1200 Hz CW-pitch band instead of raising above 900 Hz. Callers of theRadioprotocol methods are unaffected. - The
RigctldFallbackCacheprotocol is deleted (MOR-2483). Removed the exportedrigplane.core.radio_protocol.RigctldFallbackCacheprotocol.RigctldRoutable.rigctld_routing,create_routing,YaesuRouting.__init__andYaesuCatRadio.rigctld_routingno longer takecache; a third-party backend implementingrigctld_routingmust drop that parameter.
3.0.0b1 — 2026-09-05¶
Migration from 2.11.1¶
- Core 3.0 beta uses selective compatibility. The migration guide maps the frozen compatibility decisions to Python, receiver-state, HTTP, extension-host, CLI, profile and rigctld consumer examples. All TX consumers must adopt canonical ownership, admission, completion and OFF; the former leave-keyed lifecycle is not emulated. Momentary PTT and latched TRANSMIT are distinct operations.
- The package version is
3.0.0b1. HTTP contract version 1 and the extension-host contract (numeric 2, manifesthost_api: "2.0") are separate contracts. The host requires the explicit new declaration while retaining manifest schema version 1. Installed-candidate compatibility is a separate release gate; no full 2.x compatibility is claimed. -
Core includes SDR preservation against the 2.11.1 behavior baseline. Browser, packaging/rollback and hardware acceptance remain release gates; Pro packaging and release follow separately.
-
Profile aliases retain their released behavior. The 3.0 compatibility bridge restores the deprecated read-only
RadioProfile.vfo_swap_codeandRadioProfile.vfo_equal_codeproperties after their development removal. Their exact released expressions areswap_main_sub_code or swap_ab_codeandequal_main_sub_code or equal_ab_code. New consumers should select the explicit A/B or MAIN/SUB operation appropriate to their radio; see the migration guide. - Icom CTCSS tone/TSQL setters accept the public integer-centiHz
freq_hzkeyword and retainfreq_centihzas an alternative. Supplying both non-Nonespellings or neither raisesTypeErrorbefore transport; there is no unit scaling. Seesrc/rigplane/runtime/radio.py: CoreRadio.set_tone_freqandCoreRadio.set_tsql_freq, and the migration guide.
Breaking changes¶
- Tone-capable rig profiles must now declare a named CTCSS table (MOR-2129).
Any profile with
repeater_tone,tsql, orsql_typemust include[ctcss] table = "<name>", resolving through the versioned sibling catalogrigs/_ctcss_tables_v1.toml. Missing, unknown, or malformed references now fail profile loading. The resolvedRigConfig.ctcss_tones_centihzandRadioProfile.ctcss_tones_centihzvalues are immutable ordered integer centiHz tuples (8850= 88.5 Hz); order defines provider index mapping, but a reference does not add capabilities or grant a setter. The four shipped tone profiles (IC-705, IC-7300, IC-9700, FTX-1) referencestandard_50; IC-7610 remains deliberately unreferenced. - The shared state-cache poll helpers are deleted. Removed:
rigplane.runtime._shared_state_runtime(the whole module — itsDEFAULT_STATE_CACHE_TTL,is_cache_fresh,poll_frequency,poll_modeandpoll_standard_fields) and the legacy alias shimrigplane._shared_state_runtime. Importing either module path now raisesModuleNotFoundError.rigctldkeeps its own TTL inRigctldConfig.cache_ttl. - The
rigctldserver's_pollerconstructor hook is deleted. Removed fromrigplane.rigctld.server: the_pollerkeyword argument ofRigctldServer.__init__, theself._pollerattribute, and the poller start/stop andwrite_busy/hold_forpacing sites that read it inRigctldServer.stop,RigctldServer._on_client_done,RigctldServer._wsjtx_compat_prewarmandRigctldServer._handle_client, together with the module-level_is_packet_mode_sethelper that gated the packet-mode hold. Passing_poller=toRigctldServernow raisesTypeError. rigplane.commands.parse_scope_span_responseandrigplane.commands.scope_set_spanrequire the span-preset table as an argument (MOR-2258). The module constant that held the eight Icom CI-V span values (span code 0-7 -> Hz) insidesrc/rigplane/commands/scope.pyis deleted; the table now lives only inrigs/*.toml([scope].span_presets_hz) and reaches these two functions asRadioProfile.scope_span_presets_hz, passed in by the caller becausecommands/may not importprofiles/(.importlinter).parse_scope_span_responsetakes it as a second positional parameter,scope_set_spanas a required keyword-onlypresets=; both raiseTypeErrorif it is omitted. The legal span index range is now0..len(presets) - 1rather than a fixed0..7. Every caller in this repository (runtime/_civ_rx.py: CivRuntime,runtime/_scope_runtime.py: ScopeRuntimeMixin) reads the resolved profile and passes it; an external caller must do the same. At this commit every shipped profile that declaresget_scope_span(ic7300,ic705,ic7610,ic9700) also declares[scope].span_presets_hzwith the same eight values, so no shipped radio changes behaviour.- The
rigctldRadioPolleris deleted. Removed:rigplane.rigctld.poller(the module, itsRadioPollerclass,_mode_to_hamlib_str,_get_mode_reader, and_STATS_LOG_INTERVAL) and its## RadioPollersection indocs/api/rigctld.md. At that entry's parent commit nothing undersrc/constructed the class. - The misnamed
tx_freq_monitorfield and commands are deleted; XFC is the real transmit-frequency monitor (MOR-2246). Removed: theTransceiverStatusCapableprotocol (and itsrigplane.TransceiverStatusCapableexport) with itsget_tx_freq_monitor/set_tx_freq_monitormethods; the concreteCoreRadio.get_tx_freq_monitor/CoreRadio.set_tx_freq_monitorimplementations; therigplane.commands.get_tx_freq_monitor/set_tx_freq_monitorbuilders (0x1C 0x03); theget_tx_freq_monitor/set_tx_freq_monitorweb command-API entries (docs/api/command-catalog.md); thetx_freq_monitorkey inRadioState.to_dict(); and thetxFreqMonitorfield on the JSON/WebSocket state API.Radio(the coreProtocolconsumers depend on) never declaredget_tx_freq_monitor/set_tx_freq_monitor— they lived only onTransceiverStatusCapable, whichRadiodid not inherit. Every shipped profile already declared these commands absent or left them undeclared, since 0x1C 0x03 is Read transmit frequency, not a transmit-frequency monitor toggle;get_xfc_status/set_xfc_status(0x1C 0x02) remain the real, unaffected transmit-frequency monitor. rigplane.commands.configbuilders requirecmd_map; there is no hardcoded fallback (MOR-2006, Steps 5..N module 1 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md).get_acc1_mod_level/set_acc1_mod_level, the USB/LAN mod-level pair, the DATA-OFF/1/2/3 mod-input pairs, andget_civ_transceive/set_civ_transceive/get_civ_output_ant/set_civ_output_antall took an optionalcmd_mapthat, unused, fell back to bytes written into the source years ago — on the IC-7300 those bytes collided withset_mic_gain(both built14 0B), so the ACC1 slider moved MIC gain (MOR-1992).cmd_mapis now a required keyword-only argument on all eighteen; a call that omits it raisesTypeError(naming the migration and pointing atcommands/bound.py: BoundCommandsin the message) rather than silently emitting the old, sometimes-wrong bytes. The only production caller in this repository,runtime/radio.py: CoreRadio, moved ontoself._commands.<builder>(...)in the same change, which supplies the map — but an external caller of the free function must do the same: reach these builders through a radio's bound commands rather than callingrigplane.commands.get_acc1_mod_level(...)(or any of its seventeen siblings above) directly with no map.rigplane.commands.levelsbuilders requirecmd_map; there is no hardcoded fallback (MOR-2006, Steps 5..N module 2 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md). The other half of the ACC1/MIC-gain collision above:set_mic_gain's fallback built the same eight bytes on the IC-7300 asset_acc1_mod_level's did. All fifty builders in this module — RF power/gain, AF level, squelch, APF/NR/PBT/notch/compressor/break-in/NB/DIGI-SEL/drive/monitor/VOX/anti-VOX levels, CW pitch, mic gain, key speed, and the REF-adjust/dash-ratio/NB-depth/ NB-width/VOX-delay menu-address group — now requirecmd_mapas a required keyword-only argument; a call that omits it raisesTypeErrorthe same way asrigplane.commands.config's eighteen. The only production caller,runtime/radio.py: CoreRadio, moved ontoself._commands.<builder>(...)in the same change — an external caller of a free function must do the same rather than calling it directly with no map.rigplane.commands.vfobuilders requirecmd_map; there is no hardcoded fallback (MOR-2007, Steps 5..N module 3 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md). VFO select/ swap/equal, split, tuning step, scan start/stop/type/df-span/resume, and dual-watch get/set now requirecmd_mapas a required keyword-only argument, the same way asrigplane.commands.config/levelsabove. Three behaviour changes ride along with the migration:set_dual_watch_off/set_dual_watch_onresolve the split, get_/set_-prefixed command namesset_dual_watch_off/set_dual_watch_oninstead of the bareset_dual_watchthe fallback used to resolve — IC-7610/IC-9700 declare the two directions at different wire addresses, which one shared name could never carry.quick_dual_watch()/quick_split()are deleted. Bench-confirmed on the live IC-7300,1A 05 0030/0032are persistent, readable/ writable menu toggles, not the one-shot triggers their names implied — the deleted builders always sent a bare read frame and their only caller never read the reply, so they fired nothing. Replaced by realget_quick_split/set_quick_split/get_quick_dual_watch/set_quick_dual_watchpairs;runtime/radio.py: CoreRadiogained matching methods andweb/radio_poller.py'sQuickSplit/QuickDualWatchWS commands now call the getter (a strict read improvement over the old fire-and-discard; writing a caller-supplied value through the web layer is not wired yet — the WS handler never parsed one for this intent even before this change).scan_start_type/scan_set_resumeno longer validate their sub-byte against a code-level frozenset (the deletedVALID_SCAN_TYPES, which omitted the documented 0x13 fine-ΔF scan on IC-7300/IC-7610/IC-9700 and IC-705's 0x24 mode-select scan; andVALID_SCAN_RESUME, which accepted 0xD0-0xD3 where every documented CI-V guide lists only 0xD0/0xD3). Domain validation moves toruntime/radio.py: CoreRadio.scan_start/scan_set_resume, reading the newRadioProfile.scan_type_values/scan_resume_values(declared per radio inrigs/*.toml's new[scan_types]/[scan_resume]sections), the same shapecommands/dsp.py: set_agcalready uses for AGC-mode domain validation.scan_set_df_span'sVALID_DF_SPANScheck is unaffected — every profile with a citation for it declares the identical 0xA1-0xA7 table, so it stays a wire-format constant. The only production caller,runtime/radio.py: CoreRadio, moved ontoself._commands.<builder>(...)in the same change — an external caller of a free function must do the same rather than calling it directly with no map.rigplane.commands.vfo_a_equals_bandrigplane.commands.vfo_swapare removed (mechanism audit D1; see.claude/audits/2026-08-30-mechanism-audit-vfo-swap-equalize.md). The MOR-2007 migration above madecmd_maprequired on both without changing their bytes; this change deletes them outright. Both had zero production call sites and built the identical frame toset_vfo(0xA0, ...)andset_vfo(0xB0, ...)respectively — both go through the same_build_from_map(cmd_map, "set_vfo", ...)call, so a caller wanting the raw bytes gets identical frames fromset_vfoeither way. The profile-driven replacements areDualReceiverCapable.swap_main_sub/equalize_main_subandVfoSlotCapable.swap_vfo_ab/equalize_vfo_abon theRadioprotocol.vfo_a_equals_b's hardcoded0xA0equalize byte matched no shipped dual-RX profile: IC-7610 and IC-9700, the only twomain_sub-scheme profiles, both declareequal_main_sub = [0xB1].rigplane.commands.pttbuilders requirecmd_map; there is no hardcoded fallback (MOR-2007, Steps 5..N module 4 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md).ptt_on/ptt_offnow requirecmd_mapas a required keyword-only argument, the same way asrigplane.commands.config/levels/vfoabove. No wire bytes changed: every CI-V profile already declared the identical[0x1C, 0x00, 0x01]/[0x1C, 0x00, 0x00]tuple the deleted fallback built (the one profile that used to disagree, X6100, was already fixed before this migration). The two production call sites, both inruntime/radio.py: CoreRadio—set_pttand the managed-TX_write_managed_ptt— moved ontoself._commands.ptt_on/.ptt_offin the same change, touching only the frame-building expression and nothing else in either method (the latter sits in the managed-TX transmit-interlock path).rigplane.commands.scopebuilders requirecmd_map; there is no hardcoded fallback (MOR-2007, Steps 5..N module 5 -- the last module of this migration series --docs/plans/2026-08-29-profile-driven-command-bytes.md). All thirty-three scope/waterfall builders (scope_on/scope_off, data output, main/sub, single/dual, mode, span, edge, hold, ref, speed, during-TX, center type, VBW, fixed edge, RBW — each get/set pair) now requirecmd_mapas a required keyword-only argument, the same way asrigplane.commands.config/levels/vfo/pttabove. No wire bytes changed: every CI-V profile already declared the identical[0x27, sub]tuple the deleted fallback built (this module's own 4 divergence rows —get_scope_center_typewithreceiver=0— closed earlier, in #2821). One behaviour change rides along:scope_set_center_typeno longer accepts areceiverkeyword — the latent setter-side twin of theget_scope_center_typefix in #2821 (MOR-1981).0x27 0x1Ctakes exactly one data byte, no selector, on all four official CI-V references and the two bench sessions behind #2821; areceiverbyte built a genuinely different, wrong frame rather than addressing Main/Sub. No production caller ever passed it. The production call sites, inruntime/_scope_runtime.py: ScopeRuntimeMixinandbackends/_icom_serial_base.py, moved ontoself._commands.<builder>in the same change.rigplane.commands.scope's fifteenparse_scope_*_responsefunctions take a map-derivedcommand/sub, closing the response-side gap the builder migration above left open (MOR-2008). Each now accepts optionalcommand/subkeywords, defaulting to the same_CMD_SCOPE/_SUB_SCOPE_*constants checked before this change, so a caller that never passes them —runtime/_civ_rx.py's unsolicited-frame decoding (no command-map entry to derive a shape from) and every pre-migration direct test call — keeps working unchanged. The sixteen production call sites inruntime/_scope_runtime.py: ScopeRuntimeMixin(fifteen getters plusset_scope_fixed_edge's self-reparse of the frame it just built, which sharesget_scope_fixed_edge's reply shape) now derive both values viaruntime/radio.py: CoreRadio._expect_shape, the mechanism batch 1'ssystem.pydate/time/UTC parsers and batch 3's twelve dsp.py getters already use. No observable behaviour change on any of the four CI-V profiles that declare scope commands: every one already declares the identical[0x27, sub]tuple these constants held (this module's own divergence-free history, noted in the entry above). Registered intests/test_response_shape_from_profile.pyas three dedicated cases (one generic, covering thirteen of the fifteen parsers; one forget_scope_session_state's two-key round trip; one forset_scope_fixed_edge's self-reparse) rather than aMATCHER_BACKED_GETTERSrow, since none of the fifteen route through_get_bcd_level/_get_bool_value— the same reason batch 1's date/time/UTC trio andget_dual_watchare each a dedicated case there instead of a table row.rigplane.commands.system/cw/speech/powerbuilders requirecmd_map; there is no hardcoded fallback (MOR-2008, batch 1 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md). All 26 builders across the four modules — transceiver ID, band-edge frequency, tuner/XFC/TX-freq-monitor status, RIT/XIT (system.py); CW keying (cw.py); voice announcement (speech.py); power on/off/status (power.py) — now requirecmd_mapas a required keyword-only argument. 20 of the 26 carry no behaviour change at all: every declaring CI-V profile already held the identical wire tuple the deleted fallback built. The other six —get_system_date/set_system_date,get_system_time/set_system_time,get_utc_offset/set_utc_offset— carry an intended byte-level behaviour change on IC-7300. Their pre-migrationcmd_mapbranches resolved the bare keys"system_date"/"system_time"/"utc_offset", which no profile TOML has ever declared (every one spells the direction-prefixedget_/set_system_dateetc.), so those branches raisedKeyErrorfor every profile a realCommandMapever reached — dead code, invisible to the divergence sweep, since it never produced a frame to compare. The builders now resolve the direction-prefixed keys the TOML files actually declare. IC-7610's own tuple (0x01 0x58/0x59/0x62) is byte-identical to the deleted fallback's, so nothing changes there; IC-7300 declares its own family numbers (0x00 0x94/0x95/0x96) and now sends them for the first time, instead of IC-7610's.parse_system_date_response/parse_system_time_response/parse_utc_offset_responsemoved with the request: each now takes the map-derivedprefixthe reply must start with (runtime/radio.py: CoreRadio._expect_shape), rather than checking a hardcoded constant unconditionally. The only production callers,runtime/radio.py: CoreRadio's own methods, moved ontoself._commands.<builder>(...)in the same change. Also deleted: the orphaned barequick_split/quick_dual_watchrows inrigs/ic7300.tomlandrigs/ic9700.toml— no builder has resolved either since MOR-2007 ruling 2 replaced their one-shot triggers with realget_/set_pairs; the prefixed rows already carry the recorded D2 sources.- CLI:
rigplane ptt onno longer returns while the rig is keyed. The command now holds the key for as long as the process runs and unkeys on the way out, so the process that keyed the rig is the one that releases it. A script that ranrigplane ptt on, did some work, and then ranrigplane ptt offmust move torigplane ptt on --for SEC(or keep theptt onprocess alive for the length of the transmission) — the old shape now blocks at the first command. SIGINT / SIGTERM / SIGHUP unkey and exit 128 + the signal (130 / 143 / 129); a duration that simply runs out exits 0; a failed or stuck unkey exits 1 with a warning that the radio may still be transmitting.ptt offremains the recovery path for a rig left transmitting by some other process; under a managed runtime it now escalates to an operator-forced unkey (see Changed below) and its exit code is meaningful (MOR-1184, MOR-1199). rigplane.commands.set_vfotakes a selector byte, not a VFO name (MOR-1986). The builder held its own{"A": 0x00, "B": 0x01, "MAIN": 0xD0, "SUB": 0xD1}table — rig-specific bytes inside a builder, and a second implementation of a mapping the rig TOMLs declare as[vfo] main_select/sub_select. It now takes the byte and the profile supplies it:set_vfo("MAIN", to_addr=...)becomesset_vfo(radio.profile.vfo_main_code, to_addr=...)— but those fields areint | None, and four shipped profiles (X6100, X6200, TX-500, FTX-1) declare neither, so a caller must handleNonerather than pass it through. Callers that passed a name outside those four keys used to select VFO A silently; there is no such fallback now.IcomRadio's signature and accepted alphabet are unchanged —_set_vfo_wirestill takes "A"/"B"/"MAIN"/"SUB" — but its bytes are not: it now resolves them from the loaded profile, so "A" on amain_subprofile sends 0xD0 where it sent 0x00, "MAIN" on anabprofile sends 0x00 where it sent 0xD0, and a profile declaring no code raisesCommandErrorwhere it used to emit. Every guarded caller (select_receiver,swap_vfo_ab,equalize_vfo_ab, the per-receiver VFO fallback) passes "MAIN"/"SUB" on a dual-RX profile. On the dual-RX profiles that declaremain_select/sub_select(IC-7610, IC-9700) the bytes are identical, so asnapshot_state→restore_stateround-trip is unchanged.restore_stateitself forwards whatever dict it is handed, though:radio.restore_state({"vfo": "A"})against amain_subprofile now selects the MAIN receiver (0xD0) where it used to select VFO A (0x00), andrestore_stateswallows per-field failures at DEBUG, so nothing surfaces. Hand-built restore dicts should use "MAIN"/"SUB".-
rigplane.commands.get_scope_center_typetakes noreceiverargument (MOR-1981, MOR-2002). The parameter let a caller name the MAIN or SUB scope for this reading, like the library's other eight scope-selector getters, but 0x1C is not one of them: on the wire,27 1C 00is a SET of center_type=0 (Filter center), not a selector prefix on a read — confirmed by a live IC-7300 bench recheck and by all four official Icom CI-V references (IC-705, IC-7300, IC-7610, IC-9700). Thecmd_mapbranch never accepted the argument, so passingreceiver=produced a write the caller believed was a read. No shipped caller passed it —runtime/_scope_runtime.pyand theRadioprotocol method never took a receiver for this reading — so this closes a footgun rather than changing observed behaviour; a caller that did passreceiver=now gets aTypeErrorinstead of a silent SET. -
rigplane.commands.mode/tone/antenna/metersbuilders requirecmd_map; there is no hardcoded fallback (MOR-2008, batch 2 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md). All 40 builders across the four modules — operating mode, DATA mode, filter shape/width, SSB TX bandwidth, Main/Sub tracking, AGC time constant (mode.py); repeater tone/TSQL and tone/TSQL frequency (tone.py); ANT1/ ANT2 select/read and RX-ANT-state select/read for both antennas (antenna.py); S/SWR/ALC/power/comp/Vd/Id meters, S-meter-squelch/ overflow/various-squelch status (meters.py) — plusrigplane.commands.freq'sget_freq/set_freq(2 more, 42 in total this batch) now requirecmd_mapas a required keyword-only argument.freq.py's other five builders (get_selected_freq/get_unselected_freq/get_selected_mode/get_unselected_mode/set_selected_mode) have nocmd_mapparameter at all and stay out of scope — MOR-2008's Group B (these five plusmemory.pyandtx_band.py), ruled by the owner to migrate later, after the missing/inconsistent TOML declarations are filled in from the manuals. Most of these 42 carry no behaviour change: every declaring CI-V profile already held the identical wire tuple the deleted fallback built.set_repeater_tone/set_repeater_tsql/set_tone_freq/set_tsql_freq(and theirget_twins) are the exception on IC-7610:rigs/ic7610.tomldocuments that this whole family is intentionally not declared, following a live-bench readback (recorded in the TOML, citing MOR-660/661/682) that found the old hardcoded fallback's bytes decoded to garbage (16.5 Hz) on this radio. Before this migration the fallback still built and sent those bytes regardless; now, on IC-7610, the same call raisesCommandError(undeclared-command refusal) instead — a correction, not a regression: trusting what the profile actually declares over a blind fallback, the same principle behind batch 1'ssystem.pyIC-7300 date/time fix (there the profile's own bytes replace the fallback's; here the profile's declared absence does).antenna.py'sget_antenna/set_antennaANT1/ANT2 selector is documented in the module's own docstring as deliberately not wired throughruntime/radio.py: CoreRadio._expect_shape: the selector is a CI-V protocol invariant supplied as caller data, not a value the command map declares as asubbyte (the TOML'sget_antenna = [0x12]tuple carries nosubat all). The production call sites, inruntime/radio.py: CoreRadioandruntime/_dual_rx_runtime.py: DualRxRuntimeMixin(the real callers ofget_freq/set_freq/get_mode/set_mode), moved ontoself._commands.<builder>(...)in the same change. rigplane.commands.dspbuilders requirecmd_map; there is no hardcoded fallback (MOR-2008, batch 3 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md). All 37 builders — attenuator, preamp, DIGI-SEL, NB, NR, IP+, AF mute, AGC, audio peak filter, auto-notch, compressor, monitor, VOX, break-in, manual notch, manual notch width, twin peak filter, dial lock — now requirecmd_mapas a required keyword-only argument, the same contract as batches 1/2.set_attenuatoris a compatibility wrapper with no command-map key of its own (it delegates toset_attenuator_level, whose key is"set_attenuator") and carries@require_cmd_mapwithout@expose_command_key, the same shapecommands/vfo.py: set_dual_watchalready uses. Zero divergence: every declaring CI-V profile already held the identical wire tuple the deleted fallback built. Twelve getters (get_agc,get_audio_peak_filter,get_break_in,get_manual_notch_width,get_auto_notch,get_compressor,get_monitor,get_vox,get_manual_notch,get_twin_peak_filter,get_dial_lock,get_af_mute) also moved their reply matcher ontoruntime/radio.py: CoreRadio._expect_shape, registered intests/test_response_shape_from_profile.py'sMATCHER_BACKED_GETTERSkeystone table;get_attenuator/get_preamp/get_digisel/get_nb/get_nr/get_ip_plusare excluded because each reaches its reply through a transaction-correlated wait with no command/sub matching at all, never through the two matcher functions. X6100/X6200 gain three controls, lose three others, both corrections discovered by this migration, not created by it:get_/set_nr,get_/set_compressorandget_/set_auto_notchare newly declared present at[0x16, 0x40]/[0x16, 0x44]/[0x16, 0x41]— the project's own cat-audit (docs/validation/cat-audits/x6200.md) already confirmed these opcodes correct and in use by the deleted fallback; left undeclared, the migration would have turned three already-working controls into a refusal, a regression rather than a correction.get_/set_vox,get_/set_break_inandget_/set_manual_notchare newly declared absent, promoted from a comment-only note already on both profiles: the V1.0.6 documentation has no VOX row at all, no BK-IN-mode register (only QSK hang time), and no manual-notch register (the real notch mechanism is the DNF toggle this same migration declares present asget_/set_auto_notch) — calling these three pairs now raisesCommandErrorinstead of silently sending an opcode the manufacturer's own table never documented.get_/set_manual_notch_widthstays an undeclared gap on both profiles (neither present nor absent): the opcode's own table-presence, unlike the three above, was never confirmed either way, and the X6200 unit that could have settled it with a live capture was destroyed 2026-08-11. The production call sites, inruntime/radio.py: CoreRadio(dsp.py has none inruntime/_dual_rx_runtime.py), moved ontoself._commands.<builder>(...)in the same change.tests/test_command_fallback_audit.py's representative builder (get_attenuator) has no fallback left either, and a repo-wide census found no builder anywhere incommands/*.pywith an optionalcmd_mapremaining to repoint at — its four tests that used to assert "no exception, fallback bytes returned" now assert "the wrapper's warning still fires, then the builder's ownTypeErrorpropagates unchanged."rigplane.commands.memory/tx_bandbuilders, andfreq.py's five remaining selected-receiver builders, requirecmd_map; none ever had a hardcoded fallback to delete (MOR-2008, batch 4 ofdocs/plans/2026-08-29-profile-driven-command-bytes.md, "Group B" — the last 16 builders incommands/with nocmd_mapparameter at all). Coversmemory.py's nine (build_memory_mode_get/_set,build_memory_write,build_memory_to_vfo,build_memory_clear,build_memory_contents_get/_set,get_bsr/set_bsr),tx_band.py's two (get_tx_band_count,get_tx_band_edge), andfreq.py's five (get_selected_freq/get_unselected_freq/get_selected_mode/get_unselected_mode/set_selected_mode). Python function names are unchanged — the sevenmemory.pybuilders with the anomalousbuild_X_get/build_X_set/build_Xshape keep it; only the TOML key spellings were reconciled. Canonical key naming (owner ruling): a genuine get/set value pair keeps itsget_/set_prefix; a single-wire-form action command uses the bare name, matchingpower_on/ptt_on/scan_start— sobuild_memory_write/build_memory_to_vfo/build_memory_clearresolve"memory_write"/"memory_to_vfo"/"memory_clear", not aget_/set_-prefixed key. IC-705 previously declared redundantget_X/set_Xpairs for these three one-shot actions; the orphan half of each pair is deleted, with a fresh source citation on the single surviving bare-named row (neither half carried one before). The four Icom profiles' 57 DECLARE cells are byte-identical to what these builders sent before migration (verified directly against the deleted hardcoded output, not merely asserted). Two commands are left deliberately undeclared (D1 state 3) rather than guessed:get_memory_modeon all four Icom profiles (the manuals phrase 0x08 as "Select the Memory mode", imperative, not "Send/read" like the package's other bidirectional rows, and no live-bench probe has settled whether it answers a bare read), andset_selected_modeon IC-7300/IC-7610/IC-9700 (the exact (mode, data_mode, filter) SET payload convention was not independently confirmed against those three manuals' own detail pages before this batch — IC-705/X6200/X6100 already declare it and are unaffected). Calling either now raisesCommandError(undeclared-command refusal) on the affected profiles instead of unconditionally sending the byte the old hardcoded fallback always sent — the honest outcome until a live probe or a manual detail-page read settles the open question. X6200/X6100 declare all 11 memory.py/tx_band.py keys formally absent: none of the nine memory-family opcodes nor the two TX-band opcodes appear anywhere in the Radioddity X6200 CI-V V1.0.6 documented opcode table (Table 1, pp.5-9);get_bsr/set_bsrspecifically because the one opcode the table does document at[0x1A, 0x01](already exposed asget_band_spectrum_display) is a different, 2-byte-payload command, not Icom's own 9-byte band-stacking-register record — same opcode, structurally incompatible wire contract. This migration also corrects a pre-existing false claim inrigs/x6200.toml's own "PROVENANCE OPEN" note, which said the band-stacking WRITE side of[0x1A, 0x01]was undeclared/undocumented; the same manual page the file already cited documents that SET row too (just not as Icom'sset_bsr).ftx1.toml/tx500.toml(CAT-only, no CI-V command table) declare all 16 canonical keys formally absent, citing the protocol mismatch rather than leaving them as a silent gap — this removes both profiles fromtests/test_command_map_parity.py'scat_only_profilescensus/cat-onlyrows, since that classification means literally "every[commands]entry isCatCommandSpec", which a declared-absent row no longer satisfies even though both profiles remain cat-only in every functional sense. The orphan TOML keysset_selected_freq/set_unselected_freq/set_unselected_mode(no Python builder resolves any of the three) are kept, not deleted, on X6200/X6100 — each documents a real, sourced capability the same 0x25/0x26 opcodes above confirm — with a one-line comment added noting the absent builder; IC-705's own copies of the same three keys, which carry no source at all, are left untouched. Hard boundary (MOR-2055, explicitly not touched by this migration):build_memory_to_vfo/build_memory_clearstill append a 2-byte BCD channel payload that all four Icom manuals show 0x0A/0x0B accepting no data field for at all — this migration re-plumbs the command-map lookup only and does not change those wire bytes; the payload question is MOR-2055's to settle. Three now-dead constants (_CMD_MEMORY_WRITE,_CMD_MEMORY_TO_VFO,_CMD_MEMORY_CLEAR) are deleted fromcommands/_frame.py— an AST-verified census found zero importers anywhere for any of the three oncememory.py's own builders stopped reading them directly. The production call sites, inruntime/radio.py: CoreRadioandruntime/_dual_rx_runtime.py: DualRxRuntimeMixin(the selected/ unselected freq/mode readers and the X6200 selected-mode SET path), moved ontoself._commands.<builder>(...)in the same change.tests/test_command_fallback_audit.py's census is updated: 256 public builders now requirecmd_map(was 238), and the 33 that lack the parameter entirely are, for the first time, exclusivelyparse_*response decoders — no byte-emitting builder anywhere incommands/lackscmd_mapany longer.- The command-builder fallback machinery is deleted (Step Z, cmd-map
epic). With every builder's
cmd_maprequired and keyword-only, the Step 1 measurement scaffolding that logged a call reaching the hardcoded fallback path has nothing left to catch:commands/_fallback_audit.py, its install call incommands/__init__.py, theRIGPLANE_COMMAND_FALLBACK_AUDITenv var andcore/env_config.py: get_command_fallback_audit_enabled, and the charter exception recording it incommands/LAYER.mdare all deleted, along withtests/test_command_fallback_audit.py(its tests pinned "wrapper logs thenTypeErrorpropagates", a behaviour that dies with the wrapper). A regression test (tests/test_commands.py: TestFallbackAuditRemoved) pins all three staying gone. Re-running an AST-based reference census overcommands/_frame.py's 78 private module-level constants — gated on an actual import binding from this module, so a same-named local literal in a test file (several build synthetic CI-V frames independently of_frame.py, e.g.tests/mock_server.py's own_CMD_ATT) does not count as a reader — found three with zero readers anywhere, internal or external:_SUB_RX_ANT_ANT1/_SUB_RX_ANT_ANT2(flagged dead but left out of scope at cmd-map batch 2) and_SUB_AGC_TIME_CONSTANT(superseded bymode.py'sget_agc_time_constant/set_agc_time_constantreading the byte from the profile'sCommandMapby name). All three are deleted; the other 75 survive, and the module docstring now names each survivor's actual reader.docs/api/commands.mdis rewritten: it claimed every builder'scmd_mapwas optional, false since batch 1 — it now documents the required keyword-only contract, theTypeErrorraised for a call that omits it or passesNone,commands/bound.py: BoundCommandsas the recommended calling path, and D1's three-state undeclared-command policy (declared / declared-absent-with-source / unknown-refusal).tests/command_map_parity_divergences.txtis kept, not retired, despite reading all-zero: reintroducing a builder with acmd_map is not Nonebranch was tested against this file's own machinery (a temporary, reverted mutation) and it still goes red, with the per-row hex frames the bare census intests/command_map_parity_uncovered.txtdoes not carry; the file is also cited as evidence in ten docstring passages across five other test files, which a deletion would have left dangling. rigplane.commands.set_attenuator(the boolean CI-V frame builder) is deleted (MOR-2086). It hardcoded18 if on else 0-- an IC-7610 dB step invalid on the five other CI-V profiles (IC-705, IC-7300, IC-9700, X6100, X6200 declare a single, different non-zero attenuator value; bench-confirmed on the IC-7300, where the radio silently ignored the frame). The released 2.11.1 carries this bug (git show v2.11.1:src/rigplane/commands/dsp.pystill has the identical constant).runtime/radio.py: CoreRadio.set_attenuatorno longer builds a frame through the deleted function; it resolvesonfrom the connected profile's own declared[attenuator] valuesinstead:on=Falsealways resolves to 0,on=Trueresolves to the profile's single non-zero value. Each of the five now sends its own declared value on the wire (fe fe 94 e0 11 20 fdon the IC-7300); the pre-fix11 18frame was bench-confirmed ignored on the IC-7300. A profile that declares more than one non-zero value (IC-7610's 3..45 dB steps) has no well-defined boolean "on", soCoreRadio.set_attenuator(on=True)now raisesCommandErrornamingset_attenuator_levelas the call to make instead of guessing a step; the same refusal applies to a profile that declares no[attenuator] valuesat all.set_attenuator_levelitself is unchanged.rigplane.runtime._state_queries.build_state_queries(profile, capabilities, *, is_serial=False)drops thecapabilitiesandis_serialparameters (MOR-2244). After MOR-1983 (#3020, merge3683b297) rewrote query membership to come from the profile's own field-level acquisition capabilities, both parameters went unread in the function body. The two production callers,runtime/radio_initial_state.py: fetch_initial_stateandweb/radio_poller.py: RadioPoller._build_state_queries, now callbuild_state_queries(profile); a call still passing either as a keyword now raisesTypeError. The compatibility re-export atrigplane._state_queries(asys.modulesalias of the canonical module) carries the same signature change.
Changed¶
- TX consumers migrate to canonical ownership and release. Earlier development entries described per-consumer leases, poller watchdogs and UI-local teardown behavior that are superseded by the managed TX contract. Use stable-owner WebSocket momentary PTT, explicit latched TRANSMIT only when intended, and canonical ForceOff for unconditional release. Admission is distinct from settlement and observed radio state. The concrete API and timed CLI migration examples, source references and focused witnesses are in the migration guide.
Removed¶
- The unused pre-v2
src/rigplane/web/static.old/index.htmlsource copy was removed in #2400 (32eb20bd). The supported UI is built fromfrontend/and bundled with the Python artifact. Rollback uses a known complete package/frontend artifact and retained user preferences, not the deleted snapshot; source removal alone does not prove installed or active-session rollback acceptance. rigplane.backends.icom7610.drivers.serial_stubno longer ships in the package (#2129, #2131).SerialMockRadioand the serial framing test doubles were test-only code living inside the wheel; they moved totests/serial_stub.py(non-shipping) so TX-safety audits of the shipping package (grep -rn "\.set_ptt(" src/, MOR-999/MOR-1015/MOR-1016) contain no test-double false positives. Nothing in the shipping package imported the module and it was never a documented public API; external code that imported the old path should vendor the double from the repo's test tree instead.
Fixed¶
- WS
set_quick_split/set_quick_dual_watchnow write instead of re-reading (MOR-2007 ruling 2 follow-up, MOR-2045). Both intents parseonfromparamsand enqueue newSetQuickSplit/SetQuickDualWatchpoller commands, which write throughCoreRadio.set_quick_split/set_quick_dual_watch. Previously they enqueued the same bare read marker as theirget_twins and silently re-read the current toggle state instead of changing it. get_tone_freq/set_tone_freq/get_tsql_freq/set_tsql_freqused the wrong CI-V byte layout, on every profile that declares the family (IC-705, IC-7300, IC-9700; IC-7610 declares itabsent) (MOR-2091).commands/tone.py's codec packed/read the 3-byte BCD payload as[hundreds][tens+units][tenths]; the documented layout (identical in the IC-705, IC-7300, IC-9700 and IC-7610 CI-V references) is six packed BCD digits read as tenths of a Hz:[0][0][100Hz][10Hz][1Hz][0.1Hz].get_tone_freq/get_tsql_freqreturned wrong values -- a live IC-7300 capture of an 88.5 Hz tone decoded as 16.5 Hz.set_tone_freq/set_tsql_freqencoded a 100 Hz BCD digit outside its documented 0-2 range for most tones (88.5 Hz encoded as00 88 05, digit 8). The released 2.11.1 carries this bug.- FTX-1
set_sql_typewrite template used two digits where the manual documents one (MOR-2104).rigs/ftx1.toml's write template renderedCT002;for squelch-type value 2; the FTX-1 CAT OM ENG 2508-C'sCT SQL TYPEtable documents P2 as a single digit (0-5). Narrowed the template toCT0{type};, matching the read side's parse, which already used the correct width.
[2.11.1] — 2026-06-22¶
Fixed¶
- AudioSession declarative reconciler — TX-only strand after reconnect (#1226, #1212).
AudioSessionis now a declarative reconciler: a pure_desired()computes the target state and a single idempotent_converge()drives toward it. Push events converge instead of rejecting, and the reconnect/reestablish path now handlesTX_ONLY, fixing audio left stranded TX-only after a reconnect. TX arming is intent-gated, preserving the MOR-556 ordering invariant; public API unchanged. - Control WS broadcast flood (#1876). The control WS fanout now uses
put_drop_oldestinstead ofput_nowait+warning, ending the broadcast flood under backpressure. - Diagnostics logger default (#1879). The rigplane logger now defaults to INFO instead of DEBUG.
- Idle state-build gate (#1877).
_broadcast_state_updateis demand-gated on subscriber count, skipping idle state-builds when no client is subscribed.
[2.11.0] — 2026-06-22¶
Added¶
- Unified radio session lifecycle. New
RadioSessionLifecycle(public SDK) owns connect / disconnect / scan / recovery as one state machine with a guaranteed graceful session close (token-remove + OpenClose) on every exit path, plus a rich observable event/status surface (state transitions, cooldown countdown, error reasons, recovery events). New blessed public symbols:RadioSessionLifecycle,LifecycleState,LifecycleErrorReason,RadioPresence,LifecycleEvent,LifecycleStatus.
Fixed¶
- IC-7610 LAN connect livelock. A failed network-CI-V connect now always releases the held session (token-remove) before any cooldown wait, and retry is cooldown-aware and resident in-process — eliminating the previous cross-process restart storm that reopened UDP 50001 inside the radio's keepalive window and got rejected with error 0xFFFFFFFF ("previous session active"). Graceful connect/disconnect now leaves the radio immediately free (no cooldown).
Changed¶
- Recovery unified + soft_reconnect semantics. CI-V data-watchdog now only DETECTS stalls; the lifecycle owns retry/backoff/exhaustion (preserving the #1217 anti-freeze: detached recovery, self-cancel guard, unconditional re-arm).
CoreRadio.soft_reconnect()is now multi-attempt and releases the session on exhaustion (previously a single non-releasing attempt) — direct callers already handle the connection-error raise.
[2.10.8] — 2026-06-20¶
Fixed¶
- Operating-UI frequency/mode display stuck during an external CAT session. During a WSJT-X / external-CAT session the display could remain pinned to the last value tuned via the Pro UI while the radio was actually on a different frequency, because the optimistic command overlay was never displaced (the readback poller is paused for the session). The optimistic overlay is now suppressed during an external-CAT session, so the display tracks the radio's real frequency/mode from CI-V readback. Normal Pro-dial responsiveness is unchanged when no external-CAT session is active. (#1219)
[2.10.7] — 2026-06-20¶
Fixed¶
- Radio control link now auto-recovers after the radio drops. After a radio power-cycle or network blip, the CI-V control reconnect stalled after a single attempt (
already_open_stalled) and the watchdog never re-armed, so the "Radio link recovering" state hung until a manual retry or app restart. Reconnect now rebuilds and keeps retrying with bounded backoff, recovering automatically once the radio answers again. (#1217)
Added¶
- Distinguish "radio reachable but remote-control server not responding" from "radio offline". When the radio's IP is up but its CI-V/remote-control server isn't answering (e.g. Network/Remote control left off after a power-cycle), the app now reports a distinct cause so it can show an actionable hint instead of a generic recovering message. (#1217)
[2.10.6] — 2026-06-19¶
Fixed¶
- Web RX audio went silent after a reconnect until app restart. On reconnect the browser reopened the audio WebSocket, but the prior half-open socket was not reaped (it still reported alive until the pong timeout), so the audio broadcaster kept a zombie subscriber and the live tab stopped rendering audio. A stable per-client audio identity now coalesces a reconnecting client's prior subscription (dropping the superseded socket), so web audio recovers automatically without an app restart. (#1216)
[2.10.5] — 2026-06-19¶
Fixed¶
- RX audio did not recover after the radio's LAN session dropped.
soft_reconnectrebuilt only the CI-V control transport; the audio transport was never torn down, leaking the audio-UDP socket file descriptor so RX could not re-arm (File descriptor … is used by transport). The audio transport is now torn down and re-armed onsoft_reconnect, and_ensure_audio_transportis idempotent against a stale FD, so RX audio auto-recovers without an app restart. (#1215)
[2.10.4] — 2026-06-19¶
Fixed¶
- Digital/FT8 transmit produced no modulation over LAN. A TX-only audio
client (e.g. WSJT-X over the companion) holds a TX lease without subscribing
to RX;
AudioSessiongated TX arming on RX demand, so the LAN audio stream was never armed and every TX frame was rejected (AudioNotStartedError), leaving the radio keyed with an unmodulated carrier.AudioSessionnow lazily arms TX on first push for full-duplex transports (newTX_ONLYstate), preserving RX-before-TX and teardown ordering. Regression introduced by the 2.9.0 AudioSession demand-driven migration.
[2.10.3] — 2026-06-18¶
Fixed¶
- rigctld
get_modeno longer clobbers canonical StateStore. The WSJT-X CATget_modepoll was a read that wrote backbool(data_mode)and the filter number into thefilter_widthslot of the canonical StateStore, corrupting/api/v1/statemid-session (DATA mode blanked on the web UI, filter forced to "1", scope/passband narrowed) whenever WSJT-X connected.get_mode/set_modeare now read-only on the data_mode/filter projection;data_modestays an int (PR #1884). - rigctld
get_vfofirst-connect and mid-session EIO resolved.get_vforeturnedRPRT -6(EIO / "IO error") when the VFO projection was stale or not yet fresh, dropping active WSJT-X FT8 sessions mid-run; it now returns the active VFO name (VFOA). Theget_modepassband now reads the filter-number projection instead offilter_width(Hz), andset_modeoverlays the filter-number path (PR #1884).
Added¶
- Canonical state-payload schema with TypeScript codegen as a reference contract pattern (MOR-881, dev tooling; PR #1883).
2.10.2 — 2026-06-18¶
Fixed¶
- IC-7610 state-acquisition regression with an external CAT master: when a
separate CAT client (e.g. WSJT-X via rigctld) was polling the radio, false
acquisition_request_timeouts drove the adaptive cadence of frequency, mode and data-mode polling to decay all the way to 30 s, so the web VFO, mode and DATA readouts went stale and appeared to "revert". The fix makes acquisition deadlines send-relative, gates adaptive decay on transport health, and adds a bounded grace before a healthy-link expiry is treated as a real timeout (rigplane-pro#1198, MOR-874).
2.10.1 — 2026-06-17¶
Fixed¶
- IC-7610 S-meter no longer stuck at S0: repaired the MOR-334 state-pipeline reconciliation (1bd57e10), applied the first meter sample immediately instead of holding it in the coalescer (53374bcc), and raised streaming-meter freshness TTLs above the live poll cadence so the value no longer decays to the calibrated floor between frames (8a64942f).
- Mode, data-mode, RF gain, AF level and squelch no longer revert ~1 s after being set: optimistic command-response overlay plus correct normalized level scaling (53374bcc); digital modes D1/D2/D3 now reflect correctly (1bd57e10).
- IC-7610 MOD-input source (MIC/ACC/USB/LAN) no longer reverts to a blank value after selection (ae0bd583, MOR-616).
- Operating-UI control units: RF gain (5ef3d9ef) and the mobile TX-power readout (4e62a5af) now consume the backend's normalized 0–1 scale instead of a raw 0–255 range, so they show the real percentage / watts.
- Waterfall tuning now holds the commanded frequency via the optimistic overlay instead of snapping back after a click or drag (4e62a5af).
- LAN radio discovery retransmits the UDP probe within the scan window with dedup, so a network radio is found on the first scan instead of after several attempts (2ea0bf2e).
2.10.0 — 2026-06-11¶
Added¶
rigplane validate— multi-level radio regression-validation harness (epic MOR-634). A new CLI subcommand that exercises a radio against a universal capability matrix and reports per-check results. Highlights: read-modify-verify-restore (RMVR) round-trip checks for frequency, mode, levels, scope controls and MOD-input routing; a committed golden gate (--gate/--regen-golden) that pins each radio's dry-run plan (MOR-636/648, #1801, #1809, #1814); a hamlib-external provider (--provider hamlib) to validate arbitrary rigctld-backed rigs (MOR-638, #1805); command-coverage families for scope, tone, VFO, memory and system commands (MOR-642–646, #1811, #1813); automated audio-pipeline probe checks plus live-RXaudio.rx.rms/scope.fft.presenceagainst the hardware stream (MOR-639–641/668, #1810, #1822);--interactiveoperator-confirmed manual-perception checks (MOR-667, #1820); and--tx-actuateto key PTT and run a tuner tune-cycle behind a full gate with explicit confirmation (MOR-666, #1821). The registry is split into per-domain modules and its layer boundaries are import-linter-pinned (MOR-649, #1807, #1808).- IC-7610 MOD-input routing validation guard — RMVR over
0x1A 05 0089–0094(MOR-678, #1828). if_shift.setandcontour.setRMVR validation checks (MOR-671, #1825).- Backend session-teardown now restores the MOD-input source on an abnormal disconnect (MOR-624, #1796).
Changed¶
- No silent IC-7610 impersonation (behaviour change — MOR-174, #1798).
An unknown/unresolved radio model no longer silently inherits IC-7610
capabilities, scope and CI-V address: the serial backend now raises on an
unknown model, and the web profile path logs a WARNING and falls through
the normal default-resolution chain. The CLI always injects the real model,
so CLI runtime behaviour is unchanged — only library/embedding consumers
that relied on the old silent
"IC-7610"default are affected. - Capability-accuracy / CAT corrections surfaced by the validation harness:
IC-7300 band-stack-register read implemented and dead
apf 14 05dropped (MOR-681, #1827); TX-500 profile mode-map + power CAT control corrected and divergent profiles consolidated (MOR-684, #1826); scopevbw/rbw/dualreceiver-prefix corrected on SET, live-proven on the IC-7610 (MOR-664, #1829); FTX-1 tone checks repointed tosql_type/ctcss_toneand DT clock command strings added (MOR-672, #1830); Icom level checks upgraded to RMVR round-trips (rf_power, mic_gain, comp, nr/nb, cw_pitch) and made control-range-aware (MOR-679/695, #1831, #1832). - A radio operation raising
NotImplementedErrornow resolves to UNSUPPORTED in validation instead of FAIL (MOR-670, #1823).
Removed¶
These are corrections of over-declared capabilities — capabilities the profiles advertised but the radios do not actually expose over CI-V, confirmed against live hardware. A radio that previously advertised them no longer does; this is an accuracy fix, not a feature regression.
- Xiegu X6200: dropped the tone family (
repeater_tone/tsql,0x16 42/43+0x1B) — live-confirmed to time out (MOR-683, #1833) — and twin-PBT (pbt,0x14 07/08) — live-confirmed to time out in AM and USB (MOR-699, #1835).filter_widthis intentionally not advertised: the radio uses a raw-hex (non-BCD) filter codec that needs its own implementation, tracked in MOR-706. - Xiegu X6100: dropped
repeater_tone/tsql/pbtby X6100/X6200 shared-firmware inference (the two share a CAT caps struct), pending direct X6100-hardware confirmation (MOR-634, #1836). - IC-7610: removed dead
[commands]tone/TSQL entries (0x16 42/43,0x1B 00/01) that had no declared capability (MOR-682, #1839).
Fixed¶
- FTX-1 web poller no longer floods the log every cycle with
sub.s_meterparse warnings (the radio answers the subSM1;query in main formSM0000;): the warning is now emitted once per field then demoted to DEBUG, and thebreak_in_delayparse accepts the FTX-1's 2-digit reply (MOR-561, #1838). - IC-7610 scope fixed-edge read: send the range + edge selector so the radio
answers
get_scope_fixed_edge(MOR-662, #1816). preamp.setvalidation clears and restores the DIGI-SEL prerequisite before testing PREAMP (MOR-665, #1819); per-check exceptions and non-primitive evidence are contained so one bad RMVR value can't abort the run or crash artifact emission (MOR-659/663, #1815, #1818).- Linux USB-audio device pairing: normalize ALSA device names so topology matching succeeds (MOR-549, #1806).
- Refresh radio credentials on reconnect (MOR-252, #1799).
- Frontend
connection.test.tsorder-dependent flake fixed with a per-test store reset (MOR-611, #1837). - Python 3.11 test-suite fixes — CI-V timeout / serial RX, web-server, audio Protocol/async-mock, and rigctld async-dispatch failures — plus pinning the test interpreter to the matrix version (MOR-590, #1800, #1802, #1803, #1804).
- Cleared the mypy baseline (all 21 remaining type errors fixed, #1797).
Docs¶
- CAT-manual-vs-implementation gap audits for five radios under
docs/validation/cat-audits/(#1824), plus an X6200 unofficial/community CI-V reference distilled from the Hamlibxiegu.cbackend (#1834). - Mark the IC-9700 as Community-validated (field report #1719, #1795); use absolute README image/badge URLs so they render on PyPI (#1794).
2.9.0 — 2026-06-11¶
Breaking changes¶
- CLI: option abbreviations are no longer accepted. The
rigplaneargument parser now setsallow_abbrev=False, so abbreviated long options (e.g.--ser-portfor--serial-port) are rejected — scripts must spell options out in full. This also fixesrigplane discover --serialexiting with an argparse ambiguity error on Python 3.11, where prefix matching collided with the global--serial-port/--serial-baud/--serial-ptt-modeoptions (MOR-589, #1776, 878ff355).
Added¶
- Adaptive audio egress codec (opt-in): new
WebConfig.audio_adaptive_egressflag, default off. When enabled, the server switches each browser client between PCM16 and Opus per connection based on observed link quality (client-reported playback underruns + server-side send-queue drops), with dwell windows to prevent flapping; bridge/digital paths stay lossless. With the flag off (the default) egress behavior is unchanged and a client's codec never changes mid-stream (MOR-588, #1775, 1cc5e72a; link-qualityaudio_statsuplink MOR-585, #1773, 2dc28f84). - IC-7610 MOD-input routing in the Web UI (epic MOR-614, #1786–#1791): the
current MOD Input source for each DATA group (DATA OFF/D1/D2/D3 MOD, CI-V
0x1A 05 00 0x91–0x94) is now read into state and exposed in the webstate_updatesnapshot (MOR-615, #1788, 5bbf22d2), and the ModePanel shows a "MOD IN" selector for the active DATA group with the six IC-7610 sources (MIC/ACC/MIC+ACC/USB/MIC+USB/LAN) (MOR-616, #1789, dcd7f8e5). Radios without MOD-input routing never render the control. - Network-voice-TX preflight guard: keying web voice TX while the active mode group's MOD input is not LAN now shows a warn-only banner with a one-click "Set LAN" action — TX is never blocked or delayed (MOR-617, #1790, 87cd5f58). An opt-in (off by default) auto-set-LAN-on-TX setting switches the active group to LAN when TX starts and restores the previous source when TX stops (MOR-618, #1791, 1b9cfc14).
- Docs: new troubleshooting section for the IC-7610 MOD-input requirement — why network voice TX produces broadband noise, feedback squeal, or silence when MIC is in the MOD-input source list, with the per-mode-group fix (MOR-619, #1786, 25ead3ba).
Changed¶
- Per-client web RX audio codec: each browser client now gets its own egress
codec (client preference → profile policy → PCM16 default) instead of one
aggregate codec for all clients — a PCM16-only client no longer forces
PCM16 on an Opus-capable client. After
audio_startthe server sends an advisoryaudio_formatack{codec, sample_rate, channels, frame_ms}; legacy clients ignore it and keep header-driven decode (MOR-584, #1771, ddd8c232). - Audio RX-start failures now surface instead of being swallowed: a failed
radio RX start raises to the demanding consumer, and the web audio handler
sends an
errorenvelope to subscribed clients instead of presenting a "subscribed" stream that never delivers frames (MOR-582, #1769, 593e0815). - Audio TX lifecycle is now owned by a radio-owned
AudioSession: web TX and the audio bridge share one refcounted session per radio, so a strayaudio_stopfrom one consumer can no longer disarm another consumer's active TX (MOR-576/577/579/580, #1763/#1765/#1766/#1767). Related internal work in this cycle — session health watchdog + liveness events (MOR-581), unified reconnect recovery viaAudioSession.reestablish()(MOR-586), RX tap surface (MOR-565), additivePcmFramedecode-at-ingress carrier (MOR-591) — changes no default behavior. - Audio bridge degrades to RX-only (with a warning) when a neutral-surface radio negotiates a non-PCM TX codec, instead of pushing mis-typed raw PCM; no shipping backend produces this configuration (MOR-545, #1749, a52263d9).
- PTT over the web control channel is now truthful: keying is rejected with
a
radio_not_readyerror envelope when the radio session is not ready or a reconnect cycle is in flight, instead of falsely reporting success while the radio never keyed; unkeying always goes through. The status bar now degrades the radio indicator (with a tooltip) on a connected-but-not-ready session, built on the new reconnect-status surface (MOR-620, #1787, 58781f6d; reconnect status + attempt countdown MOR-594, #1784, 88a3b9b6).
Fixed¶
- FTX-1 / Yaesu CAT:
start_audio_tx_opuswas a silent no-op, so the web handler's Opus TX branch did nothing on Yaesu backends; it now arms real TX via the neutral transport surface (MOR-541, #1745, d9b8caad). - Web audio TX double-start: the benign poller-first re-arm on USB-audio
radios (
AudioDriverLifecycleError("TX stream already started.")) is now tolerated instead of closing the audio WebSocket (MOR-544, #1748, 438930e7). - Audio bridge stop ordering: stopping the bridge with TX armed leaked a running LAN RX stream (RX demand was dropped before radio TX was stopped); the bridge now stops TX first, then tears down RX (MOR-574, #1762, 3f08f659).
- FM/AM-family waterfall passband (FM, FM-N, WFM, DATA-FM, AM-N, and the digital FM modes) now renders symmetric about the carrier (±width/2) instead of one-sided to the right like USB; display geometry only, no audio or filter behavior change (#1719, #1792, 4a082e17).
- A failed
connect()now fully unwinds the commander and keep-alive tasks it started, eliminating orphaned-future warnings after a connection failure (MOR-595, #1785, 49c5ff93).
2.8.0 — 2026-06-02¶
Added¶
- Web transport: added a backend-neutral
Connectionprotocol and retargeted the control, scope, and audio handlers to depend on that transport seam rather than the concrete WebSocket wrapper. This is a no-behaviour-change foundation for alternate transports (MOR-274, #1701, 8f2b6c5b). - WebRTC transport foundation: added optional
WebRtcDataChannelConnectionsupport behind the[webrtc]extra, including an ordered/reliablecontrolDataChannel that drives the unchanged control handler with the same wire frame shape as the WebSocket path (MOR-312, #1707, 047dea74). - WebRTC multi-channel support: added
scopeandaudioDataChannel helpers on the same peer connection, configured unordered and lossy (maxRetransmits=0) while reusing the same connection adapter and unchanged handlers (MOR-306, #1710, f1dad721). - Discovery:
DiscoveryRespondercan now advertise a station fleet through an additiveradios[]array with per-radio ids, models, urls, status, and connection state, while preserving the legacy single-radio top-level fields for existing discovery clients (MOR-303, #1705, eb4e2a98).
Changed¶
- Audio bus lifecycle: subscriptions now have an awaited close path and bridge teardown waits for subscriber removal, reducing restart races and keeping rapid start/stop diagnostics deterministic (#1568, #1708, dae97577).
- Realtime TX queue policy: bridge TX overflow now evicts the oldest queued audio frame and keeps the newest live capture frame, preserving bounded latency while continuing to count overruns (#1565, #1708, dae97577).
- Public docs now clarify WSJT-X loopback device direction, advisory
/api/v1/audioframe_mssemantics, validation workflow status, and command catalog stability boundaries (#1567, #1708, dae97577).
Fixed¶
- External CAT reconnect: a leaked external-CAT ownership flag is now reset on radio reconnect, so rigctld and web pollers resume instead of serving stale frequency/state after a managed runtime restart (MOR-301, #1703, c3cb411d).
- Hamlib bridge startup: partial
open_transport()failures after CAT session acquisition now release ownership, preventing a failed listener/server setup from leaving pollers paused (#1703, c3cb411d). - Web audio TX cleanup: active PCM/Opus TX is now stopped or safely released
when an audio websocket disconnects, is cancelled, or exits with an exception;
explicit
audio_stopbehavior is preserved (#1566, #1708, dae97577).
2.7.3 — 2026-05-31¶
Fixed¶
- Windows FT8 TX delivered no audio (Po=0 W): the callback/
blocksize=0TX capture emitted 960-byte (10 ms) frames while the PCM TX validator requires 1920-byte (20 ms) frames; capture now re-chunks the continuous callback stream into fixed 20 ms frames (keeps the ~50 Hz comb fix; lossless) (MOR-256, #1699, 17492be6)
Changed¶
- Build (packaging): build the web UI during packaging so source/git installs bundle the SPA (#1698, 55a0f81f)
2.7.2 — 2026-05-30¶
Fixed¶
- Desktop RX audio: resume the
AudioContextfrom the LIVE user-gesture so streamed RX audio plays under WKWebView/autoplay-restricted contexts, and recover from suspended-context frame drops instead of silently dropping them (MOR-239, #1694, 921f4c88) - Audio scope: dispatch audio-FFT frames to
/api/v1/audio-scopefor radios without a hardware scope (e.g. Xiegu X6200), fixing the blank AUDIO SCOPE panel; hardware-scope radios (IC-7610) are unchanged (MOR-241, #1695, 4e98ab0d) - Serial USB-audio TX: arm the PCM TX path in the shared serial Icom base so bridge TX reaches the radio (IC-705/IC-7300/IC-9700/X6200) instead of raising "PCM TX not started" per frame; the audio bridge now degrades to RX-only on TX failure instead of spamming (MOR-242, #1696, 37fe3d0a)
2.7.1 — 2026-05-29¶
Fixed¶
- USB audio now clamps the requested channel count to the device's real
capability (
input_channelsfor RX,output_channelsfor TX) at stream open, mirroring the existing sample-rate negotiation. Any mono USB codec self-heals instead of failing the global stereo default with PortAudio-9998("Invalid number of channels") and starving the stream; the Xiegu X6200 mono RX path no longer depends on its profilecodec_preferenceentry (verified on hardware). The effective channel count and its source are recorded on the stream contract for diagnostics (#1692).
2.7.0 — 2026-05-29¶
Added¶
- USB-audio topology resolution on Linux (
/syssysfs) and Windows (USB PnP / SetupAPI), extending the macOS resolver so USB radios — notably the Xiegu X6200's WCH CH342 CDC-ACM bridge (/dev/ttyACM*,COMx) — map their host USB audio device by physical USB topology with a VID:PID identity fallback (#1687, #1686).
Changed¶
- USB audio↔serial device pairing is now by device identity (product name
- same-name rank) instead of positional enumeration order, correcting selection on mixed-vendor multi-radio hosts (#1685).
Fixed¶
- Xiegu X6200 browser "LIVE" RX audio now flows: the mono C-Media USB codec is
opened as 1-channel PCM via an
[audio] codec_preferenceprofile entry, instead of failing the global stereo default (PortAudio-9998) and starving the stream (#1688). - Xiegu X6200 CH342 serial session stabilized: the reconnect watchdog uses
capped exponential backoff and no longer floods the log on transient USB
renumbering, and CI-V RX framing tolerates short/partial frequency frames
instead of raising
BCD data must be exactly 5 bytes(#1689).
2.6.0 — 2026-05-29¶
Added¶
- Universal real-radio validation matrix: JSON schema, per-radio templates, and a headless dry-run runner (#1652, #1653).
rigplane validate --provider hamlib | both— validate the native rigplane path against a Hamlibrigctldand surface native-vs-Hamlib comparison dimensions, with tolerance-aware readback (#1654, #1655, #1673, #1672, #1671).- Profile-driven matrix generation: a capability→check-spec registry with
generic
_check_from_specdispatch, Generator A (from profile capabilities) and Generator B (registry → Hamlib-filtered template) (#1659, #1660, #1661, #1670, #1671, #1668). rigplane radio-validate— profile-driven wrapper over the validate run path (#1675).rigplane convert— bootstrap a draft rig TOML from Hamlibdump_capswith cross-check (#1674, #1679).- Override files for generated matrices: a pure, check_id-keyed, safety-invariant merge layer wired into the generate path with metadata audit (#1676, #1678).
- Set-and-observe engine path for write-only controls, with per-radio write-only classification (#1663, #1664).
- Disambiguate the Xiegu X6200 from the Icom IC-705 (shared CI-V address
0xA4) by the Xiegu-only model-ID opcode0x1D 0x19when the USB hwid is inconclusive (#1682).
Fixed¶
- Xiegu X6200 USB RX audio now resolves in the browser "LIVE" stream on macOS:
the topology resolver handles CDC-ACM
usbmodemports and the C-Media codec identity (#1677). - Xiegu X6200 (and other CDC-ACM / Windows COM serial radios) are no longer skipped during discovery — port acceptance anchors on USB identity (VID:PID / hwid) instead of the device-name substring (#1680).
- Set mode via CI-V
0x26selected-mode when the profile declares it (#1657). - Send plain ATT/PREAMP commands on single-RX radios (#1656).
- Classify the X6200 manual notch as a write-only control (#1665, #1667).
- Harden rigctld-client response alignment over the bridge (#1669).
- Derive TX/tuner authorization from the capability registry rather than the mutable template flag (#1683).
Docs¶
- Open-source universal validation matrix guide —
radio-validate,convert, overrides, evidence (#1681); "Running the radio validation matrix" run recipes (#1666). - ADR for the universal profile-driven validation matrix (#1658), reconciled with the tracked tickets (#1662).
2.5.1 — 2026-05-28¶
Fixed¶
- Use the selected serial rig profile's
default_baudwhen--modelis provided without--serial-baud, fixing Xiegu X6200 managed/local launches that otherwise fell back to 115200 baud and timed out CAT/control reads (#1642).
2.5.0 — 2026-05-28¶
Added¶
- Added ordered HTTP command batches so local automation can run structured commands in request order with per-step results and timeout handling (#1606, 31c41425).
- Published the full HTTP/WebSocket command catalog in the API docs so client authors can discover the supported command names, parameters, and response shapes without reading server code (#1607, f4c113e7).
- Exposed a queued, fire-and-forget raw CI-V
send_civcommand through the HTTP/WS command surface and ordered batch endpoint so automation clients can send vendor-specific CI-V without opening a competing radio session (#1616, fb2965ee). - Added
POST /api/v1/civ/transactionfor scoped raw CI-V transactions with explicitexpectmodes (none,ack,data), deterministic ACK/NAK/data JSON results, bounded timeouts, and CI-V ownership guarding (#1622, #1623). - Added response-capable raw CI-V transaction steps to ordered HTTP batches via
type: "raw_civ_transaction", preserving order when mixed with queued command steps (#1637, 06c4c7b0). - Added an internal raw CI-V pipe and local Hamlib A1 bridge runner for development of transparent external-CAT ownership over CI-V serial backends (#1610, #1612, #1614, #1615, c371bc3b, 085cd617, c3a09e1e, deabb95d).
- Added a distinct Xiegu X6200 native rig profile and discovery
disambiguation from IC-705 despite the shared factory CI-V address
0xA4(#1630, 7b593eee). - Documented Python usage for response-capable raw CI-V HTTP transactions (#1626).
Changed¶
- Cleaned up type-checking visible at integration boundaries, including raw CI-V transaction coverage and audio TX PCM signatures, reducing false positives for typed downstream users (#1628, #1629, db90a992, bda2ed0a, a356a1ab).
Fixed¶
- Registered Xiegu X6200 in the CLI model registry so
--model X6200resolves to the X6200 preset instead of silently falling back to IC-7610 (#1631, 204aea24). - Stopped sending LAN
OpenClosepackets on the serial CI-V wire, avoiding an X6200 flicker/wedge failure mode observed on hardware (#1632, 28a962d0).
Docs¶
- Clarified raw CI-V transaction and batch contracts, including the split between public/dev-facing raw transaction APIs and the internal experimental Hamlib A1 raw-pipe bridge path (#1616, #1637, #1638).
2.4.0 — 2026-05-24¶
Added¶
- External rigctld client backend that talks to a separately managed
rigctldprocess so Core can interoperate with Hamlib-managed transports without hosting its own rigctld (#1579, 32753adf). - Safe Hamlib probe ranking internals that score candidate transports without committing to them, used by the discover flow (#1581, 68907f6c).
- Serial inventory now includes Hamlib metadata for each enumerated port so upstream UIs can present capability and provenance hints (#1580, f6b4ca35).
- Hamlib provider validation in the discover CLI to confirm a candidate probe matches the expected radio profile before activation (#1582, 7e2f04a0).
Fixed¶
- Hardened the Windows core runtime — process lifecycle, signal handling, and cleanup paths now align with the desktop shell's expectations on Windows (#1594, 4ec683f8).
Docs¶
- Defined the public Hamlib provider contract, the provider strategy alignment, the agent handoff, and the rigplane.dev provider guide so external integrations have a single source of truth for the Hamlib path (#1577, #1583, #1590, #1592, f6b0c099, bd9cbbf4, 3e6c5a8e, c2d73d74).
2.3.1 — 2026-05-23¶
Added¶
- TX audio streams now expose write-health snapshots with queued, dropped, attempted, completed, failed, and last-error counters for diagnosing local transmit paths (#1553, 9125216b).
Fixed¶
- PortAudio playback now uses the callback ring, coalesces small writes, increases write chunk size, and keeps playback writes nonblocking to reduce local audio underruns (#1554, #1555, #1556, #1557, #1559, 2dcf7973, f7021beb, 763f2fd0, af51ce12).
- CI-V reconnect recovery now stays tied to the current generation, retries with ephemeral local ports when needed, and avoids spurious recovery after benign transport churn (#1546, #1550, c29a8a9a, 2169cc4a, 11e682be).
- rigctld power status responses are now compatible with WSJT-X clients (c064e5f4).
- Opus TX fails closed when no transcoder is available instead of accepting unsupported PCM-through-Opus paths (#1569, c86da7b0).
- LAN data-port cooldown recovery now reconnects cleanly after the radio holds the port in cooldown (#1572, #1573, a6976a2c).
Docs¶
- Added public docs-site analytics and IndexNow post-deploy pings, and tuned the indexing policy for rigplane.dev (#1552, #1571, #1574, c50cadfb, a513a0ad, 36662cb7).
- Documented the PortAudio callback-ring intent and kept audio implementation comments public-safe (#1561, #1562, 94884a98, fdd37686).
2.3.0 — 2026-05-20¶
Added¶
- Added the Core Svelte i18n runtime, locale store, pluralization helpers,
pseudo-locale tooling, and bundled
en-US,ru-RU, andja-JPlocale catalogs for the first localized Core web surfaces (#1528, #1534, #1536). - Added the Core language preference UX and the cross-app locale preference contract used by local shells and integrations to hand off language hints without overwriting the user's stable Core preference (#1529, #1531).
- Added localization QA coverage: string inventory, runtime/unit tests, pseudo-locale smoke tests, visual QA fixtures, and Playwright i18n screenshot coverage for desktop and mobile app surfaces (#1530, #1533).
- Added localized server toast reason codes and migrated the P0 Core web surfaces to the i18n runtime, including app shell, status bar, install prompt, language selector, diagnostics dialog, and shared toasts (#1532).
Changed¶
- Moved quick/full/rebrand/agent-review CI workflows to the Mac mini build-tier runner while preserving the existing required gates (#1512, #1513, #1514).
- Moved the lightweight rebrand and agent-review gate jobs to GitHub-hosted runners so protected-branch release fixes are not blocked when the self-hosted gate runner is unavailable.
- Refined public packaging metadata and docs SEO/navigation for the multi-vendor RigPlane framing, including Project Overview, migration guidance, per-page descriptions, and setup-guide download CTAs (#1510, #1511, #1538, #1540, #1541, #1542).
Fixed¶
- Stabilized the audio-scope aspect ratio in the web UI (#1519).
- Fixed WebView/Tauri audio compatibility by allowing browser clients to request PCM16 RX transport, adding a PCM16 TX microphone fallback when WebCodecs are unavailable, and guarding async PTT startup/release races before keying transmit (#1543).
- Fixed Japanese pilot copy after linguistic validation (#1537).
- Fixed release packaging so the generated frontend bundle is included in
sdist/wheel artifacts without relying on a local absolute
web/staticsymlink. - Fixed PyPI runtime dependency metadata so installed wheels can import and serve the Web UI diagnostics upload path without relying on dev-only dependencies.
- Refreshed the frontend lockfile to pick up Svelte/devalue security fixes;
npm audit --omit=devnow reports zero vulnerabilities.
Docs¶
- Added the Core user-facing string inventory, locale contract, translator guide, i18n contribution notes, and visual QA documentation (#1527, #1535).
2.2.0 — 2026-05-11¶
Added¶
- Added a friendly station-server discovery and status contract for desktop supervisors: UDP discovery now advertises the station-server schema, endpoint URLs, readiness, auth-required, backend, display name, and radio metadata while preserving legacy discovery fields (#1498).
- Added
GET /api/v1/stationand exposed station readiness inGET /api/v1/runtime, giving setup tools a stable machine-readable and human-readable status payload for LAN station-server discovery (#1498).
Docs¶
- Documented the new station-server discovery/status payloads and readiness states for supervisors and setup tools (#1498).
2.1.2 — 2026-05-09¶
Added¶
- Web state now exposes classified radio health with separate server reachability, radio link, readiness, likely-cause, and health revision fields (#1500).
Changed¶
/api/v1/stateETags now include both radio state revision and health revision, so health-only transitions reach UI clients instead of being hidden behind304 Not Modifiedresponses (#1501).- The web client accepts health-only state updates, stores radio health separately from legacy readiness flags, maps degraded causes into status-bar state, and blocks live-radio commands before optimistic updates while radio health is degraded (#1501, #1503).
Fixed¶
- Radios with no prior runtime evidence now remain classified as
unknowninstead of being prematurely reported asradio_not_responding(#1503).
Docs¶
- Documented IC-7610 VPN/tunnel MTU fragmentation symptoms and the
ICOM_AUDIO_SAMPLE_RATE=16000low-bandwidth workaround (#1504).
CI¶
- GitHub workflows were migrated to current Node 24-compatible action versions, and release-related docs/full-matrix workflows were manually validated after merge (#1499).
2.1.1 — 2026-05-09¶
Added¶
- Direct CW send/stop HTTP endpoints for local app integrations, plus clearer CLI launch guidance and web radio aliases (#1496).
Fixed¶
- Web TX audio on IC-7610 LAN now starts the accepted radio-native TX contract instead of starting Opus against the radio's PCM-only TX stream (#1493).
- Web RX LIVE toggling now sends
audio_start/audio_stopeven when the audio WebSocket is already open, fixing the first-click silence after page load (#1495). - IC-7610 LAN audio keeps 48 kHz PCM as the full-fidelity profile default; troubleshooting docs now call out VPN/tunnel MTU fragmentation issues and the
ICOM_AUDIO_SAMPLE_RATE=16000low-bandwidth workaround (#1494). - Existing Svelte check warnings in touched frontend files were removed so
npm run checkis clean for the hotfix branch (#1497).
2.1.0 — 2026-05-08¶
Added¶
- Managed station runtime support for Pro/supervisor integrations: runtime mode, health endpoints, startup events, token-file auth, setup-wizard discovery JSON, and stable supervisor API documentation (#1439, #1440, #1441, #1442, #1443, #1444, #1445).
- LAN/USB audio capability groundwork: radio-native audio policy contracts, USB audio device contracts and diagnostics, safe RX-only LAN audio probe CLI, JSON probe artifacts, cooldown/retry controls, and guarded profile proposal tooling (#1464, #1479, #1480, #1481, #1483, #1488).
- Automated audio regression harnesses for WSJT-X and TX pipeline work: in-process TX pipeline, rigctld WSJT-X replay, opt-in OS audio smoke, and IC-7610 hardware validation tests (#1450, #1451, #1452, #1453).
Changed¶
- WSJT-X compatibility now derives DATA/LAN policy from the resolved audio route instead of backend-name guards, preserving DATA1 as user-owned while selecting DATA2/LAN only for direct LAN radios that support it (#1446).
- Icom LAN audio profiles now carry evidence-backed PCM-first policies for IC-705, IC-9700, and IC-7610; IC-7610 defaults to the hardware-probed 48 kHz matrix with mono PCM TX (#1477, #1490).
- Browser audio routing keeps Opus as a consumer/web transport policy only; direct stock-radio LAN stream defaults remain radio-native PCM/u-law.
Fixed¶
- LAN discovery now advertises the RigPlane service identity after the v2 rebrand (#1485).
- Removed stale rebrand paths from agent commands and docs SEO metadata.
Docs¶
- Added audio codec policy, audio capability probing, audio profile audit, USB audio negotiation, managed runtime packaging, CLI, API, and audio recipe documentation for the new runtime/audio contracts.
2.0.3 — 2026-05-07¶
Fixed¶
- WSJT-X LAN TX audio on direct Icom LAN: send raw PCM when conninfo negotiates
PCM_1CH_16BITinstead of pushing Opus bytes into the radio's PCM TX stream (#1430, #1448, 999953e2). Adds explicit TX codec tracking; routes WSJT-X packet modes to DATA2/LAN only for direct Icom multi-DATA radios; keeps DATA1 user-owned (prewarm/profile-apply/state-restore no longer rewrite DATA1 MOD input). Validated on IC-7610 LAN — continuous TX audio reaches the radio.
Other¶
- Align
tests/test_pcm_e2e.pywith the new PCM-passthrough TX path: pin_audio_tx_codec = AudioCodec.OPUS_1CHon the test radio so the dummy transcoder is exercised, and use a properly-sized 1920-byte synthetic frame for loopback TX (6ad40e91, #1448 follow-up).
2.0.2 — 2026-05-06¶
Added¶
- Tailored in-repo release skill (
.claude/skills/release/SKILL.md) replacing the stale global slash command (#1436).
Fixed¶
- Frontend e2e: closed pre-existing Wave 2 rebrand leak in
playwright.config.tsandv2-ui-interactive.impl.ts; removed obsoleteICOM_LAN_V2_URLfallback (#1437).
Docs¶
- IC-7610 WSJT-X TX level workaround for FT8/JS8/fldigi multi-peak waterfall (#1382, #1384).
- GitHub Project workflow for Codex-style agents:
AGENTS.md, project doc,CLAUDE.mdlink (#1431).
2.0.1 — 2026-05-06¶
Fixed¶
- LAN TX audio bridge: fix audio not flowing in transmit direction (#1434, ba364e54)
2.0.0 — 2026-05-05¶
Headline: project renamed from icom-lan to rigplane. The old name was always misleading — the project shipped multi-vendor support (Icom CI-V, Yaesu CAT, Discovery TX-500, Xiegu X6200) over LAN, USB, and serial. Carrying "icom-lan" through a paid Pro tier also created a trademark risk on a vendor name we don't own. New brand at https://rigplane.dev.
Migration¶
Existing v1.x scripts continue to work without modification:
# This still works, emits DeprecationWarning on first import:
from icom_lan import IcomRadio, LanBackendConfig, create_radio
import icom_lan.web
# Canonical v2 form:
from rigplane import IcomRadio, LanBackendConfig, create_radio
import rigplane.web
The icom-lan CLI binary remains as a deprecated alias of rigplane (prints a notice to stderr, then forwards argv).
User-facing data (web-UI panel layouts, theme, auth token, memory channels, log directories) is migrated automatically on first launch — no re-login or reconfigure.
Breaking changes¶
- PyPI package:
icom-lan→rigplane. Preferred install:pip install rigplane. The oldicom-lanpackage on PyPI is frozen at v1.1.0; future releases ship asrigplane. - Python import path:
icom_lan.*→rigplane.*. Theicom_lanshim re-exports fromrigplanewithDeprecationWarningand will be removed in a future major release. - CLI binary:
icom-lan→rigplane.icom-lanretained as deprecated alias. - Environment variables:
ICOM_LAN_REPORT_ENDPOINT,ICOM_LAN_DISABLE_DIAGNOSTIC_LOGGING,ICOM_LAN_LOG_DIR→RIGPLANE_*. LAN discovery now usesb"RIGPLANE_DISCOVER\n"withb"ICOM_LAN_DISCOVER\n"accepted as a legacy request. - Diagnostic bundle schema: default emission is now
rigplane-bundle-v2(wasicom-lan-bundle-v1). The maintainer-operated triage service accepts both v1 and v2 for at least 12 months. - Exception class:
IcomLanError→RigplaneError. Class is re-exported fromicom_lanshim under both names.
Preserved (not renamed — vendor identifiers stay)¶
- Vendor classes:
IcomRadio,IcomBackend,IcomCommander,Icom7610Profile,YaesuRadio,YaesuCatRadio, etc. These name supported hardware vendors, not the product brand. - Backend directories:
src/rigplane/backends/icom7610/,…/yaesu_cat/, etc. The vendor-model component is preserved. - Vendor-config env vars:
ICOM_HOST,ICOM_USER,ICOM_PASS,ICOM_PORT,ICOM_AUDIO_*,ICOM_CIV_*, etc. - LAN discovery wire contract: magic byte sequence + service token unchanged in v2.0.0.
Added¶
rigplane-bundle-v2diagnostic-bundle wire-format spec (docs/contracts/diagnostic-bundle-v2.md). Bundle producer emits v2 by default; v1 emission preserved as opt-in.- Brand identity v1: wordmark, mark, lockup (light/dark/mono SVG), favicon set, app icons (Tauri + PWA), social cards, README banner, color palette, typography spec. Color palette: Ink
#0E1A28, Paper#F4F1EA, Signal#2D5F8A. Typography: IBM Plex Sans + IBM Plex Mono. - First-run platformdirs migration (
rigplane._platformdirs_migration). Copies legacy~/.cache/icom-lan/,~/.config/icom-lan/, etc. to correspondingrigplanepaths. Best-effort, idempotent, sentinel-guarded. - First-run localStorage migration (
frontend/src/lib/migrate-legacy-storage.ts). Auto-runs at module import (boot-order critical — stores read localStorage at module-init). Migrates 17 legacy keys to the new namespace. window.rigplaneExtensionHostprimary global for Pro local-extensions.window.icomLanExtensionHostalias preserved for v1.x extensions.- CI grep gate (
.github/workflows/rebrand-gate.yml) prevents accidental brand-string regressions.
Changed¶
- Default product description across docs, README, mkdocs
site_description, and PyPI metadata acknowledges multi-vendor support instead of the legacy Icom-only framing. - Docs site:
https://morozsm.github.io/icom-lan/→ https://rigplane.dev/. - GitHub repo:
morozsm/icom-lan→rigplane/rigplane-core. Auto-redirects active.
1.1.0 — 2026-05-04¶
Two headline items:
-
Critical fix for the WSJT-X / JS8Call audio regression on IC-7610 LAN bridges (#1381). Since v0.17.0 the LAN audio bridge fed stereo radio bytes (the new dual-RX
PCM_2CH_16BITdefault) into a mono PortAudio stream, halving the effective sample-rate. Users observed 1.5 kHz audio with the 3 kHz USB filter and TX silence. Fixed: the bridge now auto-detects the radio's negotiated channel count and downmixes (L+R)/2 → mono inside the bridge, mirroring the web UI broadcaster pattern. WSJT-X / JS8Call / fldigi see a clean mono stream at full sample-rate again. Single-RX rigs and the dual-RX web routing path are unaffected. -
Opt-in diagnostic reporting end-to-end (epic #1385). New
icom-lan diagnoseCLI subcommand + Web UI "Send diagnostic report" dialog build a redacted bundle from 9 contributors and (only on explicit user action) post to a maintainer-operated triage service.
Plus frontend tech-debt closeout (#1369 cluster) and a configurable RX audio jitter buffer for unreliable LAN links (#1363).
Added¶
- Diagnostic data collection (epic #1385). New
icom-lan diagnoseCLI subcommand and Web UI "Send diagnostic report" dialog (Settings panel) collect a structured ZIP from 9 contributors (system,invocation,radio,audio,logs,state,errors,dependencies,config) with PII redaction (paths, IPs, credentials, tokens). Default is local save; explicit--uploadopt-in posts tohttps://reports.msmsoft.net/v1/diagnostics/upload(override viaICOM_LAN_REPORT_ENDPOINT). Privacy invariants perdocs/architecture/open-core-policy.md§2 carve-out. - Public contract
docs/contracts/diagnostic-bundle-v1.mddocuments the anonymous-tier upload protocol; third-party self-hosters can implement against it (#1398). DiagnosticContributorprotocol + entry-point discovery (icom_lan.diagnosticsgroup) letsicom-lan-proplug in additional data sources without touching open-core (#1389).- HTTP upload client with typed errors (
RateLimited,BundleTooLarge,ForbiddenContent,MetadataInvalid,NetworkError) and aheader_providerhook for Pro-side signed uploads (#1394). - Always-on rotating diagnostic log (
SafeRotatingFileHandler,~/.cache/icom-lan/logs/, ~15 MiB cap) scoped tologging.getLogger("icom_lan")so library-mode use stays clean (#1387). - Configurable RX audio jitter buffer for unreliable LAN links (#1363).
- User documentation:
docs/guide/diagnostic-reports.mdwalkthrough with privacy invariants, troubleshooting, and CLI reference (#1401).
Changed¶
pip install icom-lanno longer transitively importsaiohttp— theicom_lan.diagnostics.uploadmodule is lazy-loaded via PEP 562__getattr__so all CLI commands work without aiohttp installed (a dev-only dependency).icom-lan diagnose --uploademits a friendly install hint instead of a Python traceback when aiohttp is missing (#1417, #1420).- Open-core policy formally documents the carve-out for user-initiated
diagnostic support reports alongside the existing "no telemetry"
rule (
docs/architecture/open-core-policy.md§2).
Fixed¶
AudioBridgestereo-to-mono downmix for hamlib clients (#1381). See headline #1 above. Critical regression fix for IC-7610 LAN bridge users running WSJT-X / JS8Call / fldigi viaicom-lan web --bridge.- IPv6 redaction handles compressed
::forms correctly viaipaddress.ip_address()validation; previously public addresses ending in::1were misclassified as loopback (#1418). - Path redaction skips URL-embedded paths via negative lookbehind (#1418).
- Web UI
handle_sendrace condition: atomic CSRF check-and-set under the session lock prevents concurrent double-uploads (#1419). - Web UI modal
handleCancelrespectsbusystate (Escape and backdrop click ignored while a request is in flight) (#1419). - macOS device labels with embedded usernames scrubbed in the audio contributor (#1418).
- Hostname redaction (
*.example.com,*.local) for the radio contributor'shostfield (#1418). - Bundle assembler cleans up partial output from failed contributors so it doesn't leak into the ZIP (#1418).
- PATH env var redacted per-segment so the
:separator no longer breaks the lookbehind guard (#1418). - Threading exceptions captured by
_error_ringviathreading.excepthookalongsidesys.excepthook(#1418). - Frontend tech-debt cluster cleared (epic #1369): 173 svelte-check errors from self-wired panel migration (#1370), 8 misc type errors (#1374), nullable narrowing + Promise generic in HTTP client (#1372), vitest 4 mock signatures (#1371), invalid jitter env-var half-apply (#1366), ruff format sweep (#1368).
- CI gate:
npm run check+ vitest now blocks frontend regressions (#1375).
Docs¶
- Diagnostic data collection design spec, 530 LOC (#1386).
- Public bundle contract
diagnostic-bundle-v1.md(#1398). - User guide for diagnostic reports (#1401).
- Open-core policy carve-out for user-initiated reports (#1418).
1.0.1 — 2026-05-01¶
Docs build + internal-import canonicalization patch. No runtime behaviour
changes, no public API changes — pip install --upgrade icom-lan from
1.0.0 is mechanical.
Fixed¶
- Docs build: mkdocstrings autorefs in
docs/api/{radios,audio,commander,sync,types}.mdresolve cleanly (#1338). Previouslyicom_lan.<shim>.Xreferences could not be traversed by griffe throughsys.modules-alias shims, breaking thedocs.ymlworkflow. - Docs build:
[Followup]markdown brackets indocs/plans/issue-drafts/00-epic.mdare backtick-quoted somkdocs_autorefsstops trying to resolve them as cross-reference targets under--strict.
Internal¶
- Canonicalised 60 in-package imports off shim paths (#1338). Files in
src/icom_lan/<layer>/*.pynow import siblings via canonical paths (e.g.icom_lan.commands.commanderinstead of the top-levelicom_lan.commandershim). Public API shims are preserved verbatim — downstreamfrom icom_lan.commander import …keeps working. - Canonicalised 13 mkdocstrings refs in
docs/api/*.md(#1338). Tutorial code examples indocs/guide/*.mdanddocs/radio-protocol.mddeliberately keep using public-stable shim paths. - Tagged release artefact (
RELEASE_NOTES.md) removed from the repo per the post-release housekeeping convention; the GitHub release body remains canonical.
1.0.0 — 2026-05-01¶
Public API stability commitment. The Tier 1 surface documented in
docs/api/public-api-surface.md — the Radio protocol, capability
protocols (AudioCapable, ScopeCapable, MetersCapable,
LevelsCapable, StatePollable, RigctldRoutable, UsbAudioCapable,
…), create_radio / BackendConfig, and the frontend/src/lib/local-extensions/
host API — is now under SemVer. No public API breaking changes vs
v0.19.0; every prior import path continues to work via sys.modules-aliased
re-export shims.
Added¶
- Tier 1 Capability Protocols extended (epic #1322). Four new
protocols on
icom_lan.radio_protocolenableisinstance-based feature detection — backends are now selected by capability, not by backend-id string: StatePollable+StatePoller(#1322, #1323) — replaces backend-id branching inweb_startup.RigctldRoutable(#1322, #1324) — pluggable rigctld routing.UsbAudioCapable(#1322, #1326) — uniform USB audio device contract.PowerControlCapable.native_power_unit(#1322, #1325) — drops the last backend-id discriminator fromweb/handlers/control.py.- rigctld per-VFO routing (#1342–#1346). Parser now accepts the
Hamlib
chk_vfo=1leading VFO argument and routesf/m/t/s/S/j/l/L/u/Uper-VFO.RigctldRoutingProtocol gains an optionalvfokwarg;AuditRecordgains avfocolumn.chk_vfo='1'is re-enabled for dual-RX (Variant A complete). YaesuCatRadio.set_rf_power(#1331) — Yaesu CAT backend now conforms toPowerControlCapable;web/handlers/control.set_powerswitched from string-cap toisinstance(PowerControlCapable).
Changed¶
- Internal: source code reorganized into explicit layered structure
(epic #1283).
src/icom_lan/is now organised into 11 layered packages —core/,commands/,profiles/,audio/,scope/,dsp/,runtime/,backends/,web/,rigctld/,cli/— with per-layerLAYER.mdcharters and the full layer matrix indocs/plans/2026-04-29-modularization-plan.md. No public API changes: every existingfrom icom_lan.<old_path> import …keeps working viasys.modules-aliased re-export shims, and the Tier 1 / Tier 2 lazy surface inicom_lan/__init__.pyis unchanged. New code SHOULD use canonical layer paths (icom_lan.runtime.radio,icom_lan.backends.discovery, …); seeARCHITECTURE.md. import-linterenforces layer boundaries. Repo-root.importlinterdeclares one layered contract plus three sibling-independence contracts (web⊥rigctld,profiles⊥audio,commands⊥scope⊥dsp); run locally viauv run lint-imports. CI gates every PR.AudioStats.jitter_msrenamed toreorder_depth_ema_ms(#1231). The field measures reorder-depth EMA, not RFC 3550 jitter. Internal field; no back-compat alias._require_*()helpers for optional deps (#1274). Newsrc/icom_lan/_optional_deps.pyprovides_require_numpy,_require_sounddevice,_require_opuslib,_require_pillow. Ad-hoctry/except ImportErrorblocks across the codebase now share uniform error messages.
Fixed¶
rigctld¶
chk_vfonow returns"0"unconditionally for all radio profiles (#1319). The dual-RX"1"advertising introduced in v0.17.0 caused WSJT-X / fldigi / JS8Call to fail with "Hamlib error: Feature not implemented" on IC-7610, IC-9700, and FTX-1 because Hamlib'svfo_optmode prefixes every command with a VFO token. After the rollback, Variant A (#1342–#1346) re-enabledchk_vfo='1'for dual-RX with full parser + per-VFO routing — WSJT-X / fldigi / JS8Call golden replay now passes.
Web server¶
- Blocking file I/O offloaded via
asyncio.to_thread(#1332).server._handle_band_plan_configandeibi.load_cacheno longer block the event loop on disk reads. runtime_capabilitiesfallback recognisesUsbAudioCapable(#1356). USB-audio detection now matches the Capability Protocol path.
Sync API¶
get_alc_meterexposed onsync.IcomRadio(#1228, refs #1226). Was missing after the v0.19get_alcremoval.
CI¶
- Subdirectory tests now actually run (#1352). The
tests/test_*.pyglob silently skipped suites in nested directories; the glob now recurses.
Removed¶
These deprecation closures were announced in v0.19 and dropped on schedule.
IcomRadio.set_split_mode(#1205). Useset_split(SplitCapable).IcomRadio.get_alc/sync.IcomRadio.get_alc(#1207). Useget_alc_meter(MetersCapable).icom_lan.commands.levelsaliases (#1208):get_power,set_power,get_sql,set_sql. Use canonicalget_rf_power/set_rf_power/get_squelch/set_squelch.IcomRadio.set_vfo("A"/"B"/"MAIN"/"SUB")legacy overload +select_vfoalias (#1206). UseReceiverBankCapable.select_receiver(MAIN/SUB) andVfoSlotCapable.set_vfo_slot(A/B). Legacy fallback paths inweb/radio_poller.pyandrigctld/handler.pyremain for third-party backends.meter_cal._TABLESandmeter_cal.calibrate()(#1209). Unreachable since #1173 shipped per-rig TOML calibration.MeterTypeandinterpolate_swrremain exported.- Web UI v1 layout shell (#1216, #1217, #1218, #1220, #1227). Legacy
AppShell/DesktopLayout/MobileLayoutremoved. v2 (RadioLayoutV2+frontend/src/components-v2/) has been the default since v0.15.1 and is now the only supported path. The?ui=v1URL fallback and theui-versionstore are gone.
Internal¶
Frontend¶
- Panel → adapter migration complete (#1240, #1244–#1248, #1241). All
18 panels under
components-v2/panels/no longer import directly from$lib/stores/*; capability flags and live radio state flow via panel adapters inwiring/. ESLintno-restricted-importsenforces the boundary (tests exempt for mocking). ControlButtonDemois now lazy-loaded (#1232). Code-split out of the main bundle.- Type safety: removed
as anycasts incommand-bus.ts(#1233). - ESLint tightened — v2 layering only (#1219). Banned
$lib/transport/*and$lib/audio/audio-managerfrom panels and layouts.
radio.py god-object decomposition (#1063 wave 3)¶
_fetch_initial_stateextracted toradio_initial_state.py(#1260)._reconnect_loop/_watchdog_loopextracted toradio_reconnect.py(#1259).snapshot_state/restore_stateextracted toradio_state_snapshot.py(#1258).
Dispatch-table refactors¶
_civ_rx._update_radio_state_from_frame→ table-driven dispatch (#1257). The 400-line if/elif over CI-V commands now dispatches via_HANDLERS: dict[int, Callable]. Behaviour preserved — verified by 72 golden-test fixtures (#1266). Dead code at lines 1082-1093 (duplicate cmd 0x12 block) collapsed.transport._handle_packet→ dispatch table (#1239). Six packet types now dispatch via a dict.web/handlers/control._enqueue_read_only→ dispatch table (#1263).
Web server decomposition (#1063 wave 4)¶
WebServer.start()/stop()orchestration extracted toweb_startup.py(#1261). ~200 LOC moved out;WebServer.start()/stop()are thin delegators.- Method/path routing extracted to
web_routing.py(#1262).
Shared helpers¶
BoundedQueueextracted to_bounded_queue.py(#1230). Four asyncio call sites (transport RX, radio scope/civ event queues, web fanout) now share one bounded-queue implementation.
Tests¶
- Public-API surface regression test (#1273). New
tests/test_public_api_surface.pyasserts every Tier 1 symbol fromdocs/api/public-api-surface.mdimports cleanly AND that Tier 1 imports do not transitively pull Tier 3 modules intosys.modules. - Golden-test fixtures for
_civ_rx(#1256). 72 synthetic frame fixtures + parametrized dispatch test fence the upcoming refactor. - rigctld parser VFO-prefix tests + CI consistency guard +
dump_statesnapshots (#1342). Wire-level integration test - golden replay fixtures (Variant A foundation).
- Lazy-resolution contract test (#1284). Locks down PEP 562
__getattr__Tier 2 surface.
Documentation¶
docs/architecture/open-core-policy.md(#1276) — codifies hard constraints: no telemetry, headless mode is sacred, no hollowing out, Radio protocol +local-extensions/as the Pro boundary, frontend WebKitGTK-floor compatibility.local-extensions/documented as Tier 1 Pro-facing contract (#1277).docs/api/public-api-surface.mdlists exported types and functions fromfrontend/src/lib/local-extensions/{host-api,manifest}.tswith breakage policy.AudioBackendProtocol Tier 2 stability marker (#1275).ARCHITECTURE.mdrefreshed for layered structure + per-layerLAYER.mdcharters (11 files) + CLAUDE.md "Layer boundaries" section.
0.19.0 — 2026-04-29¶
Tier-1 API stability commitment¶
- API: tier-1 stability commitment from v0.19 (#1195). The public API
surface is now organised into three explicit tiers (stable / best-effort /
internal) with a documented migration policy. See
docs/api/public-api-surface.mdfor the tier policy, full symbol lists, and import examples. - Lazy
__init__.pyvia PEP 562 (#1194). Trimmed eager imports from 203 to 71 lines; tier-2 symbols now lazy-load on first access.from icom_lan import Radiono longer transitively pulls inweb/,cli,rigctld/, oraudio.backend— measured ~13 % fewer submodules insys.modules. - Layering enforcement via ruff TID251 (#1196). Tier-3 internals
(
icom_lan.web.*,icom_lan.rigctld.*,icom_lan.cli) now banned from cross-tree imports. Pre-existingicom_lan.radio.IcomRadioban preserved in web modules (#1201).
Added¶
- Receiver-tier protocols on backends.
ReceiverBankCapableandVfoSlotCapable(declared since #711, never implemented) now have concrete impls on both Icom (#1170) and Yaesu CAT (#1171) backends. Profile-driven dispatch covers IC-7610 / IC-9700 / IC-7300 / IC-705 and FTX-1 / Lab599 / X6100 single-RX rigs. SplitCapableprotocol (#1108) — universal split control across all supported HF/VHF rigs.set_split_modedeprecated, alias retained until v0.20.RitXitCapableprotocol (#1099) — extracted fromTransceiverStatusCapable. Six canonical*_rit_*methods on YaesuCatRadio plus read-modify-write fix preserving the unaffected RX/TX bit on CF000.- 17 protocol declarations added across existing capability protocols:
DspControlCapable(filter family, notch, agc),LevelsCapable(af_level/rf_gain getters, squelch),MetersCapable(power_meter, alc_meter, swr_meter),AudioCapable(codec/sample_rate properties),ScopeCapable(getters, scope_stream),VoiceControlCapable(get_compressor),CwControlCapable(break_in),AntennaControlCapable(get_attenuator),PowerControlCapable(get_rf_power lift). - Default web UX (#1087):
icom-lan webnow auto-detects loopback and enables the audio bridge by default; rigctld serves on 4532 by default with--no-rigctldopt-out (#1088, #1089). [bridge]extras folded into core (#1090).pip install icom-lanshipsopuslib,sounddevice, andnumpyout of the box. Legacy[audio]and[bridge]extras retained as no-op aliases.- Calibrated SWR float on Icom rigs (#1173).
IcomRadio.get_swrnow returns calibrated SWR (1.0–6.0+) via TOML calibration tables (5 anchor points per rig, sourced from official Icom CI-V references). Newget_swr_meter()on async + sync API for raw 0-255 access. SetPowerpoller dataclass unit-tagged (#1168) — explicitunit="raw_255"(Icom default) vs"watts"(Yaesu) ends silent Icom/Yaesu unit mismatch.- State-contract sweep (#1169):
RadioState.cw_spotis now tri-state (bool | None); Yaesu-specificrx_func_mode/tx_func_modemoved intoYaesuStateExtension. - Frontend runtime architecture (epic #708 follow-up).
FrontendRuntimesingleton withScopeController, lib/runtime/ pattern, panel-props / panel-commands separation enforced via ESLint.
Changed¶
__init__.pyis now ~80 lines (was 203). Tier-1 symbols eager; everything else lazy via PEP 562__getattr__.- Audio extras simplified.
opuslib,sounddevice,numpymoved from[bridge]extra into the maindependencieslist.[dsp]extra now installs onlyscipy>=1.11. Documentation updated accordingly (#1090). set_vfo("A"/"B"/"MAIN"/"SUB")overload deprecated (#1187, #1172). Web and rigctld migrated toselect_receiver/set_vfo_slot. Legacy overload emitsDeprecationWarning; removal scheduled for v0.20.- Filter-width unified on segmented BCD index (#1157) per wfview
reference. Removed
direct_bcd_hzprofile encoding (was incorrect for IC-705 / IC-9700). Added per-mode segment tables to all four Icom rigs. - Scope poller uses public ScopeCapable getters (#1166) with bounded
per-call timeout (#1186). Eliminates raw
_civ(0x27, …)layering violation while preserving fire-and-forget semantics (#1188).
Fixed¶
- 6 rigctld consistency fixes (consolidating P0-classified findings now reclassified P3 — only Yaesu-routing path was active): dial-lock (#1092), tuner-status (#1094), squelch dispatch (#1093), notch-filter (#1102), powerstat stub (#1095), set_level SQL set-side (#1163).
- Yaesu compressor-level alias delegation (#1098) — was returning
hardcoded 0; now correctly forwards to
*_processor_level. - Yaesu APF mode reachable from web (#1110) — poller no longer drops
SetApfactions; mode-1 toggle preserves user-tuned freq (#1141). set_vfolegacy fallback for backends withoutReceiverBankCapable(#1189) —SerialMockRadioand similar legacy backends no longer silently no-opV VFOA/V VFOB.get_splitcache fallback honorsTimeoutError(#1158), not justCommandError— completes the documented fallback contract.- CW pitch idx ↔ Hz conversion on Yaesu (#1162) —
get_cw_pitch/set_cw_pitchnow correctly translate Yaesu's 0-75 idx to/from 300-1050 Hz, fixing silent state corruption instate.cw_pitch. - CI-V worker cancel propagation (#1188) — caller-side
wait_forcancel now cancels the in-flight CI-V command at worker level, preventing cascade-skipping of subsequent queued commands. - Layering violation in scope poller (#1166) and filter-width
encoding (#1101) — moved from raw
_civin web/ into backend protocol methods. - IC-7300 GET decode bug — silently read 2 BCD bytes when radio sent 1; fixed during filter-width unification.
- VfoSlotCapable RTW bug on Yaesu (#1099) —
set_rit_status/set_rit_tx_statusnow read CF000 first to preserve the unaffected RX/TX bit (P1-02 from audit catalog). - Numerous Codex post-merge review fixes — covering dispatch table
gaps, capability fallbacks, type-signature drift, and exception-narrowing
across
web/andrigctld/.
Removed¶
vfo_exchange/vfo_equalizealiases (#1114) — deprecated since v0.17, removed per accelerated Q4 deprecation policy.- Seven LAN audio aliases overdue from v0.15 (#1111) —
start_audio_rx,stop_audio_rx,start_audio_tx,push_audio_tx,start_audio,stop_audio,stop_audio_tx. Use the canonical*_opus-suffixed names. - Internal facade helpers privatised (#1112):
_push_pcm_tx,_push_tx_pcm,_has_command,_has_write_command. Publicsupports_commandis the canonical introspection API. audio_capabilities()instance method — use module-leveltypes.get_audio_capabilities().direct_bcd_hzfilter-width encoding — no Icom rig actually used it; unified on segmented BCD index per wfview.
Deprecated¶
icom_lan.commands.levelsbackward-compat aliases (#1167):get_power,set_power,get_sql,set_sql. Removal v0.20. Use canonicalget_rf_power/set_rf_power/get_squelch/set_squelch.set_split_mode(Icom) — replaced byset_split(SplitCapable). Removal v0.20.set_vfo("A"/"B"/"MAIN"/"SUB")overload — replaced byselect_receiver+set_vfo_slot. Removal v0.20.get_alc(Icom) — replaced byget_alc_meter(MetersCapable).
Docs¶
- New "Stability tiers" section in
docs/api/public-api-surface.mddocuments tier-1 / 2 / 3 policy with full symbol lists and migration rules. - Per-rig SWR calibration anchor tables documented in
rigs/*.tomlper R1 research (sourced from official Icom CI-V References + wfview).
Internal¶
- 47 PRs landed across this release cycle including audit work, Tier-1 stabilization, Form H UX defaults, Form F sealing, and Codex automated review follow-ups.
- 7 architectural epics closed: API audit (#1071), v0.18.1 hotfix bundle (#1091), Tier-1 stabilization (#1096), Form H UX defaults (#1087), Codex post-merge sweep (#1140), audit closure (#1165), Form F sealing (#1193).
0.18.0 — 2026-04-19¶
Added¶
- Meter responsiveness (epic #936) — backend priority polling tier plus frontend rAF needle smoothing. S-meter on RX jumps from ~3 Hz to an effective 16 Hz with asymmetric visual smoothing (50 ms attack / 150 ms release); Pwr/SWR/ALC on TX go from ~3 Hz to 6.7 Hz each. PTT-on skips the LOW tier so TX-tier meters meet the smooth-needle acceptance target; Vd/Id/Comp deferred to a LOW tier (~750 ms each). (#936, #937, #938, #941)
- Amber-LCD twin skins (epic #887) —
lcd-cockpitandlcd-scopeskin variants with dedicated wrappers, 60/40 scope-dominant grid for single-RX, peer dual-cockpit grid for dual-RX, per-VFO indicator zones + global strip, AmberAfScope dominant mode + running-max line, ghost-graticule fallback when AF-FFT is unavailable, memory / recent-QSY strip, telemetry strip (VD/TEMP/ID + sparklines), Display Mode effects (vintage / CRT / flicker), LCD contrast in the control-strip, warm-dark theme. (#808, #823, #836, #837, #838, #861, #864, #877, #887-#895, #896-#900, #902, #904-#908, #911, #914, #915, #916, #918, #919, #920, #921, #929, #932, #933) - Mobile IA overhaul — chip-scroll navigation + ESSENTIALS panel; persistent guarded PTT FAB; container-query collapse + aux row reserve; first-class RIT/XIT mobile chip; auto-collapse mode-specific panels. (#810, #839, #840, #842, #843, #857, #885, #894, #912, #926, #928, #930)
- MetersDockPanel (epic #820) — new station-health dock with Po/SWR/ALC/S tiles plus Id/Vd/COMP tiles gated by capabilities; peak-hold and SWR/ALC fault highlighting; replaced legacy bottom-dock cards; audio spectrum relocated accordingly. (#820, #821, #822, #823, #848, #866, #872, #878, #880)
- Active-receiver UI (epic #825) —
ActiveReceiverTogglesegmented[M|S]control; keyboard bindings for active-receiver switch with audio-focus sync; legacy activate-chip + adapter removed. (#825, #827, #828, #856, #858, #868, #875) - Skin system — StatusBar dropdown skin-switcher;
lcd-cockpit/lcd-scoperegistered as first-class skin IDs; variant prop threaded through dedicated wrappers. (#888, #889, #895, #901, #902, #904, #909, #913) - Spectrum toolbar — keyboard shortcuts; grouping + separators + visual spec. (#830, #831, #847, #855)
- Settings reorg — settings gear moved into StatusBar; subgrid VFO digit/badge layout resolves crowding. (#807, #860, #865, #871)
Changed¶
- LCD token cascade + contrast core — unified grid scaffold, AmberCockpit extracted, behavior-preserving refactors. (#833, #844, #853, #859, #890, #903)
- Panel simplification — removed panel-wide click-to-activate and STANDBY/ACTIVE pill; consolidated ON/OFF into a single power toggle; renamed SETTINGS → SETUP and pruned chip-duplicated panels; strengthened active/inactive VFO panel treatment. (#805, #824, #826, #841, #849, #854, #867, #925)
- Bottom-dock reshuffle — replaced bottom-dock cards with
MetersDockPanel; relocatedAudioSpectrumPanel; BRT/REF moved to mobile spectrum gear; SCOPE status moved toVfoHeaderbridge. (#812, #821, #832, #869, #870, #880)
Fixed¶
- Scope reconnect deadlock — break scope re-enable deadlock on reconnect. (#881)
- CI-V watchdog — self-cancel + patient OpenClose recovery. (#851)
- Session rejection — retry-with-mono fallback on stereo
rx_codecsession rejection. (#797, #802) - SWR calibration — honor TOML calibration table at
raw=0; non-linear calibration from TOML config. (#440, #924, #927) - hamlib rig model — read from TOML in Yaesu
dump_state. (#441, #923) - TX meter telemetry — preserved when SUB is active; Vd tile readable in RX idle. (#822, #872, #891, #910)
- AmberScope / AmberCockpit fallbacks — VFO A filter-width fallback restored; ghost fallback when AF-FFT unavailable. (#918, #919, #920, #921)
- Scope controls on mobile + v1 — restored source/dual controls; SCOPE pills no longer cropped by VFO bridge column overflow. (#832, #873, #883)
- lcd-scope variant reachability — end-to-end variant plumbing via dedicated skin wrappers. (#895, #909, #913)
- v2-control-button — distinguish idle vs disabled states. (#804, #879)
- RightSidebar — restored
AudioSpectrumPanel; preserve cross-sidebar drag inloadPanelOrder. (#884, #886) - Codex review batches — P1/P2 findings resolved across mobile grid rows, IP+ per-RX, legacy LCD, QSY debounce + orientation PTT release, tile-smoother pruning on capability toggle. (#887, #917, #931, #934, #935, #941)
Docs¶
- LCD twin-skin redesign plan (epic #887).
- UI refinement design spikes (epic #818, #819).
Chores¶
- Repo-wide
ruff formatpass (#922). - 53 Svelte build warnings cleared — build now emits 0 warnings.
chore(#828)VFO area tooltip audit + standardization (#882).chore(#829)StatusBar tooltip audit + standardization (#876).
0.17.0 — 2026-04-18¶
Added¶
- Dual-RX stereo LAN audio (epic #787) — IC-7610 now delivers true stereo
L=MAIN, R=SUB over LAN. Backend negotiates
PCM_2CH_16BITin conninfo, locksPhones L/R Mix = OFFat relay start, and gates the behaviour on a newlan_dual_rx_audio_routingcapability so IC-9700 / FTX-1 aren't affected. Frontend resolvesfocus×split_stereovia a WebAudio ChannelSplitter + per-channel gain + panner graph — no CI-V round-trip for routing. (#752, #753, #756, #757, #770, #775, #776, #777, #778, #779, #781, #787, #788, #789, #790, #791, #792, #793, #794, #795, #798, #799, #800) - Dual VFO / dual receiver model (epic #708) — new
ReceiverBankCapable,VfoSlotCapable,TransceiverBankCapableprotocols; per-receiver A/BVfoSlotState; splitswap_ab/swap_main_subcommand codes;DualVfoDisplayshowing both MAIN+SUB on the desktop skin; receiver focus selector on the mobile skin. (#708, #709, #710, #711, #712, #714, #715, #716, #717, #718, #719, #722) - rigctld full VFOA/VFOB protocol — implements the complete Hamlib per-VFO command set so split-aware digital-mode clients (WSJT-X, fldigi, JS8Call) round-trip cleanly. (#722, #723)
- Composite WS commands —
quick_dualwatchandquick_splitbatch the radio side of the DW/split setup into single commands with double-click / long-press affordances in the UI. (#775, #776, #778, #779) - CLI log rotation —
RotatingFileHandlercontrolled byICOM_LOG_MAX_BYTESandICOM_LOG_BACKUP_COUNTenv vars; prevents unbounded growth oflogs/icom-lan.log.
Changed¶
- IC-7610 profile declares
lan_dual_rx_audio_routingcapability and migrates VFO codes toswap_main_sub/equal_main_subplus a new0x14 0x0Dcmd29 route. (#748) - IC-9700 profile correctly declares the MAIN/SUB scheme with proper byte codes. (#713)
- IC-705 / IC-7300 migrated from legacy
swap/equalkeys toswap_ab/equal_ab. - VfoPanel now uses
receiverLabel+vfoSlotLabelfor correct dual-VFO display naming. (#747, #728) - RFC §11 documents the dual-RX LAN routing contract: wire format,
the Phones L/R Mix-OFF invariant, the focus × split_stereo gain/pan
table, and the historical context of the
0x02/0x03trap.
Fixed¶
- IC-7610 LAN session allocation — decoupled
tx_codecfromrx_codecin conninfo; stock firmware rejected the session witherror=0xFFFFFFFFwhentx_codecwas a 2-channel value (mic path is mono-only; wfview UI enforces the same constraint). (#794, #795) - CollapsiblePanel phantom-collapse on hover — left-sidebar panels
spontaneously toggled when the mouse passed over their headers. Root
cause was an uninitialised swipe-tracking state that was updated on
plain
pointermove; now gated onswipeActiveset only bypointerdown. (#796) - Audio WebSocket queue —
audio_configWS send is queued when the socket is not yet open and flushed ononopen, so ACTIVATE on MAIN/SUB always reaches the radio. (#786) - Optimistic UI state — equalize / swap operations update the UI immediately; audio focus follows ACTIVATE. (#785)
- Scope follows active receiver — spectrum/waterfall switches to the
newly selected MAIN/SUB band on every
0x07 0xD0/0xD1. (#784) - Broadcaster mid-stream codec refresh — picks up codec / channel / sample-rate changes without reconnecting. (#766, #769)
- Broadcaster frame_ms — derived from actual payload size, not hard-coded 20 ms; fixes label mismatch on IC-7610 PCM16 packets. (#765)
- VFO MAIN/SUB buttons now emit proper receiver-select
(
0x07 0xD0/0xD1), not the MAIN↔SUB swap hack. (#773) - MAIN↔SUB poller flip on IC-7610 + silence
Radio.__del__test warnings. (#751) - sync.py default codec aligned with the async
IcomRadiodefault — both paths now returnPCM_2CH_16BITby default. (#798) - vitest flakiness — split into fast + isolated projects to stabilise keyboard-wiring tests. (#771, #782)
- post-review P1 —
swap_vfo_absafety + rigctld split rollback. (#746) - rig loader parses
transceiver_countfrom[radio]section. (#745) - DualVfoDisplay — dedicated activate button resolves WCAG 4.1.2. (#744)
- command-bus — tighten focus→mode handoff race. (#720)
Docs¶
- Dual-RX / transceiver / receiver / VFO model primer in
docs/internals. (#724) - IC-7610 cmd29 parity reconciled with wfview's
IC-7610.rig. (#725) - Opus DSP/tap gate behavior + one-shot warning documented. (#762)
Chores¶
chore: mypy cleanup— zero errors on default install;numpy/sounddevice/opusliboptional-dep imports now ignored via[[tool.mypy.overrides]].- Delete speculative
AudioBufferPool+ PCM-8 mapping. (#765, #768)
0.16.4 — 2026-04-16¶
Fixed¶
- Web UI TX audio: transcoder now uses the radio's negotiated sample rate (was hard-coded to 48 kHz, silently dropped TX on radios negotiated at 24 kHz) (#691)
Changed¶
- Test suite performance: backend 87.6s → 64s (−27%), frontend 10.6s → 5.8s (−45%); CI per matrix job ~4m30s → ~2m00s (−55%) (#706, #707)
0.16.3 — 2026-04-16¶
Fixed¶
- Web UI: resolve 33 issues across controls, sync, errors, layout, a11y, and performance — wiring layer (DW/VFO targeting), control panels (freq keyboard, ATU/ATT/APF), canvas perf (rAF idle loop, DX cap, gradient cache), error notifications (Toast mounted in v2 layouts), connection state (WS reconnect, scope/audio indicators), waterfall resize preservation (#693, #694–#702)
- DSP pipeline: add sample rate validation and auto-resampling for RNNoise (#692)
- Audio broadcaster: resolve subscriber leak and pong-timeout loop (#687, #690)
- Audio WebSocket: fix crash loop on PTT (#684, #688)
- CLI: hard errors for invalid inputs, silent ignores, startup ordering (#689)
- CLI: hard errors for explicitly requested features with validation (#686)
0.16.2 — 2026-04-15¶
Added¶
- Companion tuning step sync — tuning step is now synced to the RC-28
companion dispatcher via
PUT /api/local/v1/rc28/tuning-step; incomingcompanion_stateWS messages update the step in real time - WsChannel.reconnect() — reconnects a WebSocket channel using its last-known URL, enabling full lifecycle restore after disconnect
Fixed¶
- Connect/Disconnect lifecycle — the web UI button now controls the entire frontend connection: all WebSocket channels (control, scope, audio), HTTP polling, and MediaSession are torn down on Disconnect and restored on Connect; the server↔radio connection is never affected
- Scope/audio channels survive reconnect —
reconnectAll()reopens all named WS channels (scope, audio-scope) after a disconnect+connect cycle; previously they stayed dead until page reload - StatusBar state tracking — connect button now tracks
controlState(WS+HTTP) instead ofradioState(server↔radio), so the UI updates immediately on disconnect/connect - Transport warning noise — suppressed misleading
_packet_pumpwarning and reduced UDP error log verbosity - Companion auto-step preservation —
setTuningStepFromCompanion()preserves the auto-step preference when syncing from companion
0.16.1 — 2026-04-14¶
Fixed¶
- LAN discovery crash —
OSError: [Errno 65] No route to hostwhen network is unavailable no longer produces a raw traceback; CLI prints a clear message and suggests using--hostexplicitly - CI strict mypy — resolved
no-any-returninradio_poller.pyformypy --strictboundary check - Dynamic CI badges — tests, version, and mypy badges in README now auto-update from CI via gist-backed shields.io endpoints
0.16.0 — 2026-04-14¶
Added¶
- DSP Pipeline (Epic #682) — pluggable audio processing framework:
DSPNodeProtocol,DSPPipelineengine,PassthroughNode,GainNodeNRScipyNode— spectral subtraction noise reduction (scipy FFT)TapRegistry— multi-consumer PCM analysis bus- Inter-node resampling utility;
[dsp]optional dependency group - CW Auto Tuner (#675, #677, #678) — FFT peak detection engine (
CwAutoTuner), backendcw_auto_tunecommand, restored AUTO TUNE button in Web UI - AudioAnalyzer (#679) — realtime SNR estimation from PCM stream
- UDP Discovery Responder — companion apps can broadcast
RIGPLANE_DISCOVERon UDP 8470 and receive server URL, version, and radio status via unicast;--no-discoveryCLI flag to opt out - Unified frontend architecture (Epics #647–#653, #662–#665) —
FrontendRuntimesingleton, skin registry, runtime adapters, self-wired panels (AGC, Mode, Antenna, RfFrontEnd, RIT/XIT, Scan, CW, DSP, TX, Filter, BandSelector), eslint import boundary guardrails, LCD and mobile layout migration to unified runtime path - SUB receiver polling (#562, #563) — TOML commands, receiver routing, AF/RF/squelch level polling in slow loop
- TX meters (#559) — ALC, Power, COMP, SWR polling during transmit
- Scope backpressure (#533) — adaptive poller gap, scope backlog shedding hook,
queue_pressuremetric onIcomTransport - Initial state fetch (#532) —
_fetch_initial_state()on connect and reconnect, readiness-gated state snapshot in WebServer - Cross-sidebar drag (#566–#568) — move panels between left/right sidebars, localStorage persistence, dynamic panel rendering
- Yaesu FTX-1 enhancements (#551) — IF bulk query, clarifier clear, APF, CW spot, break-in delay, power switch (PS), data mode methods
- IC-7300 improvements (#545, #546, #564) — segmented BCD filter width encoding, scope marker TOML entry, cleanup of NOT_IMPLEMENTED comments
- Meter calibration (#556) — power/SWR/ALC tables in
ic7610.toml, scope REF range constraints,meter_redlinesin RadioProfile, generic calibration accessors - SystemController (#665) — centralized HTTP system actions
- Skin abstraction (#326) —
ProfessionalSkin(Phase 1) - Frontend test coverage (#555) — component-level tests for LCD, Mobile, Spectrum, BandPlan, DspPanel, CwPanel, SpectrumToolbar, DxOverlay, EiBi, state-adapter, ws-client, radio store, audio subsystem
- FTX-1 polling tests (#551) — integration test suite for Yaesu CAT poller
Changed¶
- Single version source —
__version__now reads frompyproject.tomlviaimportlib.metadatainstead of being hardcoded in__init__.py - Frontend panel architecture — extracted DspPanel + CwPanel logic to dedicated panel-logic modules (#594); extracted SpectrumToolbar, BandPlanOverlay, MobileRadioLayout, SpectrumPanel inline logic to separate files (#590–#593)
- LCD layout (#636) — adapts to reduced viewport height
Fixed¶
- CW auto tune (#671) — reverted incorrect
cw_sync_tune, removed broken AUTO TUNE button before reimplementing correctly - Shutdown reliability (#634) —
os._exit()for orphaned threads, manual loop with executor timeout, PortAudio stream close before task cancel, shutdown step timeouts - Audio stability — drop frames while
AudioContextsuspended, resume once instart()instead of per-frame - Yaesu serial — report
disconnectedstatus correctly, show serial port in startup banner, graceful poller disconnect handling - Connection readiness (#602) — expose readiness fields from backend state
- Frontend null guards (#603–#605) — null receiver state, null numeric fields coerced to defaults, encoder revision for initial state snapshot
- Disconnect cleanup (#600) — clear stale state, reset delta and radio store
- Code review fixes (#670, #576) — 5 findings from session audit, layering and model guards, reconnect timing
- Drag-reorder — unregister instances from registry on component destroy
- All mypy errors resolved —
ControlPhaseHostprotocol gap,YaesuCatRadiomissingget_data_mode/set_data_mode, scipy stubs,no-any-returnfixes - All ruff errors resolved — unused imports in test_cli.py
Documentation¶
- Refreshed Web UI guide for v2 runtime and skin workflows (#681)
0.15.1 — 2026-04-10¶
Changed¶
- Web UI v2 is now the default layout. New visitors and fresh installs see the
redesigned RadioLayout v2 interface. Users who previously selected v1 keep their
choice (persisted in localStorage). Switch manually with
?ui=v1or?ui=v2.
0.15.0 — 2026-04-10¶
Added¶
- Zero-config CLI startup (Epic #526) —
icom-lan webauto-discovers radio via LAN broadcast,--preset hamradio|digimode|serial|headlessfor common scenarios, smart startup banner with loopback device hints (#527, #528, #529, #530). - Drag-and-drop panel reorder — drag handles on right sidebar panels (#557).
- Complete CI-V command coverage (Epic #535) — scope settings popover (#538), missing polling entries (#539), VOX/CW/DSP panels (#540), TX band edge support (#541), memory channel manager + scan modes (#542, #543), TX meters + scope toolbar controls (#536, #537).
- Center dead zone for RF/SQL dual slider — prevents accidental threshold jumps.
- Poller deadlock regression tests (#554) — state consistency + deadlock detection tests.
- Yaesu CAT backend and CLI factory routing —
--backend yaesu-cat, capability-based polling, rigctl routing strategy, Web ControlHandler support, meters/advanced-control conformance, and follow-up code review fixes for issues #427-#445. - Universal radio profile system — declarative
OperatingProfile/apply_profile()/PRESETS, packet/data profile helpers for IC-705, and additional sync control methods. - TLS/HTTPS for Web UI — HTTPS listener support with automatic self-signed certificates (#205).
- Audio FFT UI work — full-color
AudioSpectrumPanel, standard-layout integration, audio-scope WebSocket channel, variable FFT bandwidth handling, and audio spectrum rendering fixes. - Expanded Web/rigctld command coverage — raw CI-V passthrough, levels/functions support, data mode inputs/levels, VOX, tone/TSQL, CW text/stop, band/split, system/config, selected/unselected freq+mode, memory API support, and scope toolbar controls.
- Capability/tag cleanup — extracted
capabilities.py, addedsystem_settingstag,supports_command()on the Radio protocol, and removed remaining protocol abstraction gaps. - Issue #448 UI/antenna work — v2 antenna panel, capability/state tracking, corrected IC-7610 TX ANT vs RX ANT semantics, and startup readiness checks split between connect-time validation and server-side guards.
Changed¶
- Connection readiness contract —
radio.connect()now owns bounded wait-for-ready and fails if the radio never becomes usable; Web UI and rigctld startup now perform instant guards only. - Protocol/capability routing — replaced several
isinstance(AdvancedControlCapable)checks with capability tags and centralized capability constants. - Spectrum/waterfall interaction architecture — clean separation of gesture, drag, and tune layers.
- Frontend/test hygiene — resolved Svelte/type issues, fixed frontend redesign regressions, refreshed API docs and badges, and updated test fixtures for stricter protocol mocks.
Fixed¶
- Meter calibration (#536) — corrected S-meter, RF power, SWR, ALC calibration tables per IC-7610 CI-V Reference p.4; dimmed irrelevant meter rows.
- Scope REF BCD encode/decode (#553) — fixed to match IC-7610 CI-V Reference p.15.
- CENTER Type polling (#552) — fixed root cause: poller was overwriting scope CENTER Type to Filter on every poll cycle; restored CTR mode indicator at center position.
- Tuning indicator (#552) — proportional positioning + scope REF display.
- Deadlock: EnableScope — EnableScope await blocked all commands during initial fetch.
- Click-to-tune — only on waterfall, not spectrum area; via pointerup instead of click event.
- Reliable shutdown — 3-tier signal handling, reuse_address for TIME_WAIT, force exit on second Ctrl-C, proper audio relay shutdown order.
- AF scope — bandwidth tracks actual filter width, crash fix when center_freq is 0.
- Power-off state not detected on server restart.
- Startup fail-fast — added pre-flight port check (#422), fail-fast on
civ_port=0(#424), and eliminated half-working Web/rigctld startups when the radio transport is not actually ready. - IC-705 Wi-Fi binding — hardened routed local bind handling and validated LAN support.
- Audio/runtime stability — fixed broadcaster restart behavior, audio handler lifecycle, control transport queue overflow after long runs, and Python 3.13 flaky tests (#398).
- Scope/UI correctness — fixed scope dispatch capability checks, scope polling/state updates, step-control width, BCD span payloads, speed arrow direction, PTT TX wiring, and optimistic state sync for antenna/scope controls.
- Type-check/lint cleanup — resolved all 188 ruff lint errors and 499 mypy type errors: file-level noqa for re-export modules, mixin TYPE_CHECKING base pattern, per-module mypy overrides for duck-typing consumers, and ControlPhaseHost protocol expansion.
Documentation¶
- Added/updated Radio Profiles guide, web/rigctld API references, and test badges/documentation sync.
0.14.2 — 2026-03-27¶
Changed¶
- Git cleanup — removed 83 tracked files (-33k lines): backups, internal dev docs (plans/sprints/reviews/audits), scripts, mockups, references, credentials in run-dev.sh
- Documentation refresh — index.md, radios.md, README.md updated for multi-vendor reality; FTX-1 moved from "planned" to "tested"; mkdocs nav expanded with 12 missing pages; 5 broken links fixed; mkdocs build --strict passes clean
- CI fixed — removed parity matrix tests (depended on deleted files); marked 2 flaky reconnect tests as xfail (#398); CI green on Python 3.11/3.12/3.13
0.14.1 — 2026-03-27¶
Fixed¶
- FTX-1 LCD layout, band indicator, DSP/TX panel redesign, CAT fixes (feature/ftx1-filter-width)
- Removed FTX-1 monitor tests (ML command not supported via CAT)
- Fixed tuner routing through command queue for Yaesu radios
0.14.0 — 2026-03-27¶
Added¶
- Multi-vendor rig profile support — TOML schema extended for non-Icom radios:
rigs/ftx1.toml— Yaesu FTX-1 (Yaesu CAT, 17 modes, dual RX, meter calibration)rigs/x6100.toml— Xiegu X6100 (CI-V 0x70, IC-705 compatible, QRP 8W)rigs/tx500.toml— Lab599 TX-500 (Kenwood CAT, minimal command set, QRP 10W)[protocol]section —type = "civ" | "kenwood_cat" | "yaesu_cat"(default:"civ")[controls]section — UI control styles:toggle,stepped,selector,toggle_and_level,level_is_toggle[meters]section — Non-linear calibration tables for S-meter and TX meters withredline_rawthreshold[[rules]]section — Declarative constraint rules:mutex,disables,requires,value_limit- Extended VFO schemes — added
"ab_shared"(FTX-1) and"single"(simple QRP) [commands]now optional — non-CI-V radios may have empty command mapsciv_addrnow optional — defaults to 0 for Kenwood/Yaesu CAT radiosRadioProfileandRigConfigextended withprotocol_type,controls,meter_calibrations,rules- Yaesu CAT backend (Epic #107) — full implementation for Yaesu FTX-1/FT-710/FT-991A:
- YaesuCatTransport (async line protocol,
;terminated, echo handling) - CAT template formatter + response parser (compile-once)
- Polling scheduler for smooth meters (fast meters, slower state)
- Full Web UI integration (command dispatch, levels, audio)
- Audio FFT Scope (Epic #383) — IF waterfall from USB/LAN audio stream:
- AudioFftScope class (real-time FFT processor, consumes PCM, produces ScopeFrame)
- Backend-agnostic (works with any AudioCapable radio)
- Reuses existing scope protocol (SpectrumPanel + WaterfallCanvas)
- Amber LCD display (#389, #386) — retro KX3-style UI for radios without hardware spectrum:
- 7-segment font, segmented bargraph, status indicators
- Embedded Audio FFT strip (trapezoid filter visualization)
- Grouped indicators (ATT/PRE/ATU/Contour/PROC/VOX)
- Adaptive lerp (smooth animated filter width transitions)
- Profile-driven command dispatch (Epic #390-#396) — auto-wire all TOML commands to Web UI:
- Frontend capability guards for multi-radio (hide unsupported controls)
- Optimistic UI updates for NB/NR levels
- Auto-reconnect on persistent serial errors
- Serial discovery (Epic #222) —
icom-lan discoverscans LAN + USB serial: - Multi-protocol probing (CI-V auto baud, Yaesu CAT, Kenwood CAT)
- Deduplication (same radio found via LAN and serial)
- 42 new tests in
test_rig_multi_vendor.py+ 636 new tests total (3934 passed, 0 regressions)
0.12.0 — 2026-03-15¶
Added¶
- Data-driven rig profiles (Epic #251) — radio configuration moved from hardcoded Python
to TOML files in
rigs/: rigs/ic7610.toml— IC-7610 reference profile (full feature set, dual receiver)rigs/ic7300.toml— IC-7300 profile (single receiver, VFO A/B, no DIGI-SEL/IP+)rigs/_schema.md— TOML schema specificationrig_loader.py—load_rig(),discover_rigs(),RigConfig,RigLoadErrorcommand_map.py—CommandMap(immutable CI-V wire byte lookup)- IC-7300 support — tested via USB serial backend; rig profile defines all 200+ supported commands, VFO A/B scheme, and IC-7300-specific wire byte overrides
cmd_mapparameter on all 223 command functions — every builder function incommands.pynow acceptscmd_map: CommandMap | None = None; when provided, wire bytes come from the TOML profile instead of hardcoded IC-7610 defaultsRadioProfileadditions —vfo_scheme("ab"|"main_sub"),has_lanfields- Web UI capability guards — UI controls for DIGI-SEL, IP+, and dual-receiver features are automatically hidden when the active profile doesn't support them
- Dynamic VFO labels — Web UI shows "MAIN" / "SUB" for IC-7610 (main_sub scheme) and "VFO A" / "VFO B" for IC-7300 (ab scheme)
/api/v1/infoenriched —capabilitiesobject now includesvfoScheme,hasLan,maxReceivers,modes,filtersfrom the active rig profile/api/v1/capabilitiesadditions —receivers,vfoSchemefields/api/v1/stateadapts — omitssubreceiver state for single-receiver rigs
Changed¶
- +3497 lines, 236 new tests across
test_rig_loader.py,test_command_map.py,test_rig_ic7610.py,test_rig_ic7300.py,test_commands_cmd_map.py - Hardcoded IC-7610 wire bytes remain as defaults when
cmd_map=None— fully backward-compatible
0.11.0 — 2026-03-12¶
Added¶
- Abstract Radio Protocol (
radio_protocol.py) — vendor-neutral interface withRadio,AudioCapable,ScopeCapable,DualReceiverCapableprotocols - Epic #140 complete — 100% CI-V command coverage (134/134 IC-7610 commands implemented)
- Epic #215 complete — Post-audit cleanup: mypy 197→0 errors, dead code removed (-616 lines),
__all__API surface defined IcomRadio.model,.capabilities,.radio_statepropertiesset_state_change_callback(),set_reconnect_callback()public methodscontrol_connectedproperty for transport health statusget_mode()now returns Protocol-compatibletuple[str, int | None]- Graceful shutdown: SIGTERM handler ensures clean radio disconnect on kill
_force_cleanup_civ()for unconditional CI-V transport teardown- Retry mechanism for
civ_port=0(radio session not ready): 3×10s retries - Connection indicators in Web UI update from
/api/v1/statepoll (200ms) /api/v1/capabilitiesendpoint usesradio.capabilities
Fixed¶
- Sequence counter overflow —
_civ_send_seq/_audio_send_seqnow wrap at uint16 (was unbounded, crashed after ~1.5h) - Broken pipe recovery — watchdog falls back to full reconnect when soft_reconnect fails
- CI-V indicator accuracy —
connectedproperty checks actual transport health, not just state enum - UDP error logging rate-limited (first 3, then every 100th)
0x16added to_COMMANDS_WITH_SUB(NB/NR/DIGI-SEL sub-command parsing)server.stop()uses fulldisconnect()instead ofsoft_disconnect()for complete session cleanup
Changed¶
- All Web UI/rigctld consumers now use
RadioProtocol type hints instead ofIcomRadio isinstance(radio, AudioCapable)guards instead ofhasattr- Test coverage: 85% → 95% (3173 tests, +1434 from v0.10.0)
- Type safety — 0 mypy errors, full protocol-based typing for Radio/AudioCapable/ScopeCapable/DualReceiverCapable
0.8.0 — 2026-02-28¶
Added¶
- Web UI v1 — full-featured browser interface at
icom-lan web:- Real-time spectrum and waterfall display (Canvas2D, click-to-tune)
- Radio controls: VFO A/B, mode, filter, power, ATT, preamp, PTT
- Band selector buttons (160m–6m with FT8 defaults)
- Frequency entry, tuning step selector with snap, arrow keys, scroll wheel
- Frequency marker and filter passband overlay on spectrum/waterfall
- Eight real-time meter bars (S-meter, Power, SWR, ALC, COMP, Id, Vd, TEMP)
- RX audio playback and TX audio capture in the browser (WebSocket binary)
- Responsive layout, light/dark theme toggle, keyboard shortcuts
- WebSocket pub/sub for scope, meters, audio, and control channels
- Connect/Disconnect button in Web UI — toggle radio connection without restarting server
- Soft reconnect — disconnect closes only CI-V/audio, keeps control transport alive. Reconnect re-opens CI-V instantly (~1s) without discovery or re-authentication. Audio auto-restarts after reconnect.
- Skip discovery on reconnect —
transport.reconnect()reuses cachedremote_id, eliminating the 30-60s discovery timeout on IC-7610. - Connection state machine —
RadioConnectionStateenum formalizing connect lifecycle (#61) - State cache with TTL — cached GET fallback values with configurable TTL
(10s freq/mode, 30s power) via
cache_ttl_sparameter (#63) - API docs from docstrings — mkdocstrings-generated API reference (#65)
- Scope assembly timeout — 5s default prevents memory leak on incomplete frames (#62)
Changed¶
- CI-V commander: fire-and-forget for SET commands — SET operations no longer wait for ACK from the radio, matching wfview behavior. GET commands retain 2s timeout with cache fallback on timeout. NAK silently logged at debug level. (#56)
radio.pyrefactored into focused modules — split from 2395 to 1549 lines (#60):_control_phase.py(452 lines) — authentication, conninfo, connection setup_civ_rx.py(418 lines) — CI-V frame dispatch and RX pump_audio_recovery.py(132 lines) — audio stream snapshot/resume_connection_state.py— FSM enum for connection lifecycle- Public API surface unchanged (mixin pattern)
- Optimistic port connection — uses default ports (control+1, control+2) immediately
instead of blocking on status packet. Status read in background with 2s timeout;
if radio reports different ports, uses those instead. Eliminates up to 24s connection
delay when radio returns
civ_port=0after rapid reconnects. - CLI
--portrenamed to--control-portto avoid confusion (#54)
Fixed¶
- CI-V GET timeout during scope streaming (release blocker, #66) — RX pump now drains ALL pending packets from the transport queue each iteration instead of processing one at a time. Scope flood (~225 pkt/sec) no longer starves ACK/response packets behind hundreds of scope frames.
- Conninfo local ports — send reserved ephemeral UDP ports in conninfo packet
(wfview-style
socket.bind(("", 0))). Root cause of CI-V instability: radio didn't know where to send responses when local ports were 0. - Safari iOS audio — AudioContext resume after background via
visibilitychangelistener; increased jitter buffer pre-roll from 50ms to 200ms for VPN use. - Flaky
test_hello_on_connect— race condition fix, pytest-asyncio dependency (#64) - Duplicate WebSocket connections on page load/reconnect (#50)
- Scope enable — single entry point via
server.ensure_scope_enabled()(#51) - PTT button — toggle mode for click vs hold (#57)
- Filter sync after band change (#58)
- PTT wait_response restored after fire-and-forget refactor (#59)
- Watchdog false disconnect — use packet counter instead of qsize
- Tuning flood — throttle tuning commands to prevent CI-V timeout cascade
- Frequency clamping — valid range 30 kHz – 60 MHz
Documentation¶
- Web UI user guide (
docs/guide/web-ui.md) - RFC for Web UI v1 protocol spec and architecture
- Updated architecture docs with mixin pattern and new module structure
- Updated test count: 1202 tests (was 1040)
- Roadmap Phase 8: Virtual Audio Bridge
0.7.0 — 2026-02-26¶
Added¶
- Internal PCM<->Opus transcoder foundation for upcoming high-level PCM audio APIs.
- Typed audio exceptions:
AudioCodecBackendError,AudioFormatError,AudioTranscodeError. - High-level async PCM audio APIs:
start_audio_rx_pcm()/stop_audio_rx_pcm(),start_audio_tx_pcm()/push_audio_tx_pcm()/stop_audio_tx_pcm(). - Audio capability introspection:
audio_capabilities(),AudioCapabilities. - CLI:
icom-lan audio caps,audio rx,audio tx,audio loopback. - Runtime audio stats:
get_audio_stats()with packet loss, jitter, latency metrics. - Rigctld WSJT-X compatibility:
icom-lan serve --wsjtx-compat. - Golden protocol test suite: 45 parametrized fixtures.
- TCP server wire integration tests.
Changed¶
- Audio API names explicit with
_opussuffix. - Rigctld mode mapping includes
PKTRTTY.
Fixed¶
- First-TX latency spikes in WSJT-X workflows.
- Abandoned rigctld requests no longer execute in background.
Deprecated¶
- Ambiguous audio aliases (two-minor-release deprecation window).
0.6.0 — 2026-02-25¶
Added¶
- Scope/waterfall API with
ScopeFrame,ScopeAssembler, callbacks. - Scope rendering:
render_spectrum(),render_waterfall(),render_scope_image(). - CLI
icom-lan scopewith themes, capture, JSON output. - Mock radio server for integration testing (30 new tests).
0.5.1 — 2026-02-25¶
Fixed¶
_ensure_audio_transport()raisesConnectionErrorwhen audio port is 0.- Ruff lint warnings resolved.
0.5.0 — 2026-02-25¶
Added¶
- Command29 support for dual-receiver radios (IC-7610).
- Attenuator and preamp CLI commands with Command29 framing.
0.4.0 — 2026-02-25¶
Changed¶
- Faster non-audio connect path (lazy audio port init).
0.3.2 — 2026-02-25¶
Added¶
- Commander layer with priority queue, pacing, dedupe, transactions.
- New APIs:
get_mode_info(),get_filter(),set_filter(),snapshot_state(). - Extended integration test coverage.
0.3.0 — 2026-02-25¶
Added¶
- Audio streaming (full-duplex, JitterBuffer, codec enum).
- Synchronous API (
icom_lan.sync). - Radio model presets (IC-7610, IC-7300, IC-705, IC-9700, IC-R8600, IC-7851).
- Token renewal and auto-reconnect with watchdog.
0.2.0 — 2026-02-25¶
Added¶
- CLI tool with full command set.
- VFO control, RF controls, CW keying, power control, network discovery.
0.1.0 — 2026-02-24¶
Added¶
- Transport layer, authentication, CI-V commands, meters, PTT, keep-alive.
- Clean-room Icom LAN UDP protocol implementation.