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

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) — Radio class over pure-stdlib termios (no pyserial). get/set frequency, mode, volume, squelch level, S-meter, squelch status.
  • Front-endscli.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

  • 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-V row 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

  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 locallypanel.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.)
  • 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.