feat(spot): FreeDV Reporter as a live WebSocket source (FR-SPOT-08); drop WSPRnet - #201
Merged
Merged
Conversation
…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
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.
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.viewrole: 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).ws://on port 80, as the reference client does.wsswas not tried.last_updatetimestamp 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:
freq_changewithfreq0 — a station with no frequency set. Normal.message_updatewith 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
rejectedwould say "0" while every accented message was being cleared. There is now a separatedegradedcount 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
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.hashlib.MAX_TOKEN) is documented in the code.Verified
fmt, clippy
--all-features, 479 workspace tests (bounded),cargo xtaskOK. 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)
DEFAULT_REFRESHinfreedv_source.rs.)Not done
wss(TLS); whatlast_update's zone and precision are; SOTA (still blocked on API-consumers membership).🤖 Generated with Claude Code
https://claude.ai/code/session_01Ma4bQKqGNFTYyVSEKPcXRk