Issue #54 asks for a ham radio layout and an ADIF logbook, with no detail behind either. This is what they would be: what the logbook has to hold, how it is stored, where ADIF is read and written, and which of the existing parts each half hangs off. Two things it settles before any code is written. The log is append-only JSON Lines rather than the whole-file dump the bookmarks use, because a station with forty thousand QSOs would rewrite megabytes to log one contact and lose the lot if the power went mid-dump. And a decode is not a QSO: the decoders only ever heard something, so a decoded callsign pre-fills an entry and never writes one. The layout is a fifth entry in the operator layouts that already exist, gated on the rig being able to transmit. Refs #54 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7 Signed-off-by: Stan Grams <sjg@haxx.space>
21 KiB
Planned Features
Recorder
The recorder captures the demodulated audio stream alongside associated metadata (FFT data, decoded signals, rig state) into a structured session on disk, with full playback and seeking support from within the application.
Requirements
| ID | Description |
|---|---|
| REQ-REC-001 | When the user starts recording, the system shall record the currently demodulated audio stream. |
| REQ-REC-002 | When recording audio, the system shall store the recording in OPUS format. |
| REQ-REC-003 | While recording audio, the system shall automatically detect whether the recording should be stored in mono or stereo and select the appropriate format. |
| REQ-REC-004 | While recording is active, the system shall simultaneously record FFT data and all currently visible decoded elements, including APRS and FT8. |
| REQ-REC-005 | While recording metadata, the system shall store FFT data and decoded signal data in a structured data file format. |
| REQ-PLAY-001 | Where recorded sessions exist, the system shall allow playback of recordings from within the same application. |
| REQ-PLAY-002 | During playback, the system shall allow the user to seek to any position in the recording. |
| REQ-SYNC-001 | The system shall maintain time synchronization between the audio recording and the associated data file with at least one-second resolution. |
| REQ-REC-006 | While recording is active, the system shall allow the current cursor position to be stored. |
Architecture
New Crate: trx-recorder
A new crate src/trx-server/trx-recorder/ handles all record and playback logic. It is a library crate consumed by trx-server.
src/trx-server/
trx-recorder/
src/
lib.rs # Public API: RecorderHandle, start_recorder_task()
session.rs # RecordingSession: file management, open/close/finalise
writer.rs # AudioWriter: PCM → Opus encoder
data_file.rs # DataFileWriter: structured JSON Lines data track
index.rs # SeekIndex: time → byte-offset table for audio seeking
playback.rs # PlaybackEngine: file → PCM broadcast for clients
config.rs # RecorderConfig (serde, derives Default)
Integration Points in trx-server
| Source | What is tapped | How |
|---|---|---|
audio.rs pcm_tx |
Raw demodulated PCM frames | New broadcast::Receiver<Vec<f32>> subscriber |
audio.rs spectrum broadcast |
FFT/spectrum frames per RigState.spectrum |
New subscriber on the spectrum watch channel |
audio.rs decoded-message broadcast |
FT8, WSPR, CW, APRS, FT4, FT2, APRS-HF frames | New broadcast::Receiver<DecodedMessage> subscriber |
rig_task.rs state watch |
Frequency/mode/PTT changes | watch::Receiver<RigState> clone |
New RecorderCommand enum |
Start, Stop, MarkCursor | Injected into the existing command pipeline |
No existing code paths are modified beyond:
- Passing a
RecorderHandle(cheapArcwrapper) into the audio and rig tasks. - Adding
RecorderCommandvariants to the command enum (alongside existingSetFreq,SetMode, etc.). - Adding a
[recorder]section toServerConfig.
Session Layout on Disk
Each recording is a session directory named by UTC start time and opening rig state:
<output_dir>/
20260317T142301Z_14074000_USB/
audio.opus
data.jsonl # structured event log (see below)
index.bin # seek index: sorted table of (offset_ms u64, audio_byte u64)
output_dir defaults to ~/.local/share/trx-rs/recordings.
Audio File (REQ-REC-001, REQ-REC-002, REQ-REC-003)
- Format: Opus, using the
opuscrate (already a workspace dependency viatrx-backend-soapysdr). Seek index (index.bin) provides byte → time mapping. - Channel count: determined at session open from
AudioConfig.channels. Ifchannels == 1→ mono; ifchannels == 2→ stereo. Written into the file header and recorded in the session's first data event. - Sample rate: preserved from
AudioConfig.sample_rate(default 48 000 Hz).
Data File (REQ-REC-004, REQ-REC-005)
data.jsonl — one JSON object per line, each with a required offset_ms field giving the millisecond offset from session start (satisfies REQ-SYNC-001 at ≥1 s resolution):
{"offset_ms":0,"type":"session_start","freq_hz":14074000,"mode":"USB","channels":1,"sample_rate":48000,"format":"opus"}
{"offset_ms":1000,"type":"rig_state","freq_hz":14074000,"mode":"USB","ptt":false}
{"offset_ms":2000,"type":"fft","bins_db":[-90.1,-88.4,...]}
{"offset_ms":3412,"type":"ft8","snr_db":-12,"dt_s":0.3,"freq_hz":14074350,"message":"CQ W5XYZ EN34"}
{"offset_ms":4100,"type":"aprs","from":"W5XYZ-9","to":"APRS","path":"WIDE1-1","info":"!3351.00N/09722.00W-"}
{"offset_ms":5000,"type":"cursor","label":"interesting QSO"}
{"offset_ms":61000,"type":"session_end"}
Supported type values:
| Type | Source | Cadence |
|---|---|---|
session_start |
recorder | once, at open |
session_end |
recorder | once, at close |
rig_state |
watch::Receiver<RigState> change |
on change |
fft |
spectrum data from RigState.spectrum |
≤1 Hz (configurable, default 1 s) |
ft8 / ft4 / ft2 / wspr |
DecodedMessage broadcast |
on decode event |
aprs / aprs_hf |
DecodedMessage broadcast |
on decode event |
cw |
DecodedMessage broadcast |
on decode event |
cursor |
RecorderCommand::MarkCursor { label } |
on user request |
Seek Index (REQ-PLAY-002)
index.bin is a flat binary table of 16-byte records written every index_interval_ms (default 1 000 ms):
[offset_ms: u64 LE][audio_byte_offset: u64 LE] ...
At playback seek time, binary search on offset_ms locates the nearest audio frame boundary, enabling random-access playback without full file scan.
RecorderConfig
Added to ServerConfig under [recorder]:
[recorder]
enabled = false
output_dir = "~/.local/share/trx-rs/recordings"
opus_bitrate_bps = 32000
fft_record_interval_ms = 1000
index_interval_ms = 1000
max_session_duration_s = 3600 # auto-split at 1 h; 0 = unlimited
Command API
New variants added to the existing command enum (handled in rig_task.rs):
StartRecording,
StopRecording,
MarkCursor { label: String },
These are exposed via:
- HTTP frontend:
POST /api/recorder/start,POST /api/recorder/stop,POST /api/recorder/cursor - http-json frontend: same commands as JSON messages
Playback Engine (REQ-PLAY-001, REQ-PLAY-002)
PlaybackEngine opens a session directory and:
- Reads
audio.opusand decodes PCM frames in real time. - Publishes decoded PCM frames onto a
broadcast::Sender<Vec<f32>>— the same channel type as the livepcm_tx, so existing decoder tasks and audio-streaming clients receive playback data transparently. - Replays
data.jsonlevents on their originaloffset_mstimestamps, injecting them into theDecodedMessagebroadcast so the HTTP frontend displays historic decodes during playback. - For seek: binary-searches
index.binto find the audio byte offset, then replays data events from the same point.
The playback state machine has two modes, switched by a new RigState.playback field:
pub enum PlaybackState {
Live,
Playing { session: String, offset_ms: u64 },
Paused { session: String, offset_ms: u64 },
}
While PlaybackState is not Live, the server suppresses live hardware polling and PCM capture to avoid mixing live and playback audio.
Time Synchronisation (REQ-SYNC-001)
All timestamps use a single session_epoch: std::time::Instant captured at StartRecording. Every PCM frame, every data event, and every seek-index entry is stamped as (Instant::now() - session_epoch).as_millis() as u64. This gives sub-millisecond internal precision; the requirement of ≥1 s resolution is met by orders of magnitude.
Wall-clock UTC is embedded only in session_start (wall_clock_utc) and in the session directory name, providing absolute time anchoring without depending on system clock monotonicity for sync.
Implementation Phases
Phase 1 — Audio recording (REQ-REC-001, REQ-REC-002, REQ-REC-003)
- Add
trx-recordercrate skeleton;RecorderConfig;RecorderHandle. - Implement
AudioWriterwith Opus output. - Subscribe
AudioWritertopcm_txinaudio.rs; open session onStartRecordingcommand. - Auto-detect channel count from
AudioConfig.channels.
Phase 2 — Metadata recording (REQ-REC-004, REQ-REC-005, REQ-SYNC-001)
- Implement
DataFileWriter; define full event schema. - Subscribe to
DecodedMessagebroadcast; fan-in all decoder types. - Subscribe to state watch; emit
rig_stateevents on freq/mode change. - Emit
fftevents at configured interval from spectrum data. - Write
SeekIndexin parallel with audio.
Phase 3 — Cursor (REQ-REC-006)
- Add
MarkCursorcommand + HTTP endpoint. - Write
cursorevent todata.jsonlwith currentoffset_ms.
Phase 4 — Playback (REQ-PLAY-001, REQ-PLAY-002)
- Implement
PlaybackEngine; Opus decode + PCM broadcast. - Add
PlaybackStatetoRigState; suppress live capture during playback. - Implement seek via
index.binbinary search. - Replay
data.jsonlevents; feed intoDecodedMessagebroadcast. - Expose start/stop/seek endpoints in
trx-frontend-http.
Dependencies to Add
| Crate | Use | Already present? |
|---|---|---|
opus |
Opus encode/decode | Yes (via trx-backend-soapysdr) |
serde_json |
data.jsonl serialisation | Yes |
tokio::fs |
async file I/O | Yes |
Open Questions
- Playback isolation: Should playback be exclusive (block all CAT commands) or concurrent? Initial design blocks CAT polling; revisit if users need to change frequency during playback.
- Session listing API: The HTTP frontend needs an endpoint to enumerate sessions (
GET /api/recorder/sessions). Schema TBD in Phase 4. - Storage limits:
max_session_duration_sauto-splits sessions; amax_total_size_gbhousekeeping option may be needed but is out of scope for initial phases.
Configurator Helper
An interactive CLI tool that guides users through creating configuration files for trx-rs. Instead of editing TOML by hand, the user answers prompts and the tool generates valid, commented configuration files.
Overview
The configurator is a standalone Rust binary (trx-configurator) that reuses
the existing config structs from trx-app, trx-server, and trx-client. It
walks the user through a question-driven flow, validates inputs against the same
rules the binaries use at startup, and writes one or more of:
trx-server.toml— server configurationtrx-client.toml— client configurationtrx-rs.toml— combined server + client configuration
The user chooses which file(s) to generate.
Requirements
| ID | Description |
|---|---|
| REQ-CFG-001 | The tool shall interactively prompt the user for configuration values. |
| REQ-CFG-002 | The tool shall generate trx-server.toml, trx-client.toml, or trx-rs.toml per user selection. |
| REQ-CFG-003 | The tool shall validate all inputs using the same validation logic as the server and client binaries. |
| REQ-CFG-004 | The tool shall write commented TOML with descriptions of each field. |
| REQ-CFG-005 | The tool shall detect connected serial devices and offer them for rig access configuration. |
| REQ-CFG-006 | The tool shall detect available SoapySDR devices and offer them for SDR backend configuration. |
| REQ-CFG-007 | The tool shall support a non-interactive mode that generates a default config file. |
| REQ-CFG-008 | The tool shall not overwrite existing files without confirmation. |
Architecture
New Crate: trx-configurator
A new binary crate at src/trx-configurator/ that depends on trx-app for
config types and validation.
src/trx-configurator/
src/
main.rs # CLI entry point, mode selection
prompts.rs # Interactive prompt helpers (with defaults, validation)
detect.rs # Hardware detection (serial ports, SoapySDR devices)
writer.rs # TOML serialisation with inline comments
Flow
trx-configurator
├── What would you like to generate?
│ [ ] trx-server.toml
│ [ ] trx-client.toml
│ [ ] trx-rs.toml (combined)
│
├── (if server)
│ ├── General: callsign, location
│ ├── Rig: model selection, access (serial/tcp/sdr)
│ │ └── detect serial ports / SoapySDR devices
│ ├── Listen: address, port
│ ├── Audio: sample rate, channels, codec settings
│ ├── SDR: (if soapysdr selected) gain, channels, decoders
│ ├── Uplinks: PSKReporter, APRS-IS
│ └── Decode logs: enable, directory
│
├── (if client)
│ ├── Remote: server URL, auth token
│ ├── Frontends: HTTP, rigctl, http-json (enable/disable, ports)
│ └── Audio: bridge settings
│
└── Write file(s) with confirmation
Hardware Detection
- Serial ports: enumerate available serial devices using
serialportcrate (already a transitive dependency). Present as selectable list with device path and description. - SoapySDR devices: if built with
soapysdrfeature, callSoapySDR::enumerate("")to list available SDR hardware. Present device driver, label, and serial number.
Dependencies
| Crate | Use | Already present? |
|---|---|---|
dialoguer |
Interactive prompts, selection, confirmation | No |
toml_edit |
TOML serialisation preserving comments | No |
trx-app |
Config types and validation | Yes |
serialport |
Serial port enumeration | Yes (transitive) |
soapysdr |
SDR device enumeration (optional) | Yes (feature-gated) |
Logbook and Ham Radio Layout
Two halves of one feature (#54): a station logbook that speaks ADIF, and an operator layout that puts a transceiver's controls and that logbook on one screen.
Nothing in the application records a QSO today. The map's "QSO summary" cards describe contacts between other stations, reconstructed from decoded traffic; bookmarks are frequencies, not contacts. Neither is the operator's own log.
Requirements
| ID | Description |
|---|---|
| REQ-LOG-001 | The system shall record QSOs the operator makes, each holding at minimum callsign, date, time, band, frequency, mode and both signal reports. |
| REQ-LOG-002 | When logging a QSO, the system shall pre-fill frequency, band, mode and time from the selected rig, and the station's own callsign and locator from configuration. |
| REQ-LOG-003 | The system shall allow a logged QSO to be edited and deleted. |
| REQ-LOG-004 | The system shall survive a crash without losing a QSO that was recorded before it. |
| REQ-LOG-005 | The system shall list, search and filter the log by callsign, band, mode and date range. |
| REQ-ADIF-001 | The system shall export the log as an ADIF 3.1.x .adi file. |
| REQ-ADIF-002 | The system shall import ADIF .adi files produced by other logging software, preserving fields it does not itself use. |
| REQ-ADIF-003 | When importing, the system shall identify QSOs already held and shall not duplicate them. |
| REQ-LOG-006 | Where a decoded station is on screen, the system shall offer to start a log entry from it, pre-filled, without logging it unattended. |
| REQ-LOG-007 | The system shall show whether a callsign has been worked before, and on which bands. |
| REQ-LAY-001 | The system shall offer a "Ham radio" operator layout presenting the transceiver controls and the logbook together. |
| REQ-LAY-002 | Where the selected rig cannot transmit, the system shall not offer the ham layout. |
A decode is not a QSO
The decoders are receive-only: FT8, CW, APRS and the rest report what was heard. A heard callsign is the beginning of a log entry, not a contact, and the logbook must never write one by itself — REQ-LOG-006 says offer and pre-fill, never auto-log. Digital QSOs made in WSJT-X or similar arrive the way every other logger takes them: through ADIF import.
Architecture
New crate: trx-logbook
src/trx-client/
trx-logbook/
src/
lib.rs # LogbookHandle: open, add, edit, delete, query, import, export
qso.rs # Qso: the record, its ADIF field mapping, band/mode helpers
adif.rs # ADI reader and writer (pure Rust, no new dependencies)
store.rs # Append-only JSON Lines file, in-memory index, compaction
dedupe.rs # Worked-before and import-collision rules
A library crate under src/trx-client/, beside trx-frontend, consumed by
trx-frontend-http. The log belongs to the station rather than to a rig or a frontend, so it
sits where any frontend can reach it.
Storage: append-only, not a rewritten blob
Bookmarks use PickleDb with AutoDump, which rewrites the whole file on every write. That
is right for a few dozen bookmarks and wrong for a log: a station with 40 000 QSOs would
rewrite several megabytes to log one contact, and lose the lot if the power went during the
dump.
The log is instead a JSON Lines file — the shape trx-decode-log already uses — at
~/.config/trx-rs/logbook.jsonl, appended one record per write and read into an in-memory
index at startup. An edit or a delete appends a new revision of that record's id; the load
keeps the last one, and a compaction pass rewrites the file when superseded records exceed a
threshold. Appending is O(1) and atomic per line, so a crash costs at most the line being
written.
ADIF in-repo
ADI is a tagged text format: <FIELD:length>value, records ended by <EOR>, a header ended
by <EOH>, everything outside a tag ignored. It is small enough to implement exactly, which
this project already prefers for its decoders, and doing so keeps the dependency list where it
is. The reader must be lenient in the ways real files are irregular — lowercase tags, CRLF,
missing header, unknown fields, type indicators — and the writer strict. Unknown fields are
carried through import to export unchanged, so a round trip through trx-rs does not quietly
strip what another logger wrote. ADX (the XML form) is out of scope.
Integration points
| Source | What it gives the log | How |
|---|---|---|
RigState |
FREQ, BAND, MODE/SUBMODE, and the rig id a QSO was made on |
watch channel already in the frontend context |
Client config general.callsign |
STATION_CALLSIGN, OPERATOR |
already surfaced as owner_callsign in frontend meta |
| Rig latitude/longitude | MY_GRIDSQUARE |
latLonToMaidenhead, already in the frontend |
| Decoder panels and map | a pre-filled entry: callsign, grid, and the report to offer | existing decode history; no new plumbing |
bandForHz |
BAND from a frequency |
exists in map-core.ts; move to a shared module |
HTTP API
| Method | Path | Purpose |
|---|---|---|
GET |
/logbook |
Query: filters, paging |
POST |
/logbook |
Add a QSO |
PUT |
/logbook/{id} |
Edit |
DELETE |
/logbook/{id} |
Delete |
GET |
/logbook/export.adi |
Export, honouring the current filter |
POST |
/logbook/import |
Import, answering with counts: added, duplicate, rejected |
GET |
/logbook/worked/{call} |
Worked-before: bands and modes |
Writes require the control role, as the rig endpoints do.
Frontend
A Logbook tab joins the tab order: an entry form that opens pre-filled, a table with the filters of REQ-LOG-005, and import/export. Worked-before shows against the callsign as it is typed.
The ham layout is a fifth entry in the operator layouts (compact, broadcast, digital,
full), which already gate on capability, seed the disclosure sections and persist per rig:
ham: {
label: "Ham radio",
unavailable: "Ham radio needs a rig that can transmit",
advanced: true, audio: true, scheduler: false,
preferredTab: "logbook", capability: "ham",
}
with the ham capability set from RigCapabilities.tx. It keeps frequency, VFO, mode, filter,
PTT, power and the meters; it hides the broadcast furniture; and it puts the log beside the
spectrum rather than a tab away.
Phases
| Phase | Lands |
|---|---|
| 1 | trx-logbook: Qso, the ADI reader and writer, round-trip tests against files from other loggers |
| 2 | Store, dedupe, and the HTTP API behind the control role |
| 3 | Logbook tab: entry, table, filters, import, export |
| 4 | Ham layout, pre-filled entry from a decode row or the map, worked-before |
| 5 | Optional: QSL and LoTW/eQSL fields, contest exchange fields, per-band worked/confirmed statistics |
Open questions
- One station log, or one per rig? The proposal assumes one, with the rig id recorded on each QSO, since a callsign worked on the second rig is still worked.
- Multiple operators at one station:
OPERATORper QSO, or per session? - Clock: the client's or the server's? The server's is proposed — it is the machine at the radio — with the offset shown if the browser disagrees by more than a second.
- Should the log file be configurable, or fixed beside the bookmarks?
- Import collisions: the proposed key is callsign, band, mode and time to the minute. Contest operators work the same station twice in a minute on different bands, which that key allows; a same-band dupe inside a minute is treated as the same QSO.