The clock is the server's, as asked: it is the machine at the radio, where the browser may be a phone in another timezone with a clock nobody checked. When the two disagree by more than a second the panel says so, rather than logging a time the operator did not expect. The rest, decided against how logging is actually done: One station log, not one per rig. DXCC, WAS and LoTW count the callsign, not the radio, and a station worked on the second rig is still worked. The rig goes on the QSO as MY_RIG. Station location does follow the rig, though — these rigs can be in different places, so MY_GRIDSQUARE comes from the one that made the contact, which is what LoTW's station locations expect too. The operator is a per-QSO field set once per session. ADIF separates the callsign used on the air from the person at the key, and multi-operator stations rotate people through one station callsign. It defaults from the configured callsign, so a single operator never touches it. It cannot come from the session: the auth roles are control and rx, with nobody's name on them. The log file is configurable, defaulting to the user's data directory. Bookmarks sit in the config directory because they are settings and decode logs in the cache directory because they are disposable; a QSO log is irreplaceable, and cache directories get swept. Import collisions match on callsign, band, mode and a two-minute window. Loggers rarely agree to the second on the same QSO — one stamps the contact, the other the entry — so an exact-minute key duplicates half of what it is asked to merge. Two minutes absorbs that without swallowing a legitimate re-work, since contest rules forbid a second contact on the same band and mode. Times compare as instants so midnight matches, and modes are normalised or an imported SSB would miss our USB. That normalisation is now written down: a rig mode is not an ADIF mode. DIG is the one the rig cannot answer — a rig in DIG is in FT8 or FT4 depending on what is decoding — and WSPR never opens an entry at all, because hearing a beacon is not a contact. 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>
28 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 in a panel of its own, and an operator layout that puts a transceiver's controls around it.
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 starting a log entry, the system shall pre-fill exactly six fields: frequency, mode, rig name, time, callsign and locator. |
| REQ-LOG-003 | The system shall leave every other field of a log entry empty for the operator to fill. |
| REQ-LOG-004 | The system shall allow a logged QSO to be edited and deleted. |
| REQ-LOG-005 | The system shall survive a crash without losing a QSO that was recorded before it. |
| REQ-LOG-006 | The system shall list, search and filter the log by callsign, band, mode and date range. |
| REQ-LOG-007 | 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-008 | The system shall show whether a callsign has been worked before, and on which bands. |
| REQ-FMT-001 | The system shall export the log as an ADIF 3.1.x .adi file. |
| REQ-FMT-002 | The system shall import ADIF .adi files produced by other logging software, preserving fields it does not itself use. |
| REQ-FMT-003 | When importing, the system shall identify QSOs already held and shall not duplicate them. |
| REQ-FMT-004 | The system shall export a filtered selection of the log as a Cabrillo 3.0 file for contest submission. |
| REQ-LOG-009 | The system shall stamp QSO times from the server's clock in UTC, and shall tell the operator when the browser's clock disagrees with it by more than one second. |
| REQ-LAY-001 | The system shall present the logbook in a panel of its own, reachable whatever layout is selected. |
| REQ-LAY-002 | The system shall offer a "Ham radio" operator layout presenting the transceiver controls and that panel together. |
| REQ-LAY-003 | 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-007 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.
What is pre-filled, and what is not
Six fields, and no more (REQ-LOG-002, REQ-LOG-003):
| Field | From | ADIF |
|---|---|---|
| Frequency | the selected rig's dial | FREQ, with BAND derived from it |
| Mode | the selected rig | MODE, and SUBMODE where the mode implies one |
| Rig name | the rig's display name | MY_RIG |
| Time | the server's clock, UTC, when the entry opens | QSO_DATE, TIME_ON |
| Callsign | the decode row or map station the entry was started from, else empty | CALL |
| Locator | that station's grid, where the decode carried one, else empty | GRIDSQUARE |
Signal reports, power, name, QTH and the rest stay empty. A report in particular is the operator's to give: an FT8 SNR is not what was sent, and pre-filling one would put a number in the log that nobody exchanged.
The station's own callsign and locator are not pre-filled per entry either — they are station
identity, taken from configuration when the QSO is written (STATION_CALLSIGN, OPERATOR,
MY_GRIDSQUARE), and shown once at the top of the panel rather than typed into every row.
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 — 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.
Two formats, for the two things a log is asked for
The file formats were left open ("pick a well-known ham format"), so: ADIF for interchange, Cabrillo for contest submission. Both are implemented in-repo, in the way this project already implements its decoders, and neither adds a dependency.
ADIF has to stay. It is not one option among several — it is the only thing the ecosystem
reads. LoTW, eQSL, Club Log, QRZ.com and every other logger take ADIF and nothing else, so a
log that cannot write .adi cannot be uploaded, confirmed, or moved to another program. That
is a one-way door, and the interoperability is most of the point of keeping a log at all.
Nothing on disk is ADI regardless: the store is JSON Lines, and ADIF is what comes out of an
export.
ADI is a tagged text format — <FIELD:length>value, records ended by <EOR>, a header ended
by <EOH>, everything outside a tag ignored — small enough to implement exactly. 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 serialisation of the same data model, is out of scope: it is part
of the standard but almost nothing reads it.
Cabrillo is the second format, because ADIF cannot do its job. Contest logs are submitted
to sponsors in Cabrillo 3.0 and are rejected in anything else — a header of CALLSIGN:,
CONTEST:, CATEGORY-* and CLAIMED-SCORE: lines, then one fixed-column QSO: line per
contact carrying frequency in kHz, a mode code (CW, PH, FM, RY, DG), the UTC date and
time, and both stations' calls, reports and exchanges. It is export-only and drops everything
outside the contest's exchange, which is why it complements ADIF rather than replacing it.
It arrives with the contest exchange fields in phase 5, since without a serial or a zone to
put in the exchange there is nothing for it to write.
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, and the default OPERATOR |
already surfaced as owner_callsign in frontend meta |
| The QSO's own rig, and its position | MY_GRIDSQUARE |
per-rig latitude and longitude already carried in the rig list |
| Server clock | QSO_DATE, TIME_ON in UTC |
new GET /logbook/now, which also feeds the browser-clock check |
| 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 |
ADIF export, honouring the current filter |
GET |
/logbook/export.cbr |
Cabrillo export of a contest selection |
POST |
/logbook/import |
Import, answering with counts: added, duplicate, rejected |
GET |
/logbook/worked/{call} |
Worked-before: bands and modes |
GET |
/logbook/now |
The server's UTC clock, for stamping entries and checking the browser's |
Writes require the control role, as the rig endpoints do.
Frontend
The logbook is its own panel, not a strip bolted to the radio page: a logbook entry in
the tab order beside Bookmarks, holding the entry form, the table with the filters of
REQ-LOG-006, and import and export. It stands on its own in every layout, so a log can be kept
without adopting the ham layout, and read while another layout is selected (REQ-LAY-001).
The panel is three parts: the station line at the top (own callsign, locator, the rig a QSO would be logged against), the entry form beneath it opening with the six pre-filled fields, and the log itself under that, filtered as REQ-LOG-006 asks. 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, and hides the broadcast furniture. What it adds over full is where
it starts: the logbook panel, with the radio controls a keystroke away rather than the other
way round — the layout an operator working the bands wants, where logging the contact is the
task and the rig is the instrument.
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 | Contest exchange fields and Cabrillo export; QSL and LoTW/eQSL fields; per-band worked/confirmed statistics |
Decisions
One station log, not one per rig. Awards and uploads are per station callsign — DXCC, WAS
and LoTW all count the callsign, not the radio — and a station worked on the second rig is
still worked. The rig is recorded on the QSO (MY_RIG) rather than dividing the log by it.
The station location fields do follow the rig, though: trx-rs rigs can be in different
places, so MY_GRIDSQUARE is taken from the rig that made the QSO rather than from one global
setting, which is also what LoTW's station locations expect.
The operator is a per-QSO field, set once per session. ADIF separates STATION_CALLSIGN
(the call used on the air) from OPERATOR (the person at the key); multi-operator stations
rotate operators through one station callsign, which is why contest loggers record it per QSO.
It is stored per QSO, defaulted from the configured callsign so a single operator never touches
it, and changed on the station line at the top of the panel where it sticks for the session.
It cannot be taken from the session's identity: the auth roles are control and rx, with no
notion of who is logged in.
Server clock, and the log says so. The server is the machine at the radio; the browser may be on a phone in another timezone with a clock nobody has checked. QSO times are UTC from the server, and when a browser's clock disagrees by more than a second the panel says so rather than silently logging a time the operator did not expect.
The log file is configurable, and defaults to the user's data directory. Bookmarks live in
the config directory because they are settings; decode logs live in the cache directory because
they are disposable. A QSO log is neither — it is irreplaceable, and cache directories are
swept by cleaners. [logbook].path in the client config, defaulting to
dirs::data_dir()/trx-rs/logbook.jsonl, so a station that keeps its log on a synced or
backed-up volume can say so.
Import collisions: callsign, band, mode and a two-minute window. Two loggers rarely agree
to the second on the same QSO — one records the time the contact started, another the time it
was entered — so an exact-minute key duplicates half of what it is asked to merge. Two minutes
absorbs that. It does not swallow legitimate re-works: contest rules forbid a second contact
with the same station on the same band and mode, so a repeat inside two minutes is the same
QSO. Times are compared as instants rather than date and time strings, so a QSO either side of
midnight matches. Modes are normalised before comparison, or a log that stored SSB would fail
to match ours that stored USB.
Rig modes to ADIF modes
The rig reports what it is demodulating; ADIF wants what the contact was made on, which is not always the same word:
| Rig mode | ADIF MODE |
ADIF SUBMODE |
|---|---|---|
USB, LSB |
SSB |
USB / LSB |
CW, CWR |
CW |
— |
AM, SAM |
AM |
— |
FM, WFM |
FM |
— |
PKT |
PKT |
— |
DIG |
decided by the decoder in use, not by the rig | |
AIS, VDES |
none — not amateur modes, and these rigs do not log | |
Other(..) |
passed through when it names an ADIF mode, else left for the operator |
DIG is the one that cannot come from the rig: a rig in DIG is in FT8, FT4 or something else
depending on which decoder is running, and an entry started from an FT8 row logs FT8 rather
than the rig's word for it. WSPR never opens an entry at all — it is a beacon mode, and hearing
a beacon is not a contact.
The table is data in qso.rs, checked against the ADIF enumeration when it is written, with
anything unrecognised left to the operator rather than guessed into the log.