Files
id5100/.claude/handoffs/2026-06-14-144031-id5100-civ-control-suite.md
T

173 lines
7.5 KiB
Markdown

# Handoff: ID-5100 CI-V control suite (library + CLI + web panel)
## Session Metadata
- Created: 2026-06-14 14:40:31
- Project: /home/claude/bin/id5100
- Branch: [not a git repo — ~/bin is not under git]
- Session duration: ~1 session
### Recent Commits (for context)
- [not a git repo — no commits]
## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
> This is the first handoff for this task.
## Current State Summary
Built a complete software control layer for Seth's ICOM ID-5100 mobile transceiver
over CI-V, from nothing to a working web control panel, in one session. Started by
verifying the hardware link (RT Systems USB-29A cable into the radio's [SP2] jack),
then built a pure-stdlib codec, a serial transport, a CLI, and an industrial-themed
web panel. **Everything is verified against the live radio** — reads, writes, and the
UI screenshot. The web server is running now and reachable on the LAN. The project is
in a clean, finished-MVP state; remaining items are optional enhancements, not gaps.
## Codebase Understanding
### Architecture Overview
Three clean layers, each a front-end on the one below:
- **Codec** (`civ.py`) — pure functions, no I/O: BCD freq/level encode-decode, CI-V
frame build/parse. Fully unit-tested (11 tests).
- **Transport** (`radio.py`) — `Radio` class over pure-stdlib `termios` (no pyserial).
get/set frequency, mode, volume, squelch level, S-meter, squelch status.
- **Front-ends** — `cli.py` (argparse) and `server.py` + `panel.html` (web). Both just
call `radio.Radio`.
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| `civ.py` | Pure CI-V codec (BCD, frames) | Core; change here = re-run test_civ.py |
| `radio.py` | Serial transport + `Radio` class | All control flows through this |
| `cli.py` | CLI front end | `./cli.py status` etc. |
| `server.py` | Web panel HTTP server + JSON API | Holds the serial port while running |
| `panel.html` | Instrument-panel UI (one self-contained file) | Amber VFD aesthetic |
| `test_civ.py` | 11 codec unit tests | `python3 -m unittest test_civ` |
| `civ_probe.py` | One-shot link probe | Quick "is the radio alive" check |
| `README.md`, `DECISIONS.md` | Docs + decision log | Read first when resuming |
### Key Patterns Discovered
- **Verified link params:** `/dev/ttyUSB0`, **9600 baud**, radio CI-V address **0x8C**,
controller address 0xE0.
- **Bus echo:** the radio echoes your TX frame before its reply; `transact()` filters
frames whose `frm != RADIO_ADDR` (0x8C) to skip the echo.
- **Two BCD byte orders on the same radio:** frequency is 5-byte **little-endian** BCD;
levels/meters are 2-byte **big-endian** BCD (128 -> `01 28`). Pinned by unit tests.
- **Mode FM = 0x05** confirmed empirically (radio returned `05 01`).
## Work Completed
### Tasks Finished
- [x] Verified PC<->radio CI-V link (read VFO = 147.300000 MHz)
- [x] TDD'd pure codec `civ.py` (11 tests green, grounded in real capture)
- [x] Built transport `radio.py`, ground-truthed all reads + write path on live radio
- [x] Optimized `transact()` early-return (status poll ~3.6s -> ~0.2s)
- [x] Built CLI `cli.py` (status/freq/mode/vol/sql/smeter)
- [x] Built web panel `server.py` + `panel.html`, verified via headless screenshot + control POST
- [x] Added `ID-5100 CI-V` row to ~/bin/CLAUDE.md projects table (backed up first)
- [x] Wrote README.md + DECISIONS.md
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| /home/claude/bin/CLAUDE.md | +1 projects-table row | Register the project (backup: .backup/CLAUDE.md-1781461768) |
| (all id5100/* files) | Created this session | The deliverable |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Pure stdlib `termios` | pyserial | System Python is PEP-668 locked; --break-system-packages violates escalation-brake rule |
| stdlib `http.server` for web | Flask/FastAPI | Keep no-deps promise |
| Early-return `transact()` | fixed timeout | Makes ~1 Hz live S-meter polling viable |
| Industrial VFD aesthetic | generic dashboard | Matches radio-gear domain + Sethian dark/orange brand |
| Reject full headless-head replica | reverse-engineer head bus | Proprietary/undocumented; CI-V covers operating functions |
(Full list in DECISIONS.md.)
## Pending Work
## Immediate Next Steps
1. **(Optional) systemd user unit for `server.py`** — it's currently a bare background
process (won't survive reboot/logout). This is the most likely next ask.
2. **(Optional) Vendor the web fonts locally** — `panel.html` pulls DSEG7 (jsdelivr) +
Saira/Chivo Mono (Google Fonts) from CDNs; offline it falls back to monospace.
3. **(Optional) Verify DV mode (0x17) on the live radio** — currently from the generic
ICOM table, flagged not-trusted. Test `./cli.py mode dv` and confirm the radio ACKs.
### Blockers/Open Questions
- [ ] None blocking. Open question: does Seth want this behind Caddy with a subdomain
(e.g. `radio.sethpc.xyz`) like his other services? Not discussed.
### Deferred Items
- Full software control-head replica (proprietary head bus) — rejected, see DECISIONS.md.
- RS-MS1A data-jack protocol (D-STAR DR routing / callsign / messaging) — out of scope
for this CI-V layer; separate effort if wanted.
- HA/MQTT bridge — offered, not chosen this session.
## Context for Resuming Agent
## Important Context
The whole suite is **verified against real hardware**, not just written. If resuming,
you can trust the link params and the codec. The radio is a physical device on Seth's
desk — **set-commands change real radio state**, so prefer no-op writes (write current
value back) when testing the write path, as was done here. The only process that can
hold `/dev/ttyUSB0` is one at a time: **stop `server.py` before running `cli.py`** or
the probe, and vice versa.
### Assumptions Made
- The radio stays on `/dev/ttyUSB0` (single USB-serial device; confirmed by Seth).
- CI-V baud stays 9600 (Seth set this in the radio menu this session).
- The PC hosting all this is steel141 (192.168.0.141).
### Potential Gotchas
- **Don't `--break-system-packages`** — stdlib-only is a deliberate decision.
- **Bus echo** will confuse any new raw-serial code; reuse `civ.parse_frames` + the
`frm == RADIO_ADDR` filter.
- **DV=0x17 is unverified** — don't present `set_mode('dv')` as trusted yet.
- The web panel needs internet for the nice fonts (graceful monospace fallback otherwise).
## Environment State
### Tools/Services Used
- RT Systems USB-29A cable (FTDI), enumerates as `/dev/serial/by-id/usb-RT_Systems_CT29A_Radio_Cable_RT90581N-if00-port0` -> `/dev/ttyUSB0`
- `claude` user is in `dialout` group (serial access OK)
- chromium (headless) used for the verification screenshot
- Python 3.13 (system, PEP-668 externally-managed)
### Active Processes
- **`server.py --port 8550` is RUNNING** (was PID 380991 this session) at
**http://192.168.0.141:8550/** (bound 0.0.0.0, LAN/phone reachable). Log at
`id5100/.server.log`. It holds `/dev/ttyUSB0`. Kill with `pkill -f "server.py --port 8550"`.
### Environment Variables
- None specific to this project. (Homelab-wide: `HOMELAB_PASSWORD` exists but unused here.)
## Related Resources
- `id5100/README.md` — usage + verified-facts
- `id5100/DECISIONS.md` — decision log incl. Deferred/Rejected
- `~/bin/CLAUDE.md` — projects table entry
- ICOM ID-5100 full manual CI-V section (p.305) — frame format / address 0x8C
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.