CI / frontend (pull_request) Successful in 5m13s
CI / reuse (pull_request) Successful in 29s
CI / frontend (push) Successful in 4m15s
CI / reuse (push) Successful in 5s
CI / lint (pull_request) Successful in 2m25s
CI / test (pull_request) Successful in 9m24s
CI / lint (push) Successful in 2m23s
CI / test (push) Successful in 8m19s
591 lines
29 KiB
Markdown
591 lines
29 KiB
Markdown
# 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:
|
|
1. Passing a `RecorderHandle` (cheap `Arc` wrapper) into the audio and rig tasks.
|
|
2. Adding `RecorderCommand` variants to the command enum (alongside existing `SetFreq`, `SetMode`, etc.).
|
|
3. Adding a `[recorder]` section to `ServerConfig`.
|
|
|
|
---
|
|
|
|
### 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 `opus` crate (already a workspace dependency via `trx-backend-soapysdr`). Seek index (`index.bin`) provides byte → time mapping.
|
|
- **Channel count**: determined at session open from `AudioConfig.channels`. If `channels == 1` → mono; if `channels == 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):
|
|
|
|
```jsonl
|
|
{"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]`:
|
|
|
|
```toml
|
|
[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`):
|
|
|
|
```rust
|
|
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:
|
|
|
|
1. Reads `audio.opus` and decodes PCM frames in real time.
|
|
2. Publishes decoded PCM frames onto a `broadcast::Sender<Vec<f32>>` — the **same channel type** as the live `pcm_tx`, so existing decoder tasks and audio-streaming clients receive playback data transparently.
|
|
3. Replays `data.jsonl` events on their original `offset_ms` timestamps, injecting them into the `DecodedMessage` broadcast so the HTTP frontend displays historic decodes during playback.
|
|
4. For seek: binary-searches `index.bin` to 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:
|
|
|
|
```rust
|
|
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)
|
|
|
|
1. Add `trx-recorder` crate skeleton; `RecorderConfig`; `RecorderHandle`.
|
|
2. Implement `AudioWriter` with Opus output.
|
|
3. Subscribe `AudioWriter` to `pcm_tx` in `audio.rs`; open session on `StartRecording` command.
|
|
4. Auto-detect channel count from `AudioConfig.channels`.
|
|
|
|
#### Phase 2 — Metadata recording (REQ-REC-004, REQ-REC-005, REQ-SYNC-001)
|
|
|
|
1. Implement `DataFileWriter`; define full event schema.
|
|
2. Subscribe to `DecodedMessage` broadcast; fan-in all decoder types.
|
|
3. Subscribe to state watch; emit `rig_state` events on freq/mode change.
|
|
4. Emit `fft` events at configured interval from spectrum data.
|
|
5. Write `SeekIndex` in parallel with audio.
|
|
|
|
#### Phase 3 — Cursor (REQ-REC-006)
|
|
|
|
1. Add `MarkCursor` command + HTTP endpoint.
|
|
2. Write `cursor` event to `data.jsonl` with current `offset_ms`.
|
|
|
|
#### Phase 4 — Playback (REQ-PLAY-001, REQ-PLAY-002)
|
|
|
|
1. Implement `PlaybackEngine`; Opus decode + PCM broadcast.
|
|
2. Add `PlaybackState` to `RigState`; suppress live capture during playback.
|
|
3. Implement seek via `index.bin` binary search.
|
|
4. Replay `data.jsonl` events; feed into `DecodedMessage` broadcast.
|
|
5. 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
|
|
|
|
1. **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.
|
|
2. **Session listing API**: The HTTP frontend needs an endpoint to enumerate sessions (`GET /api/recorder/sessions`). Schema TBD in Phase 4.
|
|
3. **Storage limits**: `max_session_duration_s` auto-splits sessions; a `max_total_size_gb` housekeeping 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 configuration
|
|
- `trx-client.toml` — client configuration
|
|
- `trx-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 `serialport` crate
|
|
(already a transitive dependency). Present as selectable list with device
|
|
path and description.
|
|
- **SoapySDR devices**: if built with `soapysdr` feature, call
|
|
`SoapySDR::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](https://git.haxx.space/sjg/trx-rs/issues/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 /api/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
|
|
|
|
Under `/api/logbook`, as the recorder's endpoints are, because `/logbook` itself is the page:
|
|
the `/bookmarks` API and the bookmarks page already share a path, and whichever is registered
|
|
first wins.
|
|
|
|
| Method | Path | Purpose |
|
|
|--------|------|---------|
|
|
| `GET` | `/api/logbook` | Query: filters, paging |
|
|
| `POST` | `/api/logbook` | Add a QSO |
|
|
| `PUT` | `/api/logbook/{id}` | Edit |
|
|
| `DELETE` | `/api/logbook/{id}` | Delete |
|
|
| `GET` | `/api/logbook/export.adi` | ADIF export, honouring the current filter |
|
|
| `GET` | `/api/logbook/export.cbr` | Cabrillo export of a contest selection |
|
|
| `POST` | `/api/logbook/import` | Import, answering with counts: added, duplicate, rejected |
|
|
| `GET` | `/api/logbook/worked/{call}` | Worked-before: bands and modes |
|
|
| `GET` | `/api/logbook/statistics` | Contacts, stations and confirmations, per band |
|
|
| `GET` | `/api/logbook/now` | The server's UTC clock, for checking the browser's |
|
|
| `GET` | `/api/logbook/prefill` | The six fields an entry opens with |
|
|
|
|
Writes require the admin 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:
|
|
|
|
```ts
|
|
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
|
|
|
|
All five are implemented.
|
|
|
|
| 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 admin 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 |
|
|
|
|
Two decisions phase 5 settled. The Cabrillo header cannot be derived from a log — how many
|
|
operators, how much power, what the score is claimed to be — so it comes from the operator,
|
|
with `SINGLE-OP`, `LOW`, `ALL` and `MIXED` behind it. And a confirmation counts from whichever
|
|
bureau answered: an award wants one card or one electronic match, not one of each, so a log
|
|
that counted them separately would tell the operator they were short of what they have.
|
|
|
|
### 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 `admin` and `user`, 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.
|