Extract CatHub into standalone project#2
Merged
Conversation
Captures the proposed Rust daemon that owns the radio serial port and fans it out to multiple concurrent client apps (HDSDR via OmniRig, N1MM, and the QsoRipper engine via the Hamlib net protocol). Splits the design into a RadioBackend trait, a ClientDialect trait, and a universal state model so adding a new transceiver or a new client app is an additive change. Documents v1 scope (Kenwood TS-590 backend with TS-590 and TS-2000 dialects) and the v2 roadmap (Icom CI-V, Yaesu CAT, FlexRadio SmartSDR). Co-authored-by: Mike Treit (from Dev Box) <mtreit@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…okering (#468) Adds Kenwood ARCP-590 and WSJT-X as first-class clients and reworks the design to broker many apps against one TS-590 at high performance: full read+write Hamlib net face, central AI2 auto-information ownership with per-face virtualization and push fan-out, passthrough for unmodeled native commands, per-face permissions, per-face-FIFO priority scheduling, PTT lease arbitration, dual read-only/write Hamlib listeners, expanded split/VFO state, and a QsoRipper architecture-alignment section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#470) Generalize the cathub design from TS-590-specific to rig-agnostic behind the RadioBackend trait and BackendCapabilities, and reason explicitly about which backend strategy is best rather than defaulting to wrapping rigctld. - Generalize the AI2 fan-out into a rig-neutral event/push abstraction (events module, RadioEventSource), with poll-diff synthesis for rigs without native push and back-off gated only on NativePush coverage. - Reframe the anti-oscillation guarantee as a wire-level no-VFO-retargeting invariant rather than a blanket no-Hamlib rule. - Add four backend strategies (native, out-of-process rigctld bridge, descriptor, in-process FFI) and an explicit best-backend rationale: native is the recommended default for the TS-590 (full fidelity, native passthrough, native push, lowest latency, invariant by construction); rigctld is the trusted breadth bridge for modeled control only; descriptor is the scaling endgame; FFI is a last resort. - Treat the rigctld bridge as uncertified by default and modeled-control only, certified per rigctld version+model+config via a radio-side wire soak across the client compatibility matrix; require a daemon-private, sole-client rigctld. - Finish event-naming cleanup (native_push_covered, [events] config, native-push metrics) and add OmniRig RigIni/OmniRig2 references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduces the qsoripper-cathub lib+bin crate implementing phase 1 of the multi-client CAT hub design (docs/design/cathub-multi-client-cat-hub.md). A single tokio task owns the TS-590 serial port; a universal RigState is populated by a baseline poller and served to an N1MM-facing native Kenwood dialect over one serial face. Includes a LoopbackBackend for hardware-free testing, a TOML config loader with --dry-run, and thiserror typed errors. Uses serial2-tokio (BSD-2-Clause) for the serial transport to satisfy the permissive-only cargo deny license policy. Startup disables radio auto-information (AI0), primes the cache with an initial poll, and bounds the shutdown PTT release with a timeout. Co-authored-by: Mike Treit (from Dev Box) <mtreit@ntdev.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#471)" (#472) This reverts commit 7b492ac.
* test: verify review-required workflow
* WIP: cathub mixed state recovery snapshot (skeleton tracked + new untracked files)
* cathub: wire lib.rs/main.rs daemon entry, fix trait default + tests
* cathub: clippy/fmt clean (writeln describe, map_outcome by ref, doc fixes)
* cathub: add run()/open_transport coverage tests (lib 73%, total 89%)
* cathub: station config, operator scripts, runbook, engine-spec front-door note
* cathub: unkey radio on PTT ceiling + shutdown (close §8.5 gap); doc v1 limits
* cathub: prime universal state with an awaited startup poll
Adopt the one genuinely useful idea from the alternate phase-1 skeleton (PR #475):
run a single time-bounded poll before any face begins serving so the first client
read (e.g. HDSDR/OmniRig connecting at startup) sees real radio state instead of
defaults. Best-effort and bounded so a slow or absent radio cannot block startup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: read unified config.toml [cat_hub] section; fix radio port to COM4
Resolve the daemon config from the shared per-user config.toml (QSORIPPER_CONFIG_PATH /
%APPDATA%/qsoripper or XDG/HOME), parsing the [cat_hub] subtree when present and falling
back to a standalone layout for back-compat. Correct the sample radio port to COM4 (the
CP210x TS-590).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* engine(rust): merge-preserving setup config writer
Splice engine-owned tables into the existing config.toml via toml_edit instead of a full
rewrite, so unknown top-level tables ([cat_hub], [launcher]) survive an engine setup save.
Engine-owned keys are removed-then-reinserted, so cleared tables are dropped while shared
sections are preserved. Adds tests for unknown-table survival and stale-table removal.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* engine(dotnet): merge-preserving setup config writer
Splice engine-owned tables into the existing config.toml via a Tomlyn model merge instead
of a full rewrite, so unknown top-level tables ([cat_hub], [launcher]) survive a .NET engine
setup save. Mirrors the Rust engine's owned-key removal+reinsert. Adds a preservation test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* scripts: build and launch cathub from the unified workflow
build.ps1 now publishes qsoripper-cathub alongside qsoripper-server. Start-CatHub.ps1 prefers
the published binary for instant startup and auto-resolves the unified config.toml ([cat_hub])
when present, falling back to the standalone sample. launcher.ps1 gains -WithCatHub to bring
the radio daemon up first so engines/UIs can connect to its rigctld face.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: document unified config.toml and cold-start cathub workflow
Describe the shared [cat_hub] table in config.toml, merge-preserving setup saves, and the
build.ps1 + launcher.ps1 -WithCatHub cold-start flow in the engine spec and operator runbook.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix -Debug param collision in Start-CatHub.ps1
PowerShell's [CmdletBinding()] reserves -Debug as a common parameter, so
declaring a custom [switch]$Debug made the script fail to load. Rename the
switch to -DebugBuild and update launcher.ps1's -WithCatHub passthrough.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make cathub hub-vs-client serial port mapping discoverable
Operators were confused that an application's COM dropdown does not list the
daemon-side port (e.g. ARCP-590 sees COM31 but not COM30). Clarify in three
places that the hub OWNS the 'transport' port and the app must connect to the
paired com0com port:
- cathub --dry-run / startup now prints a 'Client connection guide' mapping
each face to where its application should connect (paired serial port) and
each Hamlib NET endpoint to its TCP bind.
- The serial-face startup log line notes the port is hub-owned.
- config/cathub.toml and the setup runbook gain an explicit pair table plus a
troubleshooting note that the daemon-side port intentionally does not appear
in app COM dropdowns.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Document that cathub radio baud must match the rig's CAT port speed
The TS-590's PC/CAT speed (menu 62) is 57600 here, not 115200. cathub opens
COM4 at [radio].baud, so a mismatch means the daemon opens the port but never
talks to the radio. Set the sample to 57600 and clarify in the runbook that
[radio].baud must match the rig while [[face]].baud is nominal (com0com ignores
it). Adds a troubleshooting bullet for the silent timeout/stale symptom.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: assert RTS/DTR when opening serial radio port
The Kenwood TS-590 gates its CAT transmit on the RTS line and sends no
replies unless RTS is asserted. The hub opened the serial port without
touching the modem-control lines, so every poll timed out and the daemon
served default state (freq=0) to all clients. Assert RTS and DTR after
opening the port, matching the line state OmniRig/Hamlib clients use.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: fix Kenwood AI; read handshake and add transport/face tracing
AI; is a read query, not a write. The Kenwood dialects treated a bare
AI; as a write with an empty payload, calling set_ai(false) and returning
an empty reply. Clients like ARCP-590 that poll AI; as a keepalive and
rely entirely on auto-info push never received a valid AI<n>; answer, so
they never enabled push and froze (showing BUSY, not tracking the dial).
Branch AI handling on read vs write: a read now reports the face's current
auto-info state via a shared ai_frame() helper without mutating it; a write
still toggles per-face virtualized auto-info. Add regression tests to both
ts590 and ts2000 dialects.
Also add high-signal tracing to the radio transport (tx/rx frames, reply
timeout warnings, dropped unsolicited frames) and per-face request/reply/
notify tracing, and document the auto-info behavior and CATHUB_LOG tracing
in the operator runbook.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cathub): relay unmodeled radio frames to native pass-through faces
Native clients like ARCP-590 that enable auto-information run client-side
feature state machines (e.g. the NB off/NB1/NB2/off cycle) that advance by
observing the radio's echo on the CAT stream. cathub forwarded the writes but
dropped the radio's unmodeled echoes, so those clients stalled on first press.
Add a RadioEvent enum so modeled changes and unmodeled native frames travel on
the same ordered broadcast. StateHandle::record_raw relays verbatim frames
without touching the snapshot or native-push coverage. Faces forward them via a
new ClientDialect::format_passthrough hook; the Kenwood TS-590 dialect relays
them only when the face has auto-information enabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: HDSDR read+write face and rig-control freshness guidance
Update the hdsdr-omnirig sample face to perms=[read, write] so OmniRig
click-to-tune sets the radio (FA/FB/MD) while FR/FT VFO-target writes stay
rejected (no A/B oscillation). Document keeping the engine's rig_control
stale_threshold_ms low (~200) when reading through cathub so the GUI/TUI
frequency display tracks knob turns promptly instead of lagging by the
stale window.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Accept Hamlib decimal set_freq format in cathub hamlib_net
WSJT-X and other Hamlib clients format set_freq as a %f double (e.g.
'F 14040005.000000'). The handler only parsed u64, so every real
set_freq returned RPRT -1 ('Invalid parameter while setting frequency').
Add parse_freq_hz to accept integer or decimal Hz, plus per-frame trace
logging in serve_conn for live rigctl debugging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Key cathub PTT on any non-zero Hamlib set_ptt value
WSJT-X transmits in Data/Pkt mode by sending 'T 3' (RIG_PTT_ON_DATA),
and other clients may send 'T 2' (on mic). The handler only treated
'1' as keyed, so 'T 3' was parsed as unkey: the hub returned RPRT 0 but
left the radio in RX, so transmit silently failed. Treat 1/2/3 as keyed
and only 0 (or a missing arg) as unkey.
Also document the WSJT-X mode guidance (use Mode=None or USB; PKTUSB data
sub-modes are not mapped to the TS-590 DATA mode) in cathub-setup.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: map Hamlib PTT source to TS-590 TX/TX0/TX1 (fix WSJT-X data beep)
WSJT-X keys digital modes with RIG_PTT_ON_DATA (T 3), but the hub sent a
bare TX; for every keyed value. On a TS-590 a bare TX; transmits from the
mic path and emits a data-confirmation beep (Morse 'U') on each key/unkey.
Add a PttSource to SetPtt and translate it faithfully, mirroring Hamlib's
kenwood backend: T 1 -> TX;, T 2 (mic) -> TX0;, T 3 (data) -> TX1;,
T 0 -> RX;. TX1; routes the DATA/USB audio for digital modes and stops the
beep. Document the mapping in the WSJT-X setup notes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(cathub): correct TS-590 PC-control beep cause (radio menu, not PTT form)
The Morse 'U' beep WSJT-X users hear on the TS-590 is the radio's
'beep output for PC control commands' menu setting, not a CAT command
form the hub can change. Reframe the WSJT-X PTT note (TX1 is the
faithful Hamlib data-PTT mapping for audio routing) and add a
'Required radio setting' section telling operators to turn the
PC-control beep OFF on the front panel.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: suppress redundant CAT sets to stop TS-590 PC-control beep
The hub forwarded every modeled write to the radio unconditionally, so clients
like WSJT-X that re-assert mode/frequency on each poll made the TS-590 emit its
PC-control beep (a Morse U) on every no-op set. A native Hamlib driver never
beeps because it caches state and only sends a value when it changes.
Add Snapshot::is_redundant and gate apply_modeled on it so a modeled write
reaches the wire only when it would change the radio. Frequency, mode, split,
RIT and XIT are deduplicated against the current snapshot; PTT is never
suppressed so keying and unkeying always reach the radio.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: dedup TS-590 mode sets by wire digit to silence WSJT-X beep
WSJT-X asserts PKTUSB, which maps to Mode::Unknown and never compared equal to the polled-back Mode::Usb, so the hub kept forwarding MD2; to the radio. The TS-590 beeps on every mode set it receives (frequency sets are silent), producing a repeated U tone on FT8<->WSPR switches and polls.
Compare SetMode redundancy by the Kenwood wire digit instead of enum identity so Usb/Unknown (both MD2) are treated as no-ops, matching native Hamlib which never re-sends an unchanged mode. Genuine mode changes still send one MD and beep once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* cathub: map WSJT-X PKTUSB/PKTLSB to TS-590 DATA sub-mode
WSJT-X (and other Hamlib NET clients) drive FT8/WSPR with the PKTUSB/
PKTLSB mode tokens. The hub previously modeled only the native Kenwood
MD mode set and honored those tokens as plain USB/LSB, so the radio
never entered its DATA sub-mode and the mode read-back disagreed with
what the client requested.
The TS-590 models data as two independent wire facts: the base mode
(MD2=USB / MD1=LSB) plus a separate DATA flag (DA1/DA0). Readback must
poll both MD; and DA;; setting MD does not change DA. PKTUSB = MD2+DA1,
PKTLSB = MD1+DA1, plain = MDx+DA0.
Model the DATA flag as a separate StateChange/StateMutation field so the
stateless parse_event path composes correctly for both polling and
native push. The modeled write path issues two mutations (base mode then
DATA flag), each deduped independently, so FT8<->WSPR (both PKTUSB) is
suppressed after the first set and the radio does not chirp. get_mode
recomposes the PKT token; set_mode decomposes it into base + DATA flag.
Poll now includes DA; and the dump_state mode mask advertises the PKT
modes.
Verified live against a TS-590SG: PKTUSB/PKTLSB round-trip and light the
DATA indicator; plain USB clears it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(rig): lower default snapshot stale threshold to 200ms
The engine cached rig snapshots for up to the stale threshold before re-polling
the provider. The code default was 500ms but docs/.env/proto documented 5000ms,
and a live GUI/TUI polls rig state about every 500ms. Through the cathub front
door (which serves fresh rig state in under 50ms) this produced a multi-second
lag between a frequency/band change and the QsoRipper rig-status display
updating, while other apps tracked the radio near-instantly.
Lower DEFAULT_RIGCTLD_STALE_THRESHOLD_MS to 200ms (well under a typical poll
interval) and reconcile the documented defaults in .env.example,
engine-specification.md, and the runtime-config field metadata. Add a
regression test that primes the monitor cache, retunes the fake provider, and
asserts the change surfaces within one interactive poll window; it fails at the
old default and passes at 200ms.
Also harden the GUI rig poll: guard OnRigTimerTick against reentrancy and run
the DispatcherTimer at Default priority so the poll is not starved behind UI
rendering or band-change bursts.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(cathub): support Hamlib Extended Response Protocol for Log4OM
Log4OM-NG drives rig control through Hamlib's Extended Response Protocol
(ERP) rather than the plain rigctld protocol: it opens every session with
';V ?' (list supported VFOs) and polls with '+\get_vfo_info VFOA'. The
hub's hamlib_net face only understood the plain protocol, so it answered
the ERP handshake with RPRT -11 and Log4OM stayed offline.
Parse the leading ERP separator ('+' newline-joined; ';' '|' ',' joined on
one line by that char) and emit replies in the exact byte format real
rigctld produces: an echoed long command name, labeled data records, and a
trailing 'RPRT x'. The plain protocol path (WSJT-X, N1MM, the engine) is
unchanged. Verified byte-for-byte against Hamlib 4.7.0 rigctld and online
against a live TS-590.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the launcher.ps1 -WithCatHub switch with a first-class daemon entry in the launcher TUI. The CAT hub (qsoripper-cathub) now appears as the first row of the relabeled Services column. Selecting it starts the hub first, waits for its rigctld face on 127.0.0.1:4532, and only then launches the selected engines and UIs so everything connects through the hub. A hub failure aborts the rest of the launch with a clear message. Adds ComponentKind::Daemon, a daemons list in Selection (persisted to launcher.daemons), config resolution that prefers the unified config.toml when it has a [cat_hub] table and falls back to config/cathub.toml, and regression tests guarding that the daemon is never treated as an engine endpoint or binding target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Poll the TS-590 IF status frame so cathub records which VFO is currently receiving. This keeps rigctld, OmniRig, and HDSDR pointed at the displayed VFO instead of always reporting VFO A. Co-authored-by: Randy Treit <rtreit@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add CAT hub setup-wizard management to engine setup Operators can now read and replace the qsoripper-cathub daemon's [cat_hub] section through the engine SetupService instead of hand-editing config.toml. A new CatHubSettings proto family (radio/poll/ptt/events plus serial faces and hamlib_net endpoints) is wired into SaveSetup (field 11), SetupStatus (field 26), and a new SETUP_WIZARD_STEP_CAT_HUB wizard step. The contract is full-replacement: when cat_hub is supplied it is the complete desired section (radio with backend required, at least one endpoint required) and the engine rewrites [cat_hub]; when omitted the section is preserved verbatim, so routine saves never reserialize the daemon's configuration. The section is parsed leniently and separately for status display so a malformed [cat_hub] can never break engine load. Both engines implement identical validation, conditional-write, lenient-read, status, and 5-step wizard behavior (Rust setup.rs and .NET SharedSetupConfig/ ManagedEngineState), with matching tests. The CLI status view gains a CAT hub summary line. Engine specification updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CAT Hub config editor to GUI Settings dialog Adds a CAT Hub tab to the Settings dialog so operators can edit the [cat_hub] section of config.toml from the GUI instead of hand-editing the file. Covers radio backend, polling, PTT, events, serial faces, and hamlib_net endpoints with add/remove rows and per-face permissions. The whole config.toml is rewritten on save, so cat_hub is only emitted when the section is actually edited (dirty-gated). A warning banner is shown before the first rewrite since unmodeled keys/comments in the section are dropped. Optional bools use tri-state (omit/true/false). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix launcher SaveSetupRequest for new cat_hub field The cat_hub field added to SaveSetupRequest broke the launcher sync constructor. Set cat_hub: None so the launcher status re-save preserves the existing [cat_hub] section verbatim instead of rewriting it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make publish copy resilient to in-use binaries launcher.ps1 -Rebuild failed when a published binary (e.g. qsoripper-tui) was still running, because Copy-Item cannot overwrite a locked exe/DLL. Add a Copy-PublishArtifact helper that, on a locked destination, renames the in-use file aside (.locked-<timestamp>.old) and copies the fresh build into place. On Windows a running executable or loaded DLL can be renamed but not overwritten; the running process keeps using the renamed file and the next launch picks up the new binary. Stale .old files are best-effort cleaned on the next sideline. Routes the tui, stress-tui, server, cathub, and ffi publish copies through the helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Side-line in-use .NET publish outputs before dotnet publish launcher.ps1 -Rebuild fails with MSB3021/MSB3027 when a running app (e.g. QsoRipper.Gui) locks its published DLL/EXE: dotnet publish's own copy retries ten times then errors out. The earlier Copy-PublishArtifact helper only covered the Rust Copy-Item publish steps, not MSBuild's copy. Add Clear-LockedPublishArtifacts (and a Test-FileLocked probe) that, before each dotnet publish, renames any locked output files aside as .locked-<ts>.old. On Windows a running exe / loaded DLL can be renamed but not overwritten, so this frees the destination paths for the fresh publish while the running process keeps its renamed handles; the next launch picks up the new build. Stale .old files are swept on the next publish and live under gitignored artifacts/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Side-line in-use Win32 exe before MSVC link launcher.ps1 -Rebuild fails the Win32 C build with LNK1104 (cannot open file qsoripper-win32.exe) when a previously built instance is still running: the MSVC linker writes the exe directly into the publish dir and cannot overwrite the locked image. Reuse Clear-LockedPublishArtifacts before the link step so the running exe is renamed aside, freeing the path for a fresh link. The running process keeps its renamed handle; the next launch picks up the new build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Stop stale published processes after launcher rebuild After .\launcher.ps1 -Rebuild, the build side-lines in-use binaries and drops fresh ones but does not restart long-running engine processes. A fresh GUI then keeps talking to a stale engine started before the rebuild, surfacing as stale data such as an empty CAT Hub settings page even when config.toml is valid. Add Stop-StalePublishedProcesses, invoked only after a successful rebuild. It stops processes whose image lives under artifacts\publish and whose start time predates the on-disk binary's last-write time, so the next launch spawns fresh engines. Scoped to the publish tree (external apps like HDSDR/N1MM are untouched) and best-effort so it never aborts the script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make publish copy resilient to in-use binaries launcher.ps1 -Rebuild failed when a published binary (e.g. qsoripper-tui) was still running, because Copy-Item cannot overwrite a locked exe/DLL. Add a Copy-PublishArtifact helper that, on a locked destination, renames the in-use file aside (.locked-<timestamp>.old) and copies the fresh build into place. On Windows a running executable or loaded DLL can be renamed but not overwritten; the running process keeps using the renamed file and the next launch picks up the new binary. Stale .old files are best-effort cleaned on the next sideline. Routes the tui, stress-tui, server, cathub, and ffi publish copies through the helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Side-line in-use .NET publish outputs before dotnet publish launcher.ps1 -Rebuild fails with MSB3021/MSB3027 when a running app (e.g. QsoRipper.Gui) locks its published DLL/EXE: dotnet publish's own copy retries ten times then errors out. The earlier Copy-PublishArtifact helper only covered the Rust Copy-Item publish steps, not MSBuild's copy. Add Clear-LockedPublishArtifacts (and a Test-FileLocked probe) that, before each dotnet publish, renames any locked output files aside as .locked-<ts>.old. On Windows a running exe / loaded DLL can be renamed but not overwritten, so this frees the destination paths for the fresh publish while the running process keeps its renamed handles; the next launch picks up the new build. Stale .old files are swept on the next publish and live under gitignored artifacts/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Side-line in-use Win32 exe before MSVC link launcher.ps1 -Rebuild fails the Win32 C build with LNK1104 (cannot open file qsoripper-win32.exe) when a previously built instance is still running: the MSVC linker writes the exe directly into the publish dir and cannot overwrite the locked image. Reuse Clear-LockedPublishArtifacts before the link step so the running exe is renamed aside, freeing the path for a fresh link. The running process keeps its renamed handle; the next launch picks up the new build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add CAT hub setup-wizard management to engine setup Operators can now read and replace the qsoripper-cathub daemon's [cat_hub] section through the engine SetupService instead of hand-editing config.toml. A new CatHubSettings proto family (radio/poll/ptt/events plus serial faces and hamlib_net endpoints) is wired into SaveSetup (field 11), SetupStatus (field 26), and a new SETUP_WIZARD_STEP_CAT_HUB wizard step. The contract is full-replacement: when cat_hub is supplied it is the complete desired section (radio with backend required, at least one endpoint required) and the engine rewrites [cat_hub]; when omitted the section is preserved verbatim, so routine saves never reserialize the daemon's configuration. The section is parsed leniently and separately for status display so a malformed [cat_hub] can never break engine load. Both engines implement identical validation, conditional-write, lenient-read, status, and 5-step wizard behavior (Rust setup.rs and .NET SharedSetupConfig/ ManagedEngineState), with matching tests. The CLI status view gains a CAT hub summary line. Engine specification updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CAT Hub config editor to GUI Settings dialog (#483) Adds a CAT Hub tab to the Settings dialog so operators can edit the [cat_hub] section of config.toml from the GUI instead of hand-editing the file. Covers radio backend, polling, PTT, events, serial faces, and hamlib_net endpoints with add/remove rows and per-face permissions. The whole config.toml is rewritten on save, so cat_hub is only emitted when the section is actually edited (dirty-gated). A warning banner is shown before the first rewrite since unmodeled keys/comments in the section are dropped. Optional bools use tri-state (omit/true/false). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix launcher SaveSetupRequest for new cat_hub field (#484) * Add CAT Hub config editor to GUI Settings dialog Adds a CAT Hub tab to the Settings dialog so operators can edit the [cat_hub] section of config.toml from the GUI instead of hand-editing the file. Covers radio backend, polling, PTT, events, serial faces, and hamlib_net endpoints with add/remove rows and per-face permissions. The whole config.toml is rewritten on save, so cat_hub is only emitted when the section is actually edited (dirty-gated). A warning banner is shown before the first rewrite since unmodeled keys/comments in the section are dropped. Optional bools use tri-state (omit/true/false). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix launcher SaveSetupRequest for new cat_hub field The cat_hub field added to SaveSetupRequest broke the launcher sync constructor. Set cat_hub: None so the launcher status re-save preserves the existing [cat_hub] section verbatim instead of rewriting it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
If the radio serial/TCP link drops mid-session (USB unplug, radio power-cycle, cable hiccup, or write error), the daemon previously left every client face wired to a dead link until the whole hub was restarted. This is why restarting HDSDR alone never recovered rig control - the dead session lived in the hub, not in HDSDR. Add run_transport_supervised: run_transport now returns a TransportOutcome and hands its command receiver back on disconnect, so the supervisor reopens the transport with capped exponential backoff (0.5s..5s) and resumes serving the same queued commands. Native push (auto-info) is re-armed on every reconnect, since a power-cycled radio forgets its AI state (design 8.4/8.7). Shutdown (all RadioLinks dropped) still stops cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix cathub VFO tracking Restore trusted TS-590 active receive VFO changes while keeping Hamlib set_vfo benign for foreign CAT clients. Record full TS-590 IF native-push status so cached active-VFO frequency, mode, PTT, and split state stay current, and notify TS-590/TS-2000 faces with synthesized IF status when the active VFO changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix TS-590 active VFO event tracking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix HDSDR tuning on active VFO B Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Randy Treit <rtreit@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Randy Treit <rtreit@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Harden cathub against multi-client failure modes Fleet-mode bug hunt on the CAT hub daemon. Fixes a set of concrete failure modes found across the serial face, hamlib_net face, radio frame matcher, baseline poller, and event fan-out: - HDSDR/OmniRig VFO A<->B switch: the TS-2000 translator now leads the RxVfo notification with an FA frequency frame (plus MD) for the new active VFO, not an IF-only frame. HDSDR retunes the panadapter only from FA, so an IF-only push left it parked on the old VFO. - PTT release on disconnect: a face that drops while holding the PTT lease (TCP drop, crash, EOF, over-long frame) now unkeys the radio and frees the lease in teardown instead of holding TX up to ptt_max_tx_ms. Releases only when the disconnecting face owns the lease. - Lagged-subscriber resync: on a broadcast lag a face replays the full state snapshot through its dialect (active VFO applied last) instead of skipping ahead and rendering permanently stale state. - Input bounds: serial-face partial-frame buffer, radio frame reassembler, and hamlib_net request-line reader are all capped at 4096 bytes; the hamlib_net reader is now a length-bounded manual reader, not an unbounded lines() adapter. - hamlib ERP: generic get-style replies terminate with RPRT 0 so ERP clients (Log4OM) stop blocking on a missing terminator. - hamlib set_mode: an unknown mode token is rejected with RPRT -EINVAL and writes nothing, instead of silently coercing to USB with RPRT 0. - Poll back-off keys off the currently active VFO's native-push coverage, not a hard-coded VFO A. Updates docs/design/cathub-multi-client-cat-hub.md with the new behavioral contracts (8.2, 8.4, 8.5, 8.9). Deferred and documented (regression risk / low value): matched-frame re-routing, select! self-lag, and the remaining Tier-3 findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix N1MM frequency tracking on VFO B in native TS-590 dialect The native Ts590Dialect (N1MM Logger+, ARCP-590, Log4OM-as-TS590) pushed only a bare per-VFO FB frame on an active-VFO frequency change. Operating-frequency trackers read the displayed frequency from the IF; status answer, not from FB, so frequency updates silently stopped whenever the rig was on VFO B while VFO A worked. MD reads and writes were also hardcoded to VFO A instead of following the active VFO. - format_notification now appends a synthesized IF frame for active-VFO Freq and Mode changes (mirrors the TS-2000 lead-with-FA rule for HDSDR). - MD; read and MD<n>; write now follow snapshot.rx_vfo. - Add ts590 repro tests for VFO-B freq push, mode read, and mode write. - Add a hamlib_net guard test (WSJT-X/Log4OM) for set_freq/set_mode on VFO B. - Document the native-dialect IF-on-active-VFO rule in the design doc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SO1V) (#494)
N1MM Logger+ in SO1V mode rejects VFO B ("You should not use VFO B when
configured for SO1V") and freezes its frequency display whenever the radio
reports VFO B as active in the Kenwood IF; frame (P10=1). Reporting P10=1 is
faithful radio behavior, but it defeats seamless A/B operation for single-VFO
loggers.
Add an opt-in per-face single_vfo policy to the native ts590 dialect. When
enabled, the face always presents the operating (receive) VFO as VFO A:
- IF; reads force P10=0 and split=0 with the operating freq/mode (38-byte len kept)
- FA reads/writes target the operating VFO; FB mirrors it (no physical VFO-B leak)
- FR/FT VFO-select verbs are intercepted (read VFO A, write swallowed)
- notifications re-present an A/B switch as FA+MD+IF (P10=0); inactive churn suppressed
- raw passthrough is suppressed so a native frame can't leak a VFO-B verb
Enabled for the n1mm face only; ARCP-590 stays dual-VFO (single_vfo=false).
Adds 8 ts590 unit tests + an A/B-switch integration test, plus config and docs
(design 8.4.2, engine spec, operator setup).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… Log4OM) (#495) WSJT-X stopped decoding on VFO B and Log4OM logged the wrong frequency because the rigctld-compatible hamlib_net faces had no single_vfo operating-VFO virtualization (unlike the serial faces fixed in #494). On VFO B the engine exposed VFOB via 'v' (confusing WSJT-X) and get_vfo_info VFOA returned the STALE inactive VFO A frequency, so Log4OM (which polls +\get_vfo_info VFOA) tracked the wrong freq. Adds a strict single-VFO contract to hamlib_net faces flagged single_vfo=true: any requested VFO resolves to the operating (rx) VFO and is presented as VFOA; get_vfo_info forces Split 0; get_split reports 0/VFOA; V ? advertises only VFOA; set_split_vfo 1 is rejected with RPRT_ENAVAIL (so split is visibly unavailable, forcing WSJT-X Fake It rather than silently TXing on the wrong freq). Freq/mode writes already target the operating VFO, so tuning stays real. wsjtx (4533) and log4om (4534) get single_vfo=true; the engine face (4532) stays single_vfo=false so the engine logs the true operating VFO. Adds 10 hamlib_net unit tests; all 186 cathub tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ograms (#496) * cathub: track real TS-590 split and TX VFO, reflect to all attached programs The hub modeled the receive VFO and split state but never learned the transmit VFO from the rig, and a raw FT write from a client (ARCP-590, N1MM) was not observed at all. As a result the hub's split/TX-VFO view could go stale and attached programs would not reflect what the radio was actually doing. Three changes on the native TS-590 backend: - record_if_status now derives the transmit VFO from each IF poll. On a two-VFO radio the TX VFO during split is always the non-receiving VFO, so split + RX VFO fully determines it. The hub's split/TX-VFO state is now authoritative from the rig itself on every poll. - record_event observes raw FT passthrough writes, recording split as active whenever the transmit VFO differs from the receive VFO, so the snapshot tracks reality between polls. - apply(SetRxVfo) now emits FR;FT; together when not in a deliberate split, so a modeled operating-VFO switch can never leave the radio in an accidental reverse-split; the deliberate-split path still preserves the operator's transmit VFO. Adds tests for IF-derived TX VFO (split and simplex), raw FT observation, and the FR+FT operating-VFO switch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * cathub: add transparent mirror dialect for ARCP-590 Native TS-590 controllers (ARCP-590) drifted out of sync because the hub served them a hybrid view: modeled frames were synthesized while only unmodeled frames were relayed verbatim, so the controller's operating-VFO belief could invert from the radio (A/B sent the wrong FR direction, tuning/split appeared dead). Add a transparent mirror dialect (ts590-transparent): forward every request to the radio verbatim except PTT (hub-lease-arbitrated), AI (per-face virtualized; rig stays in AI2), and ID/PS keepalives (answered locally so an idle controller generates zero radio traffic). Relay the radio's entire CAT stream byte-for-byte, so a controller that already speaks the native protocol behaves as if wired directly to the rig. This eliminates the synthesis/snapshot drift class entirely. The event bus now broadcasts a modeled native frame twice: a coalesced Change (for virtualizing faces) and a verbatim RawNative (for transparent faces). Unmodeled frames still relay as a single Raw event. Existing dialects are byte-for-byte unchanged. A lagged mirror face re-syncs via raw FA/FB/FR/FT/MD/IF frames. ts590-transparent is rejected with single_vfo. Live-verified on a real TS-590 with ARCP-590, HDSDR/OmniRig, WSJT-X, N1MM (SO1V), and Log4OM all running together: clean A/B, correct frequency/mode propagation, no drift. 192 lib tests pass; clippy -D warnings and fmt clean; workspace line coverage 84.9%. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * cathub: honor single_vfo on Hamlib NET faces so Log4OM follows A/B HamlibNetConfig had no single_vfo field, so single_vfo=true on the wsjtx and log4om endpoints was silently dropped by serde and never applied to any Hamlib NET face. Log4OM polls +\get_vfo_info VFOA, which resolved the VFO from the literal argument and always returned physical VFO A, so it showed a stale frequency after an A/B switch (WSJT-X polls get_freq, which already reads the operating VFO, so it was unaffected). Add the single_vfo field to HamlibNetConfig, apply it when building the hamlib_net FaceContext, and virtualize the VFO-identity read paths: under single_vfo, get_vfo_info / get_vfo / get_split_vfo present the operating (rx) VFO as VFOA with Split forced to 0, the ;V ? handshake advertises only VFOA, and set_split_vfo is absorbed without mutating the radio's real split. Faithful dual-VFO faces (the engine endpoint) keep reporting the literal physical VFO and real split. Verified end-to-end against the live TS-590 on VFO B: the log4om endpoint now reports the operating VFO as VFOA while the engine endpoint still reports physical VFOA/VFOB separately. 200 lib tests pass (+8 new), clippy -D warnings clean, workspace coverage 84.95%. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reduce the TUI rig snapshot poll interval and the default rig-control stale threshold to 100 ms so CAT frequency changes are visible on the next interactive refresh instead of waiting behind a 500 ms UI poll or stale cache window. Align the .NET rig-control default and update the engine spec/setup docs so both engine implementations advertise the same responsiveness target. Co-authored-by: Randy Treit <rtreit@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror TS-2000 FB reads and writes onto the active operating VFO so HDSDR/OmniRig waterfall tuning cannot silently tune inactive VFO B while the TS-590 stays on VFO A. Add dialect and integration regressions for the observed HDSDR FB write path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Display fractional frequency digits across UIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use radio-style frequency readouts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unnecessary string.Create usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document string interpolation guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address frequency formatting review findings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add unified test scripts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add PR test coverage gates Run Pester and engine conformance in pull request validation, route Win32 CI through the shared test script, and include coverage-excluded Rust packages in CI tests. Update the shared test script to print per-suite pass, fail, skipped, and total counts alongside timing output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove ignored live TS-590 tests Remove the opt-in TS-590 hardware integration tests so Rust test summaries no longer report ignored tests. Also remove the now-dead helper script and live-radio test runbook references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align local validation with PR checks Run CI-style quality gates from build-and-test, include engine conformance in the shared test runner, and make Pester tests compatible with the Pester version used in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI PowerShell parity regressions Make Pester tests visible to Pester 5 run-phase scoping and keep engine startup candidate lists array-shaped under StrictMode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Randy Treit <rtreit@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Improve logger and lookup display Add the UTC date to the TUI recent QSO list and widen the editable frequency field so precise MHz values are visible. Also include the city field in CLI callsign lookup output when available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update quick-xml for cargo deny Bump quick-xml to the fixed 0.41 line so the Rust quality gate resolves past RUSTSEC-2026-0194 and RUSTSEC-2026-0195. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix cargo-deny workflow invocation Run cargo deny from src/rust without passing the config flag after the check subcommand. Recent cargo-deny versions treat --config as a common option before subcommands and default to deny.toml in the working directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Randy Treit <rtreit@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add multi-client WinKeyer broker to CatHub * Separate JS8Call CAT endpoint * Preserve signed digital signal reports * Reuse persistent rigctld connection * Bound CatHub radio reply deadlines * Isolate panadapter mode and normalize N1MM speed * Cover N1MM buffered speed transcript * Preserve N1MM WinKeyer transmit streams * Add a dedicated WinKeyer maintenance face * Expose CatHub application-side ports * Fix infinite spin loop in n1mm_buffer_pointer test that caused 60+ second timeout The test was draining pending data with an infinite while loop that had no upper bound. If the broker streamed data in 10ms intervals, the loop would continue indefinitely. Added a 100ms timeout boundary so the drain operation completes quickly while still allowing queued data to drain safely. Test execution time reduced from 60+ seconds to 0.27 seconds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4d9b5673-f9fc-4eb1-ab51-484b81931473 * ci: rerun rust checks after test hang fix * Fix WinKeyer pointer regression test deadlock --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
17 tasks
This was referenced Jul 15, 2026
rtreit
marked this pull request as ready for review
July 15, 2026 20:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CatHub is independently useful station infrastructure and does not require QsoRipper logging functionality. This change imports the original CatHub source history and turns the initialized repository into a standalone Rust workspace with its own daemon, configuration ownership, protocol packages, documentation, tests, CI, and tagged-release automation.
Standalone configuration now defaults to CatHub-owned paths and supports validation, effective TOML or JSON output, explicit managed-section selection, and reversible migration from a QsoRipper [cat_hub] section. The first protocol release preserves the deployed qsoripper.services wire package while transferring ownership to CatHub.
The release workflow builds Windows and Linux archives, emits SHA-256 checksums, and packages the Rust and .NET protocol clients. Hardware transmission testing remains an attended release step.
Validated with .\build.ps1 check, a release-mode daemon build, cargo package for cathub-protocol, NuGet packaging, Buf lint, strict Clippy, and 272 Rust tests.
Closes #1.
Cross-repository QsoRipper integration remains tracked by treitforge/qsoripper#540.