# 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.