Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a20fd260 | ||
|
|
ca5cd65c85 | ||
|
|
a9e1e86fdc | ||
|
|
76e33a91bb |
@@ -322,3 +322,255 @@ trx-configurator
|
||||
| `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 /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:
|
||||
|
||||
```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
|
||||
|
||||
| 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.
|
||||
|
||||
@@ -519,6 +519,30 @@ most the contact being written. It lives in your data directory by default;
|
||||
`[trx-client.logbook].path` moves it, for a station that keeps its log on a
|
||||
backed-up volume.
|
||||
|
||||
### Contests
|
||||
|
||||
The **Contest exchange** block on the entry holds the contest's name and the
|
||||
serials — sent and received. Both stay between contacts, because they belong to
|
||||
the session rather than to the contact just logged, and the serial sent counts
|
||||
on by itself so it is not retyped forty times an hour. An exchange that is a
|
||||
zone, a section or a name rather than a number is kept as written.
|
||||
|
||||
**Contest entry (Cabrillo)** exports the entry sponsors accept. Only the
|
||||
contacts of the contest named there are included. The header — operator
|
||||
category, power, claimed score — cannot be worked out from a log, so it is
|
||||
yours to fill in; the defaults are single operator, low power, all bands, mixed
|
||||
mode.
|
||||
|
||||
### Confirmations
|
||||
|
||||
The **QSL** column shows a tick when the other station has confirmed, and the
|
||||
**Confirm** button on a row records a card that has arrived. A confirmation
|
||||
counts from wherever it came: a paper card, LoTW or eQSL. An award wants one of
|
||||
them, not all three, so the log does not ask for all three.
|
||||
|
||||
**Bands worked** counts, per band, the contacts made, the distinct stations
|
||||
worked, and how many of those contacts are confirmed.
|
||||
|
||||
### Ham radio layout
|
||||
|
||||
The **Ham radio** operator layout opens on the logbook with the transceiver
|
||||
|
||||
Reference in New Issue
Block a user