Skip to content

Record what the radio was doing, and turn PTT into a recording - #4

Merged
jwussler merged 2 commits into
mainfrom
qso-record
Sep 2, 2026
Merged

Record what the radio was doing, and turn PTT into a recording#4
jwussler merged 2 commits into
mainfrom
qso-record

Conversation

@jwussler

@jwussler jwussler commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Reopens the work from #2, which GitHub auto-closed when its base branch (macos-bundle-identity) was deleted on merge. Rebased onto main; the two commits are unchanged.

The premise was wrong, and checking cost less than building

"Port recording like the C# has" — it was already ported. src/recorder.cpp does WAV writing, continuous record and the replay ring, wired at main.cpp:213 with unit tests, and AUDIT-CSHARP.md:42 has had it as ✅ the whole time.

The real gap: a recording carried no provenance. hamdeck-rec-<local time>.wav and nothing else — no frequency, no mode, and its one timestamp in local time while every log worth matching against is UTC.

What changed

  • A .json sidecar beside every recording — UTC start/end, frequency at start and end, mode, and the operator's overs. Manual, replay and auto alike. Provenance is pushed from the poll loop, not pulled (pulling took the poller's lock while holding the recorder's, from the poll thread). A replay's started_utc is derived from the sample count, because the ring holds what happened before the press. "overs": null"overs": [].
  • QsoRecorder — the C# PTT auto-record (Views/MainWindow.xaml.cs:420), ported rather than invented. Won't start on a tune. Off by default.
  • tools/identify_recording.py — answers "who was that" from the log, reporting LOGGED (a fact) separately from ON THE NET (candidates). The net name is already in COL_COMMENT, so this works retroactively over the whole log with no API.

Gates, each proven to fail first

NoteOver before the file opens · localtime_r for the sidecar · replay stamped when saved · auto-record starting on a tune.

⚠️ The UTC test was blind and passed anyway — this box runs UTC, so localtime_r and gmtime_r agree. It now pins TZ to a POSIX string and asserts the two clocks differ before testing anything.

Proven on the built host

Simulated rig, keyed, unkeyed, waited out the timer → a 3.0 s WAV (rms 5656, real audio) beside a correct sidecar. And identify_recording.py against the real log returned N4GTO / N4TTU / W4ETA with their offsets and the net name.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EqubrbjLXwz1GrVgmTcrf9

jwussler and others added 2 commits September 2, 2026 15:45
The recorder was already ported - WAV, continuous record and the replay ring,
wired and unit-tested. What was missing is that a recording carried no
provenance: filename in local time, no frequency, no mode. Every log worth
matching against is UTC, so nothing downstream could join on it and "who was
that" was unanswerable by construction.

- Every recording now writes a .json sidecar: UTC start/end, frequency at start
  and end, mode, and the operator's overs. Manual, replay and auto alike.
  Provenance is pushed from the poll loop rather than pulled, because pulling
  took the poller's lock while holding the recorder's, from the poll thread.
  A replay's start time is derived from the sample count - the ring holds what
  happened before the press, so stamping it "now" would match it to the wrong
  contact. "overs": null means not tracked; an empty array would claim the
  operator never transmitted.
- QsoRecorder ports the C# PTT auto-record: start on the rising edge, each over
  pushes the idle deadline out, stop on idle or a QSY measured from where the
  QSO started. It must not start on a tune, and a tune must not move the
  remembered PTT state. Off by default - it records whoever the operator is
  talking to, unasked.
- RadioPoller::OnPoll is one fan-out point for everything that reacts to rig
  state, rather than a second loop competing for the CAT port.

The UTC assertion was blind: this box runs UTC, so localtime_r and gmtime_r
agree and the test passed against a local-time stamp. It now pins TZ to a POSIX
string glibc parses without tzdata, asserts the two clocks actually differ, and
compares the filename's local hour to the sidecar's UTC hour. Only then does
localtime_r fail it. Same for the other three: each bug was reintroduced and
watched to fail.

Proven on the built host against the simulated rig, not only in tests: keyed,
unkeyed, waited out the timer, got a 3.0 s WAV with rms 5656 and a sidecar
carrying both UTC times, the frequency, the mode and one closed over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqubrbjLXwz1GrVgmTcrf9
…me claim

Takes a recording's sidecar and reports LOGGED (a fact - the operator wrote the
callsign down) separately from ON THE NET (candidates - a check-in says present,
not that they were the voice on the tape). Those must never be presented as the
same kind of answer.

The net name turns out to be in the log already, so this layer needs no API and
works retroactively over the whole log: NetLogger-sourced QSOs carry it in
COL_COMMENT in the two encodings qsl-queue measured over 29,573 rows. Bracketed
text is not always a net, so only net-shaped tokens are taken as one - checked
against the [New call sign May 2025] rows that would otherwise be reported as a
net name.

Matching is on time alone and the band is shown rather than filtered on:
filtering would silently drop true matches whenever the sidecar's frequency is
unreliable, and a dropped true match is invisible in a way a flagged odd one is
not. A QSO on another band in the same window is marked, never hidden.

A logged QSO is an instant, not a span - COL_TIME_OFF equals COL_TIME_ON on
every row - so the timestamp is when it was logged, usually the end of the
exchange. Hence the pad, and hence every match prints its offset into the
recording so an edge match reads as one.

"Nothing found" is never printed as "nobody". An unlogged QSO and a station
heard but not worked look identical from here.

Verified against the real log: a 26-minute window over the 07/26 80m net
returned three callsigns with their offsets and the net name; the same window
with the band claim changed flagged both rows as a different band; a window
with no QSOs said nothing was found and said why that is not an absence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqubrbjLXwz1GrVgmTcrf9
@jwussler
jwussler merged commit 3adc0ed into main Sep 2, 2026
3 checks passed
@jwussler
jwussler deleted the qso-record branch September 2, 2026 16:19
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