The logbook of issue #54, in the shape the proposal settled on. A new crate, trx-logbook, holds the contact, the ADIF reader and writer, the file, and the rules for telling one contact from two. ADIF because it is the only thing the ecosystem reads: LoTW, eQSL, Club Log, QRZ and every other logger take it and nothing else, so a log that cannot write .adi cannot be uploaded, confirmed or moved. The reader is forgiving in the ways real files are irregular -- lowercase tags, CRLF, missing header, unknown fields, a declared length that is the only thing ending a value -- and carries what it does not model through to the export, so a round trip does not strip what another program wrote. The file is JSON Lines, appended one line per contact. A log is the one thing here that cannot be regenerated, and the bookmark store's whole-file dump would rewrite megabytes to log one contact and lose all of them if the power went halfway; an append costs the record being written and no more, which a test tears a line in half to prove. Edits append revisions, deletes append tombstones, and the file compacts when the superseded outnumber the live. The panel is its own tab and stands in every layout. An entry opens with six fields and no more -- frequency, mode, rig name, time, and the callsign and locator of whatever decode it was started from. A report stays empty: an FT8 SNR is not what was sent. Times come from the server, because the browser may be a phone in another timezone, and the panel says so when the two disagree by more than a second. Worked-before answers as a callsign is typed. A decode is not a contact, so the Log button on an FT8 or APRS row opens an entry and logs nothing by itself. The ham layout is the fifth operator layout, opening on the logbook with the radio controls around it, offered only where the rig can transmit. Two bugs found on the way, both in code written here: a frequency of a whole number of megahertz ending in a zero rendered as a tenth of itself, in Rust and in TypeScript alike, because trimming trailing zeros from "20.000000" walks back through the point. The API also sits under /api/logbook rather than /logbook, so it cannot shadow its own page the way /bookmarks does. Closes #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>
7.4 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
# Build
cargo build --release
# Run tests
cargo test
# Lint
cargo clippy
# Run a single test (by name pattern)
cargo test <test_name>
# Run tests for a specific crate
cargo test -p trx-core
# Generate example config
./target/release/trx-server --print-config > trx-server.toml
./target/release/trx-client --print-config > trx-client.toml
# Validate a config without starting anything (reports every problem)
./target/release/trx-server --check-config --config trx-rs.toml
# Regenerate trx-rs.toml.example after changing a config struct
cargo run -p trx-config --example generate_example
# Run server
./target/release/trx-server --config trx-server.toml
# or via CLI args:
./target/release/trx-server -r ft817 "/dev/ttyUSB0 9600"
# Run client
./target/release/trx-client --config trx-client.toml
Crate Layout
This is a Cargo workspace. All crates live under src/:
src/
trx-core/ # Core types, traits, state machine, controller (~3,500 LOC)
trx-protocol/ # Client↔server protocol DTOs, auth, codec, mapping (~1,100 LOC)
trx-app/ # Shared application helpers (logging init, name normalization)
trx-config/ # Client + server config structs, loader, validators (~2,500 LOC)
trx-reporting/ # PSKReporter UDP uplink + APRS-IS TCP uplink (~1,150 LOC)
trx-server/ # Server binary: rig_task, audio pipeline, listener (~3,700 LOC)
trx-backend/ # Backend abstraction trait + factory + dummy
trx-backend-ft817/ # Yaesu FT-817 binary CAT (BCD encoding)
trx-backend-ft450d/ # Yaesu FT-450D ASCII CAT
trx-backend-soapysdr/ # SoapySDR RX with full DSP pipeline (~5,000+ LOC)
trx-client/ # Client binary: remote connection, frontend spawning (~1,500 LOC)
trx-logbook/ # Station log: QSO record, ADIF reader/writer, append-only store
trx-frontend/ # Frontend trait (FrontendSpawner), runtime context
trx-frontend-http/ # Web UI: REST API, SSE, WebSocket audio, session auth
trx-frontend-http-json/ # JSON-over-TCP control frontend
trx-frontend-rigctl/ # Hamlib-compatible rigctl TCP interface
trx-configurator/ # Interactive setup wizard
decoders/
trx-aprs/ # APRS packet decoder (AX.25 + APRS-IS)
trx-cw/ # CW (Morse) decoder (Goertzel tone detection)
trx-ftx/ # Pure Rust FTx decoder (FT8/FT4/FT2, LDPC/OSD) (~3,000+ LOC)
trx-wspr/ # WSPR weak-signal decoder
trx-ais/ # AIS maritime transponder decoder
trx-rds/ # RDS decoder for WFM (~2,000 LOC)
trx-vdes/ # VDES maritime data exchange decoder (~1,300 LOC)
trx-decode-log/ # JSON Lines file logging with date rotation
Architecture
The project is split into a server (connects to the radio hardware) and a client (exposes user-facing frontends). They communicate over a JSON TCP connection (default port 4530). Audio streams over a separate TCP connection (default port 4531) using Opus encoding.
Data flow
graph TD
Radio["Radio Hardware"] <-->|serial / TCP| Server["trx-server (rig_task.rs)"]
Server <-->|"JSON-TCP :4530"| Client["trx-client (remote_client.rs)"]
Server -->|"Opus-TCP :4531"| Client
Client <-->|internal channels| F1["HTTP Frontend :8080"]
Client <-->|internal channels| F2["rigctl Frontend :4532"]
Client <-->|internal channels| F3["JSON-TCP Frontend"]
trx-core controller
The rig controller (src/trx-core/src/rig/controller/) is the central state management component:
machine.rs—RigMachineStateenum with states:Disconnected,Connecting,Initializing,PoweredOff,Ready,Transmitting,Errorhandlers.rs—RigCommandHandlertrait; commands:SetFreq,SetMode,SetPtt,PowerOn/Off,ToggleVfo,Lock/Unlock,GetSnapshot, etc.events.rs—RigListenertrait andRigEventEmitterfor broadcasting frequency/mode/PTT/state/meter/lock/power changespolicies.rs—RetryPolicy(ExponentialBackoff,FixedDelay,NoRetry) andPollingPolicy(AdaptivePolling,FixedPolling,NoPolling)
Decoders
Signal decoders run as background tasks in trx-server, consuming decoded audio. trx-ftx provides the FT8/FT4/FT2 decoder in pure Rust. Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via trx-decode-log.
Diagrams
Always use Mermaid for diagrams in Markdown files. Never use ASCII art, box-drawing characters, or plain-text diagrams. GitHub renders Mermaid natively in ```mermaid fenced code blocks.
Commit Format
[<type>](<crate>): <description>
Types: feat, fix, docs, style, refactor, test, chore. Use (trx-rs) for repo-wide changes. Sign commits with git commit -s. Write isolated commits per crate.
Codebase Review Observations
Full architecture documentation: docs/Architecture.md
Improvement plan: docs/Improvement-Areas.md
Last reviewed: 2026-03-29
Strengths
- Explicit state machine:
RigMachineStateFSM (7 states) prevents invalid states with a deterministic transition table and exhaustive matching. Well-tested with lifecycle, error recovery, and invalid transition tests.ReadyStateData/TransmittingStateDatausepub(crate)fields with controlled accessors. - Trait-based polymorphism: Clean abstraction boundaries (
RigCat,RigSdr,AudioSource,RigListener,RigCommandHandler,CommandExecutor,TokenValidator,FrontendSpawner) enable loose coupling and testability.RigCat/RigSdrsplit cleanly separates CAT ops from SDR-specific methods. - Multi-rig architecture: Per-rig task isolation with
HashMap<rig_id, RigHandle>routing, per-rig state/spectrum/audio/decoder-history channels, dual-connection model (main + spectrum) in the client, and backward-compatible single-rig mode. - Async concurrency model: Proper use of tokio channels --
watchfor state snapshots,broadcastfor PCM/decode fan-out,mpscfor commands. No mutex contention on hot paths. Spectrum deduplication collapses concurrent GetSpectrum requests. - Comprehensive SDR support: Full DSP pipeline with multi-mode demodulation (SSB, AM, SAM, FM, WFM, AIS, VDES), virtual channel management, squelch, noise blanker, spectrum FFT, RDS decoding. AVX2-optimized FM discriminator with scalar fallbacks.
- Pure Rust decoders: FT8/FT4/FT2, APRS, CW, WSPR, AIS, VDES, RDS -- all implemented without C FFI dependencies. Consistent decoder pattern: stateful struct →
process_block()→decode_if_ready(). - Good test coverage in protocol layer: codec, mapping, auth all have thorough unit tests with round-trip verification. 45+ mapping tests cover all command variants.
- Feature-gated backends: ft817, ft450d, soapysdr compiled conditionally to minimize binary size. Factory pattern with name normalization for registration.
- Defensive error handling: Lock poisoning recovery, stream error deduplication with 60s summaries, input truncation in logs (128 chars), per-IP rate limiting on auth endpoints.
- Well-documented DSP guidelines:
docs/Optimization-Guidelines.mdcaptures lessons on NCO design, polyphase resampling, AVX2 batching, and stereo FM decoding.
Areas for Improvement
All P0–P3 items resolved or dropped. See docs/Improvement-Areas.md for details.