Skip to content

feat(spot): FreeDV Reporter as a live WebSocket source (FR-SPOT-08); drop WSPRnet - #201

Merged
dc0sk merged 3 commits into
mainfrom
feat/freedv-reporter
Sep 21, 2026
Merged

dc0sk merged 3 commits into
mainfrom
feat/freedv-reporter

Conversation

@dc0sk

@dc0sk dc0sk commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Stacked on #199#198#197#196.

What

Stations on the air right now on FreeDV Reporter (qso.freedv.org) appear as coral nameplates. WSPRnet is dropped, per your decision.

  • Read-only. The source joins in the view role: you are not listed as a station, and nothing identifying is sent. The connect is exactly {"role":"view","protocol_version":2}; the request carries only the program name and version; the only other thing sent is a pong to each ping (FR-SPOT-12).
  • Plain ws:// on port 80, as the reference client does. wss was not tried.
  • It is a presence list, so a station's spot is re-stamped every 30 s while it stays on the roster and fades after it leaves. (A design choice — please overrule if you'd rather age from the last thing a station did; the events carry a last_update timestamp that I did not use.)

New in k4-spot, hand-written, dependency-free and bounded like the MQTT client: a strict JSON parser, a WebSocket client (RFC 6455, including SHA-1 and base64 for the accept key), Engine.IO/Socket.IO framing, the roster, and the source. App: a fifth worker slot, FreeDvPrefs, a Networks section, demo spots.

Checked against the real service

The format came from another client's public source. I then joined the real service five times for at most ten seconds each, read-only, printing counts and field shapes only — never a callsign or any text. The first build rejected 10–12 events per session as malformed. Both causes were my strictness, not the server's:

  1. freq_change with freq 0 — a station with no frequency set. Normal.
  2. message_update with non-ASCII text. Normal.

Hand-written test data had never contained either. The rule now: a field of the wrong type rejects the event; a well-typed value that can't be shown degrades that one field. The same probe then reported 0 rejected (42–45 stations on 14–16 distinct frequencies, 1 MHz to 10.49 GHz).

A review then caught that this fix had blinded the probe. The degrade path had no counter, so a probe printing only rejected would say "0" while every accented message was being cleared. There is now a separate degraded count with value-free shapes, and the fifth session read 0 rejected and 3 degraded — three non-ASCII messages that had been dropped all along. (I had also written that a nameplate "cannot" show non-ASCII text; it can — keeping only printable ASCII is a deliberate policy for untrusted text, and the docs now say so.) A clean live run only exonerates the parser against the shapes those seconds contained.

Findings beyond this feature

  • Nearly every bound was unpinned. My tests built their boundary inputs from the constants under test, so MAX_DEPTH, MAX_NODES, MAX_STRING, the header limits, the ping limits, MAX_STATIONS… could each change by one and nothing failed — 22 survivors after the checks themselves had been mutation-tested. The same was true of the POTA parser in feat(spot): POTA as a polled spot source (FR-SPOT-08) #196 (six more). All are now numbers; the POTA fix is its own commit, also pushed to feat(spot): POTA as a polled spot source (FR-SPOT-08) #196's branch.
  • SHA-1 vectors I wrote from memory were wrong in the last hex digit of one; replaced with values computed by hashlib.
  • A JSON string reader that re-validated the whole remaining input per character (quadratic) — found by reading, now linear with a test.
  • More than 250 mutations in all; redundant guards that masked each other were deleted, and one equivalent mutation (MAX_TOKEN) is documented in the code.

Verified

fmt, clippy --all-features, 479 workspace tests (bounded), cargo xtask OK. A scripted server whose frames are built and read by hand, independent of the crate's own codec, checks the wire.

Validation parked with DC0SK (not done here)

  • Enable FreeDV Reporter in Networks with a 20 m or 40 m view. Do coral plates appear at plausible frequencies? Is the status line "connected"? — Confirmed 2026-09-22 on the live K4: real coral plates draw at plausible frequencies and click-to-tune lands on them correctly (status-line wording not specifically checked).
  • Watch a station leave: does its plate stop being refreshed and fade?
  • Is a 30 s presence refresh the right feel? (Constant DEFAULT_REFRESH in freedv_source.rs.)
  • Confirm nothing about you shows up on the FreeDV Reporter map or list while this is on.

Not done

wss (TLS); what last_update's zone and precision are; SOTA (still blocked on API-consumers membership).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ma4bQKqGNFTYyVSEKPcXRk

dc0sk and others added 3 commits September 21, 2026 17:05
…stants

The boundary inputs of the POTA parser tests were built from the constants they
test (MAX_BODY + 1, MAX_SPOTS + 1), so changing a constant moved the test with it
and nothing noticed. Mutating the constants themselves showed MAX_BODY, MAX_SPOTS,
the frequency ceiling and the default poll interval all unpinned.

The limits are now written as numbers (512 KiB, 2000 spots, 64-character text,
300 GHz plus one hertz, 30 s / 60 s / 1 h). MAX_TOKEN stays an equivalent
mutation: the same 64 is applied again downstream by sanitise_text, and a string
there is a borrowed slice; documented in the code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ma4bQKqGNFTYyVSEKPcXRk
…drop WSPRnet

Stations on the air right now on FreeDV Reporter (qso.freedv.org) appear as coral
nameplates. The source joins in the read-only `view` role: the operator is not
listed as a station and nothing identifying is sent beyond the program's name and
version.

New in k4-spot, hand-written, dependency-free and bounded like the MQTT client:
- json: a strict parser (8 levels, 20 000 values, 4096-byte strings; no trailing
  comma, duplicate key or lone surrogate).
- ws: a WebSocket client (RFC 6455) with SHA-1 and base64 for the accept key,
  checked against FIPS 180-4, RFC 4648 and the RFC's own examples; the upgrade
  reply is checked in full, and masked, reserved, oversize or fragmented-control
  frames are refused.
- sio: Engine.IO 4 / Socket.IO 4 packets.
- freedv: the station roster (a presence list; capped at 4096 stations).
- freedv_source: upgrade, open, view connect, liveness from the ping timing the
  server announces, reconnect with backoff, periodic re-stamping of stations that
  stay on the roster.
App: a fifth worker slot, FreeDvPrefs, a Networks section, demo spots.

The format is from another client's public source and was then checked against the
real service, four sessions of at most ten seconds in the view role. That showed the
first parser rejected 10-12 events per session as malformed: a freq of 0 (meaning no
frequency) and non-ASCII message text, both normal. A wrong field type still rejects
an event; a well-typed value that cannot be shown now degrades only that field. The
same probe then reported 0 rejected.

Mutation testing also found that nearly every bound in the new modules was unpinned
(tests built their boundary inputs from the constants they tested); they are now
numbers. The same fix for the POTA parser is the previous commit.

WSPRnet is dropped by decision; SOTA stays blocked on API-consumers membership.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ma4bQKqGNFTYyVSEKPcXRk
…lind

The fix for the strict parser (a wrong type rejects an event; a well-typed value
that is not kept degrades only that field) left the degrade path with no counter
and no shape. A probe printing only `rejected` would then report 0 while every
accented message was being cleared, which is the class of problem it had found.

- Roster::degraded counts well-typed but unusable text fields (too long, or not
  printable ASCII); blank, null and absent are ordinary and are not counted, and a
  wrong type stays a rejection.
- Value-free shapes are kept for degraded events as for rejected ones, and the
  live probe prints both.
- The fifth live session then read 0 rejected and 3 degraded: three non-ASCII
  messages that had been dropped all along.
- The docs said a nameplate cannot show non-ASCII text; it can. Keeping only
  printable ASCII is a deliberate policy for untrusted text, and now says so.
- json tests pin each bound once and derive their fixtures from that one number.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ma4bQKqGNFTYyVSEKPcXRk
@dc0sk
dc0sk changed the base branch from feat/spectrum-afterglow to main September 21, 2026 18:07
@dc0sk dc0sk closed this Sep 21, 2026
@dc0sk dc0sk reopened this Sep 21, 2026
@dc0sk
dc0sk merged commit 39cb5ad into main Sep 21, 2026
5 checks passed
@dc0sk
dc0sk deleted the feat/freedv-reporter branch September 22, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant