7.5 KiB
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) —Radioclass over pure-stdlibtermios(no pyserial). get/set frequency, mode, volume, squelch level, S-meter, squelch status. - Front-ends —
cli.py(argparse) andserver.py+panel.html(web). Both just callradio.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 whosefrm != 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
- Verified PC<->radio CI-V link (read VFO = 147.300000 MHz)
- TDD'd pure codec
civ.py(11 tests green, grounded in real capture) - Built transport
radio.py, ground-truthed all reads + write path on live radio - Optimized
transact()early-return (status poll ~3.6s -> ~0.2s) - Built CLI
cli.py(status/freq/mode/vol/sql/smeter) - Built web panel
server.py+panel.html, verified via headless screenshot + control POST - Added
ID-5100 CI-Vrow to ~/bin/CLAUDE.md projects table (backed up first) - 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
- (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. - (Optional) Vendor the web fonts locally —
panel.htmlpulls DSEG7 (jsdelivr) + Saira/Chivo Mono (Google Fonts) from CDNs; offline it falls back to monospace. - (Optional) Verify DV mode (0x17) on the live radio — currently from the generic
ICOM table, flagged not-trusted. Test
./cli.py mode dvand 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+ thefrm == RADIO_ADDRfilter. - 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 claudeuser is indialoutgroup (serial access OK)- chromium (headless) used for the verification screenshot
- Python 3.13 (system, PEP-668 externally-managed)
Active Processes
server.py --port 8550is RUNNING (was PID 380991 this session) at http://192.168.0.141:8550/ (bound 0.0.0.0, LAN/phone reachable). Log atid5100/.server.log. It holds/dev/ttyUSB0. Kill withpkill -f "server.py --port 8550".
Environment Variables
- None specific to this project. (Homelab-wide:
HOMELAB_PASSWORDexists but unused here.)
Related Resources
id5100/README.md— usage + verified-factsid5100/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.