Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenWSFZ — Requirements Document

**Version:** 1.31
**Version:** 1.33
**Date:** 2026-07-12
**Status:** Draft
**Prepared by:** Requirements Analyst (AI-assisted)
Expand Down Expand Up @@ -196,6 +196,8 @@ Only one persona is in scope for v0.x.
| FR-053 | GridTracker2 / WSJT-X UDP outbound broadcaster | A new `ExternalReportingService` (`IHostedService`, registered unconditionally, inert until `externalReporting.enabled` is `true` with at least one enabled target) SHALL implement the WSJT-X UDP network protocol's outbound message subset: **Heartbeat** (fixed interval), **Status** (dial frequency, mode, TX/RX state, DX call/grid, audio offsets — on-change plus at least once per heartbeat interval), **Decode** (one per decoded FT8 message, mirroring `ALL.TXT`), **Clear** (once per decode-cycle boundary before that cycle's Decode datagrams), **QSOLogged** (mirroring `ADIF.log`, skipped on watchdog/operator abort exactly as the ADIF write itself is), and **Close** (on graceful daemon shutdown). Every enabled target SHALL receive an identical copy of each datagram; a disabled target SHALL receive none; an unresolvable target host SHALL log a Warning once and SHALL NOT block delivery to other targets. Config-save reconciliation (open new targets, close removed ones) SHALL take effect without a daemon restart. **Absolute exclusion, no exceptions:** a decode, an active-QSO partner named in a Status datagram, or a completed QSO reported via QSOLogged whose associated callsign resolves to an R&R-study synthetic entry (NFR-021 Q-prefix convention) or an unresolved (unknown) region SHALL NEVER be sent to any external target, under any circumstance. This exclusion is enforced unconditionally inside `ExternalReportingService` itself, independent of `DecodeNoiseSuppressionConfig`'s operator-toggleable suppression settings (which gate the decode panel/QSO automation only), and SHALL NOT be exposed as any Settings-page control or config field. | Must Have |
| FR-054 | GridTracker2 / WSJT-X UDP inbound listener and trust boundary | The same service SHALL bind a single inbound listener (when enabled) to the first enabled target's port (WSJT-X convention: the app listens on the port it sends to) and honour exactly four inbound message types. **Halt Tx** SHALL always call `IQsoController.AbortAsync` — never gated by `honourInboundCommands`, since a third-party program forcing TX *off* is safety-improving by construction. **Reply** (operator selects a decoded station) and **Free Text** (accepted and stored; intentionally has no transmission effect — no OpenWSFZ TX state machine has a free-message slot yet) SHALL be acted on only when `honourInboundCommands` is `true`; when `false` they SHALL be discarded with an Information-level log naming the ignored command. **Close** SHALL be logged at Information and SHALL NOT terminate the daemon under any circumstance. Any other recognised message type (Replay, Location, Highlight Callsign, Switch Configuration, Configure, WSPR Decode, …) SHALL be parsed only far enough to identify it, then discarded at Debug with no observable effect. A datagram that fails to parse (too short, bad magic number, unsupported schema version, truncated field) SHALL be discarded and logged at Debug — no parse failure SHALL propagate an unhandled exception out of the receive loop. `QsoAnswererService` SHALL additionally expose `Task<bool> TryEngageExternal(string callsign, CancellationToken ct)`, engaging a named, currently-decoded, non-filtered-out CQ exactly as its existing auto-answer path would, unaffected by `tx.autoAnswer` (an explicit external reply is a one-shot manual instruction, not automatic behaviour). | Must Have |
| FR-055 | External Programs settings tab | The Settings page SHALL gain an **"External Programs"** tab (subject to FR-016 — ships only once the backend round-trip is fully implemented and testable end-to-end) displaying: an **Enabled** checkbox; an editable target table (columns: Name, Host, Port, Enabled, Delete) with an **"Add target"** button appending a blank row (`host = "127.0.0.1"`, `port = 2237`, `enabled = true`); and a **"Honour inbound commands (Reply / Free Text)"** checkbox with adjacent text stating that Halt Tx is always honoured regardless of this setting. All changes SHALL participate in the existing FR-040 unsaved-changes flow and SHALL be posted via `POST /api/v1/config` on Save, with client-side port-range validation mirroring the server-side FR-052 rejection rule. | Must Have |
| FR-056 | PTT-over-CAT and serial RTS/DTR keying | `IRadioConnection` SHALL gain `Task SetPttAsync(bool transmitting, CancellationToken cancellationToken = default)`, amending the FR-045 restriction (previously read-only apart from frequency-set) to also permit PTT-set. `SerialCatConnection` SHALL implement it with the Kenwood/Yaesu-family commands `TX;` (key) / `RX;` (unkey), the same dialect family as its existing `FA;`/`FA<Hz>;` frequency commands; `RigctldConnection` SHALL implement it with `\set_ptt 1\n` / `\set_ptt 0\n`, reading and validating the `RPRT 0` acknowledgement exactly as `SetDialFrequencyMhzAsync` already does for `\set_freq`. `CatPollingService` SHALL serialise all access to the shared `IRadioConnection` instance — poll-loop reads, `SetDialFrequencyMhzAsync` tune commands, and `SetPttAsync` PTT commands — through a single mutual-exclusion gate, and SHALL expose PTT keying to consumers outside `OpenWSFZ.Daemon.Cat` only via a narrow `ICatPttGate` interface; no other component SHALL hold a direct reference to the shared connection. `AppConfig` SHALL gain a top-level `Ptt` field (`PttConfig`, sibling to `Cat`) with `Method` (`"AudioVox"` \| `"CatCommand"` \| `"SerialRtsDtr"`, default `"AudioVox"`), `SerialPort`, `SerialLine` (`"Rts"` \| `"Dtr"`, default `"Rts"`), `LeadTimeMs` (default 50), `TailTimeMs` (default 50), and `WatchdogTimeoutMs` (default 20000). The daemon SHALL register exactly one `IPttController` at startup per `Ptt.Method`: `AudioOnlyPttController` (unchanged, existing VOX-only behaviour — default), a new `CatPttController` (asserts PTT via `ICatPttGate` before audio, releases after audio + tail time), or a new `SerialRtsDtrPttController` (asserts/de-asserts a raw RTS or DTR line on its own independently-configured serial port, with no dependency on any CAT connection). `ISerialPort`/`SerialPortWrapper` SHALL gain `RtsEnable`/`DtrEnable` properties to support the latter. Both new PTT-asserting controllers SHALL run a hard failsafe watchdog (`WatchdogTimeoutMs`) that forces PTT release and logs at Error if `KeyUpAsync` has not completed in time, in addition to `try`/`finally` and `DisposeAsync` release guarantees. A missing `ptt` config key SHALL deserialise to all defaults, preserving today's VOX-only behaviour exactly; an unrecognised `Method`/`SerialLine` value SHALL log a Warning and fall back to `AudioVox`/`Rts`. No mode-set or other rig-altering command beyond frequency-set (FR-045) and PTT-set is introduced. | Must Have |
| FR-057 | Settings-page PTT configuration UI and hardware Test button | The Settings → Radio hardware tab SHALL split the existing "CAT rig connection" fieldset's container into two side-by-side fieldsets on wide viewports (stacking on narrow ones, following this page's existing intrinsic flex-wrap layout pattern) — "CAT rig connection" (unchanged) and a new "PTT Config" fieldset. The new fieldset SHALL expose `ptt.method` (`AudioVox` \| `CatCommand` \| `SerialRtsDtr`), `ptt.serialPort`/`ptt.serialLine` (shown only when `method = SerialRtsDtr`, reusing the existing `GET /api/v1/serial/ports` endpoint and the `input-with-action`/"↺ Refresh" pattern), `ptt.leadTimeMs`, `ptt.tailTimeMs`, and `ptt.watchdogTimeoutMs` (hidden when `method = AudioVox`, since there is nothing to configure). `web/js/settings.js`'s `POST /api/v1/config` save payload SHALL include a `ptt` key built from these controls — the first time the Settings page has ever sent one (superseding the "Settings page never sends `ptt`" behaviour from design.md Decision 6, whose null-guard fallback-to-`Current.Ptt` behaviour remains as defense-in-depth for any caller that still omits the key). The fieldset SHALL also expose a **Test** button and a three-state result badge (Pass/Error/idle, mirroring `.cat-status-badge`'s existing visual pattern) that `POST`s to a new `POST /api/v1/ptt/test` endpoint. The endpoint SHALL resolve the currently-running `IPttController`/`IQsoController` singletons, reject with HTTP 409 when `IQsoController.Keying` is `true` ("a QSO is currently transmitting") or when the running `Ptt.Method` is `AudioVox` (nothing to test), and otherwise perform a brief (~200–300 ms) **silent** software PTT pulse (`LoadAudio` with a zero-amplitude buffer, then `KeyDownAsync` immediately followed by `KeyUpAsync`), returning `{ "result": "pass" }` on success or `{ "result": "error", "message": "<exception message>" }` (HTTP 200) on any exception — Test confirms only that the assert/release commands were accepted, never that the rig physically keyed, and the UI SHALL state this plainly. The Test button SHALL be disabled/hidden when the last-loaded, live `Ptt.Method` is `AudioVox`, with a hint that a Save plus daemon restart is required before Test reflects a newly-selected method (`ptt.method` is read once at DI-registration time, not hot-reloaded). `CatPttController` and `SerialRtsDtrPttController` SHALL each serialise their entire `KeyDownAsync`→`KeyUpAsync` critical section behind a private mutual-exclusion gate, so a Test click racing an in-progress real transmission queues behind it rather than interleaving with it — a second, independent caller of the shared `IPttController` singleton must never be able to reset the real transmission's watchdog or de-assert its PTT mid-transmission. | Must Have |

### 4.2 User Journeys

Expand Down Expand Up @@ -457,3 +459,5 @@ data in v0.x:
| 1.29 | 2026-07-09 | QA (AI-assisted) | Added the **region-lookup-data-refresh** capability (operator-triggered "refresh region data" backend operation sourcing DXCC entity/continent/CQ-zone/ITU-zone data live from country-files.com's XML country-file release, converting and overwriting the runtime `callsign-regions.json`; new "Region data" settings-page tab with status summary, refresh button, and a diagnostic callsign lookup tool). Implemented by `f-006-region-lookup-country-file-refresh`, archived 2026-07-09 (GitHub issue #40). Backfilled the archived proposal's missing **User-facing:** declaration alongside 1.28 above — see that row for the shared VERSION-bump rationale. |
| 1.30 | 2026-07-11 | QA (AI-assisted) | Added the **decode-noise-suppression** capability (two persisted, independent Region-data-tab settings — "Suppress Unknown region/DXCC decodes" and "Suppress R&R Synthetic decodes" — gating the decode-panel broadcast and `QsoAnswererService`/`QsoCallerService` eligibility upstream of the existing ephemeral column filter; `ALL.TXT` always unaffected; the Unknown control is always interactive, defaulting from region-data presence until the operator makes an explicit choice, which then persists regardless of subsequent refreshes). Implemented by `decode-noise-suppression`, archived 2026-07-11 (PR #68). Bumped **VERSION** to **v0.34**; also corrected this header's **Version**/**Date** fields, which had lagged at 1.27/2026-07-05 since row 1.27 was added despite the changelog having since reached 1.29. |
| 1.31 | 2026-07-12 | QA (AI-assisted) | Added **FR-052** (external reporting configuration schema — `externalReporting` block on `AppConfig`, fully inert by default), **FR-053** (GridTracker2/WSJT-X UDP outbound broadcaster — `ExternalReportingService`, Heartbeat/Status/Decode/Clear/QSOLogged/Close), **FR-054** (inbound listener and trust boundary — Halt Tx always honoured, Reply/Free Text gated by `honourInboundCommands`, Close never terminates the daemon, malformed input never crashes the listener, `QsoAnswererService.TryEngageExternal`), and **FR-055** (External Programs settings tab). Added a §4.3 Integrations row for "GridTracker2 / WSJT-X UDP protocol", distinct from the still-future "PSK Reporter / DX cluster" row. Implemented by `gridtracker-udp-reporting` — a **post-v1.0 addition**: v1.0's gate remains a confirmed two-way QSO via CAT+TX (tracked separately by the in-flight `cat-tx-ptt` change); this change does not alter any `IMPLEMENTATION_PLAN.md` phase or gate, and depends on `cat-tx-ptt` only conceptually (Halt Tx reuses the pre-existing `IQsoController.AbortAsync`, which predates that change). Bumped **VERSION** to **v0.35** (user-facing: new Settings tab). **Amended same day, pre-merge, per QA's review** (dev-tasks/2026-07-12-gridtracker-udp-reporting-review-fixes.md — folded into this row rather than a new one, since `gridtracker-udp-reporting` had not yet merged to `main`): fixed D-014 (the inbound listener's bind silently failed whenever a peer, e.g. GridTracker2, already owned the configured port — the normal case, not an edge case; fixed via `SO_REUSEADDR` plus routing the primary target's outbound sends through the same shared socket) and added FR-053's now-documented absolute, non-configurable exclusion of R&R-synthetic/unknown-region traffic from all external output (Captain's directive — see FR-053's own text above for the exact scope). |
| 1.32 | 2026-07-12 | DEVELOPER (AI-assisted) | Added **FR-056** (PTT-over-CAT and serial RTS/DTR keying — `IRadioConnection.SetPttAsync`, `SerialCatConnection`'s `TX;`/`RX;`, `RigctldConnection`'s `\set_ptt 1\n`/`\set_ptt 0\n` with RPRT ack validation, `CatPollingService`'s wire-serialisation gate and `ICatPttGate` seam, the new `Ptt` config block on `AppConfig`, the new `CatPttController`/`SerialRtsDtrPttController` `IPttController` implementations, `ISerialPort` RTS/DTR support, and the mechanism-agnostic failsafe watchdog). This is the last item standing between v0.x and the v1.0 gate (a confirmed two-way FT8 contact: RX + CAT control + TX) — see `IMPLEMENTATION_PLAN.md` release gate R3. Implemented by `cat-tx-ptt`. |
| 1.33 | 2026-07-12 | DEVELOPER (AI-assisted) | Added **FR-057** (Settings-page PTT configuration UI and hardware Test button — split "CAT rig connection" into two side-by-side fieldsets with a new "PTT Config" fieldset exposing `ptt.method`/`serialPort`/`serialLine`/`leadTimeMs`/`tailTimeMs`/`watchdogTimeoutMs`, a new `POST /api/v1/ptt/test` silent software-pulse endpoint gated by a 409 while a real QSO is keying or the running method is `AudioVox`, and a `.ptt-test-badge` Pass/Error/idle result indicator). Raised directly by the Captain during hardware acceptance (gates 14–15) after discovering there was no way to see or change `ptt.method` without hand-editing `config.json` and restarting — this amends design.md Decision 6's original "no new UI" scope (`cat-tx-ptt` change, tasks.md section 17). Also fixed a safety-critical gap surfaced while scoping this UI: `CatPttController`/`SerialRtsDtrPttController` had no internal call-serialisation, so a second caller (the Test button) could race a real in-progress transmission and de-assert its PTT mid-transmission — both controllers now hold a private `SemaphoreSlim(1,1)` across their entire `KeyDownAsync`→`KeyUpAsync` critical section. Implemented by `cat-tx-ptt` (dev-tasks/2026-07-12-cat-tx-ptt-settings-ui.md). |
Loading
Loading