Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
d3dc5f8
feat(gridtracker-udp-reporting): add externalReporting config schema …
frank001 Jul 11, 2026
496e1fe
feat(gridtracker-udp-reporting): implement WSJT-X UDP datagram (de)se…
frank001 Jul 11, 2026
67df59f
feat(gridtracker-udp-reporting): add external reply routing (task 5)
frank001 Jul 11, 2026
6b916f6
feat(gridtracker-udp-reporting): implement outbound broadcaster + inb…
frank001 Jul 11, 2026
aeff9ed
feat(gridtracker-udp-reporting): add External Programs settings tab (…
frank001 Jul 11, 2026
7cc8ae5
docs(gridtracker-udp-reporting): document FR-052..FR-055 and close tr…
frank001 Jul 11, 2026
6ef3127
chore(gridtracker-udp-reporting): close out verification (task 10)
frank001 Jul 11, 2026
9d1009a
chore(gridtracker-udp-reporting): correct task 5.5 checkbox
frank001 Jul 11, 2026
c49b81f
fix(gridtracker-udp-reporting): D-014 — inbound listener bind coexist…
frank001 Jul 12, 2026
8f0c03b
fix(gridtracker-udp-reporting): absolute exclusion of synthetic/unkno…
frank001 Jul 12, 2026
b793aad
docs(gridtracker-udp-reporting): amend spec/design/tasks/REQUIREMENTS…
frank001 Jul 12, 2026
fe081c7
fix(release-versioning): sync README/REQUIREMENTS anchor sentences to…
frank001 Jul 12, 2026
cf51267
docs(gridtracker-udp-reporting): dev-task for PR #70's Linux CI failure
frank001 Jul 12, 2026
53347f4
fix(gridtracker-udp-reporting): deterministic Linux-safe D-014 AC-2 test
frank001 Jul 12, 2026
57a820f
docs(gridtracker-udp-reporting): close out CI-green confirmation for …
frank001 Jul 12, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ JS8, JT9, JT65, WSPR, and related).
## Status

> **Pre-release — source only.** No binaries are distributed yet.
> The current release is **v0.34**. v0.x scope: FT8 receive and transmit,
> The current release is **v0.35**. v0.x scope: FT8 receive and transmit,
> CAT rig control, a web UI (loopback or passphrase-protected LAN),
> single operator.
> v1.0 is reached when the software can complete a confirmed two-way contact
Expand Down
14 changes: 10 additions & 4 deletions REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# OpenWSFZ — Requirements Document

**Version:** 1.30
**Date:** 2026-07-11
**Version:** 1.31
**Date:** 2026-07-12
**Status:** Draft
**Prepared by:** Requirements Analyst (AI-assisted)
**For:** Architecture & Planning Team
Expand All @@ -20,7 +20,7 @@ they prefer (Windows, Linux, macOS) and reach it from a local browser.
work prior to a confirmed QSO; v1.0 is reached when the software can
make a confirmed two-way contact (RX + CAT control + TX); each
user-facing feature shipped increments the minor version.
The current release is v0.34.** The v0.x body of work now covers FT8
The current release is v0.35.** The v0.x body of work now covers FT8
receive and transmit, CAT rig control, an automated QSO answerer and
caller, and a web UI reachable over loopback or a passphrase-protected
LAN — for a single operator, source-only distribution. v1.0 is reached
Expand Down Expand Up @@ -192,6 +192,10 @@ Only one persona is in scope for v0.x.
| FR-049 | FT8 TX encode and audio synthesis | The native shim SHALL export `ft8_encode_message(const char* message, uint8_t* tones_out, int tones_capacity)` returning 79 on success or a negative error code on failure. The shim version SHALL be `20260017`. The managed binding (`Ft8LibInterop`) SHALL validate arguments and throw `InvalidOperationException` on negative return codes. The application SHALL synthesise FT8 audio from 79 tone indices using continuous-phase rectangular-pulse frequency modulation at 48000 Hz, 6.25 baud symbol rate, 6.25 Hz tone spacing — producing exactly 606720 samples (79 × 7680) per transmission with amplitude normalised to ±0.5. TX audio SHALL be played through the configured audio output device via an `IPttController` abstraction (`AudioOnlyPttController` using WASAPI on Windows). | Must Have |
| FR-050 | FT8 QSO answerer state machine | The daemon SHALL implement a `QsoAnswererService` (`IHostedService`) that monitors decoded FT8 messages for CQ calls and drives a state machine: `Idle → TxAnswer → WaitReport → TxReport → WaitRr73 → Tx73 → QsoComplete → Idle`. The answerer SHALL be configured via a `tx` block in `AppConfig` with fields `callsign`, `grid`, `retryCount` (≥ 1, default 3), and `watchdogMinutes` (≥ 1, default 4). A watchdog timer SHALL abort the QSO to `Idle` if `watchdogMinutes` elapse without a state advance. The retry counter SHALL re-transmit the last message up to `retryCount` times before aborting on silence. `POST /api/v1/tx/abort` SHALL cancel any active TX session and return to `Idle`. `GET /api/v1/tx/status` SHALL return `{ "state": string, "partner": string\|null }`. State transitions SHALL be broadcast to WebSocket clients as `{ "type": "txState", "state": string, "partner": string\|null }` events. | Must Have |
| FR-051 | ADIF QSO log | On every completed QSO (`QsoComplete` state), the daemon SHALL append one record to `ADIF.log` in the same directory as the ALL.TXT decode log (`decodeLog.path`). Each record SHALL use ADIF 3.x tagged-field format `<FIELD_NAME:length>value` terminated by `<EOR>`. Mandatory fields: `CALL` (partner callsign), `GRIDSQUARE` (partner grid, when known), `RST_SENT`, `RST_RCVD`, `QSO_DATE` (YYYYMMDD UTC), `TIME_ON` (HHmm UTC), `QSO_DATE_OFF`, `TIME_OFF`, `OPERATOR` (`tx.callsign`), `MY_GRIDSQUARE` (`tx.grid`), `MODE` (`FT8`). When `decodeLog.dialFrequencyMHz` is non-zero, `FREQ` (MHz) and `BAND` (ITU band name derived from frequency) SHALL also be written; both SHALL be omitted when the frequency is 0.0. Write failures SHALL be logged at Warning and SHALL NOT affect the QSO state or any other pipeline component. No record is written on watchdog or operator abort. | Must Have |
| FR-052 | External reporting configuration schema | `AppConfig` SHALL gain an `externalReporting` object with `enabled` (bool, default `false`), `targets` (array of `{ name, host, port, enabled }`, default `[]`), and `honourInboundCommands` (bool, default `false`). A missing `externalReporting` key SHALL deserialise to these fully-inert defaults, identical to today's behaviour. `GET`/`POST /api/v1/config` SHALL round-trip the object. `POST /api/v1/config` SHALL reject (HTTP 400, no partial persistence) any target entry whose `port` falls outside 1–65535. | Must Have |
| 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 |

### 4.2 User Journeys

Expand Down Expand Up @@ -241,7 +245,8 @@ Only one persona is in scope for v0.x.
| Host OS filesystem | Native API | Both | Read the configuration file at startup; write it on Save. Serve frontend assets read-only. |
| Web browser on the same machine | HTTP + WebSocket | Both | Only client of the application in v0.x. Loopback bind only. |
| **Future** rig control (v1.0) | Serial / USB / network CAT | Both | Required for v1.0 (confirmed QSO); architecture must keep this door open. |
| **Future** PSK Reporter / DX cluster | HTTPS / telnet | Outbound | Not in v0.x, but typical for HAM tools and worth noting for the architect. |
| GridTracker2 / WSJT-X UDP protocol | UDP (proprietary, WSJT-X-defined) | Both | FR-052–FR-055. Post-v1.0 addition; lets GridTracker2 (and other WSJT-X-protocol clients: JTAlert, N1MM+, Log4OM, …) see spots/map and log completed QSOs. Inert until the operator opts in; unauthenticated by design, matching WSJT-X's own trust model. |
| **Future** PSK Reporter / DX cluster | HTTPS / telnet | Outbound | Not in v0.x, but typical for HAM tools and worth noting for the architect. PSK Reporter itself remains future work — distinct from the GridTracker2/WSJT-X UDP row above. |
| **Future** logbook formats (ADIF, Cabrillo) | File I/O | Outbound | Not in v0.x; deferred with decode persistence. |

### 4.4 Data Requirements
Expand Down Expand Up @@ -451,3 +456,4 @@ data in v0.x:
| 1.28 | 2026-07-09 | QA (AI-assisted) | Added the **qso-confirmation** capability (in-memory worked-before index built from `ADIF.log`, resolving each decoded callsign's DXCC entity/continent match against logged QSO history; three new readonly indicator columns — P/C/R — in `#decodes-table`, fully advisory and never affecting decode acceptance). Implemented by `adif-qso-confirmation`, archived 2026-07-09. Backfilled the archived proposal's missing **User-facing:** declaration (predates Gate G9 being consistently enforced at merge time) and bumped **VERSION** to **v0.33** alongside 1.29 below, since both this and `f-006-region-lookup-country-file-refresh` were archived together in one commit without either bump having been applied at the time. |
| 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). |
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.34
0.35
Loading
Loading