IC-7610 (Icom) — CI-V manual vs implementation¶
Manual: Icom IC-7610 CI-V Reference Guide rev 1a (2021 printing) — full command table (cmds 00–27, 1A 05 00xx menu) + command formats. Source: docs/validation/cat-audits/manuals/ic7610.txt (extracted from the official Icom Japan PDF IC-7610_ENG_CI-V_1a.pdf, retrieved via the radiomanual.info mirror — Icom's own download endpoint serves an HTML/JS shell to curl, not the PDF; the mirror's bytes are the official document).
Driver: src/rigplane/runtime/radio.py (IcomRadio + mixins) · scope in src/rigplane/runtime/_scope_runtime.py · dual-RX in src/rigplane/runtime/_dual_rx_runtime.py. Backend assembly src/rigplane/backends/icom7610/.
Profile: rigs/ic7610.toml. Validation: src/rigplane/validation/registry/*.
Live run: /tmp/ic7610.live.json — 70 checks: 59 pass · 3 fail (scope_dual.set, scope_vbw.set, scope_rbw.set) · 8 manual_required. (core 2.9.0, mode=hardware, tx_allowed, LAN 192.168.55.40:50001.)
Protocol: Icom CI-V, FE FE 98 E0 … FD framing (transceiver addr 0x98). @9 in the manual = "Command 29 supported" → dual-RX targeting via the 0x29 <00|01> prefix; the profile's [cmd29] block routes 26 commands this way. Set-then-read round-trips (RMVR) are the harness's strongest signal; many implemented controls only get a *.presence check because no RMVR is registered.
Command matrix (operator-facing set)¶
CI-V cmd · Documented S(et)/R(ead) · Backend method (or —) · Profile cap/cmd · Validation check_id + live status.
| CI-V cmd | Doc | Backend method | Profile | Validation (live) |
|---|---|---|---|---|
03 read freq / 05 set freq |
S/R | get_freq/set_freq |
get_freq/set_freq |
freq.write RMVR ✅ · freq.reverse_sync ✅ · discovery.identify ✅ |
04 read mode / 06 set mode |
S/R | get_mode/set_mode |
get_mode/set_mode |
mode.set RMVR ✅ |
02 band edge |
R | get_band_edge_freq |
band_edge cap |
band_edge.presence ✅ |
07 B0/B1 swap/equalize |
S | swap_main_sub/equalize_main_sub |
[vfo] |
(via vfo_slot.set RMVR ✅) |
07 C0/C1/C2 dualwatch |
S/R | get_dual_watch/set_dual_watch |
dual_watch cap |
dual_watch.set RMVR ✅ |
07 D0/D1/D2 main/sub select |
S/R | select_receiver/get_active_receiver |
[vfo] main/sub |
vfo_slot.set RMVR ✅ |
0F split |
S/R | get_split/set_split |
split cap |
split.set RMVR ✅ |
10 tuning step |
S/R | get_tuning_step/set_tuning_step |
tuning_step cap |
tuning_step.presence ✅ |
11 attenuator |
S/R | get_attenuator/set_attenuator |
attenuator cap |
attenuator.set RMVR ✅ |
12 00/01 ANT 1/2 + RX-ANT |
S/R | get/set_antenna_1/2, get/set_rx_antenna_* |
antenna,rx_antenna |
antenna.presence ✅ · rx_antenna.presence ✅ |
13 speech synth |
R | get_speech |
— | (front-panel; not checked) |
14 01 AF / 14 02 RF gain / 14 03 SQL |
S/R | get/set_af_level, …rf_gain, …squelch |
yes | af_level.set✅ rf_gain.set✅ squelch.set✅ (RMVR) |
14 05 APF pos |
S/R | get/set_apf_type_level |
apf cap |
apf.presence ✅ |
14 06 NR level |
S/R | get/set_nr_level |
[commands] |
(none — NR level not round-tripped) |
14 07/08 inner/outer PBT |
S/R | get/set_pbt_inner/outer |
pbt cap |
pbt.presence ✅ |
14 09 CW pitch |
S/R | get/set_cw_pitch |
cw |
(none — pitch not round-tripped) |
14 0A RF power |
S/R | get/set_rf_power |
[commands] |
(none) |
14 0B MIC gain |
S/R | get/set_mic_gain |
[commands] |
(none) |
14 0C keying speed |
S/R | get/set_key_speed |
cw |
key_speed.set RMVR ✅ |
14 0D notch filter pos |
S/R | get/set_notch_filter |
[commands] |
(none) |
14 0E COMP level |
S/R | get/set_compressor_level |
[commands] |
(none — see compressor.presence) |
14 0F break-in delay |
S/R | get/set_break_in_delay |
[commands] |
(none) |
14 12 NB level |
S/R | get/set_nb_level |
[commands] |
(none) |
14 13 DIGI-SEL shift |
S/R | get/set_digisel_shift |
[commands] |
(none) |
14 14 DRIVE gain |
S/R | get/set_drive_gain |
drive_gain cap |
drive_gain.presence ✅ |
14 15 MONI level |
S/R | get/set_monitor_gain |
[commands] |
(none) |
14 16 VOX gain |
S/R | get/set_vox_gain |
[commands] |
vox_gain.set manual_required (TX-gated) |
14 17 Anti-VOX gain |
S/R | get/set_anti_vox_gain |
[commands] |
(none) |
14 19 LCD backlight |
S/R | — | — | (device front panel) |
15 01 SQL status |
R | get_s_meter_sql_status |
meters | meters.read ✅ (S-meter) |
15 02 S-meter |
R | get_s_meter |
meters | meters.read READ ✅ |
15 05 various SQL |
R | get_various_squelch |
meters | — |
15 07 OVF |
R | get_overflow_status |
meters | — |
15 11/12/13/14/15/16 PO/SWR/ALC/COMP/Vd/Id |
R | get_power_meter/get_swr/get_alc_meter/get_comp_meter/get_vd_meter/get_id_meter |
meters | (streamed; not individually checked) |
16 02 preamp |
S/R | get/set_preamp |
preamp cap |
preamp.set RMVR ✅ |
16 12 AGC time const |
S/R | get/set_agc (speed) |
agc cap |
agc.set RMVR ✅ |
16 22 NB |
S/R | get/set_nb |
nb cap |
nb.set RMVR ✅ |
16 32 audio peak filter |
S/R | get/set_audio_peak_filter |
[commands] |
(none — polled only) |
16 40 NR |
S/R | get/set_nr |
nr cap |
nr.set RMVR ✅ |
16 41 auto notch |
S/R | get/set_auto_notch |
[commands] |
(polled; no check) |
16 42/43 repeater tone / TSQL |
S/R | get/set_repeater_tone/…tsql |
[commands] |
(cap not declared — see Gap D) |
16 44 compressor |
S/R | get/set_compressor |
compressor cap |
compressor.presence ✅ |
16 45 monitor |
S/R | get/set_monitor |
monitor cap |
monitor.presence ✅ |
16 46 VOX |
S/R | get/set_vox |
vox cap |
vox.read ✅ · vox.set manual_required |
16 47 break-in |
S/R | get/set_break_in |
break_in cap |
break_in.presence ✅ |
16 48 manual notch |
S/R | get/set_manual_notch |
notch cap |
notch.set RMVR ✅ |
16 4E DIGI-SEL |
S/R | get/set_digisel |
digisel cap |
digisel.presence ✅ |
16 4F twin peak filter |
S/R | get/set_twin_peak_filter |
twin_peak cap |
twin_peak.presence ✅ |
16 50 dial lock |
S/R | get/set_dial_lock |
dial_lock cap |
dial_lock.set RMVR ✅ |
16 53 ANT-RX I/O |
S/R | get/set_rx_antenna_* |
rx_antenna |
rx_antenna.presence ✅ |
16 56 DSP IF filter type |
S/R | get/set_filter_shape |
filter_shape cap |
filter_shape.presence ✅ |
16 57 manual notch width |
S/R | get/set_manual_notch_width |
[commands] |
(none) |
16 58 SSB TX bandwidth |
S/R | get/set_ssb_tx_bandwidth |
ssb_tx_bw cap |
ssb_tx_bw.presence ✅ |
16 5E MAIN/SUB tracking |
S/R | get/set_main_sub_tracking |
main_sub_tracking |
main_sub_tracking.presence ✅ |
16 65 IP+ |
S/R | get/set_ip_plus |
ip_plus cap |
ip_plus.presence ✅ |
17 send CW message |
S | send_cw_text/stop_cw_text |
send_cw |
(TX-gated; not checked) |
18 00/01 power off/on |
S | set_powerstat/get_powerstat |
power_on/off |
power_control.presence ✅ |
19 transceiver ID |
R | get_transceiver_id |
[commands] |
(used at discovery) |
1A 00 memory contents |
S/R | get/set_memory_contents |
— | (memory family — see Gap C note) |
1A 01 band stacking reg |
S/R | get/set_bsr |
bsr cap |
bsr.select manual_required (set-only) |
1A 03 IF filter width |
S/R | get/set_filter_width |
filter_width cap |
filter_width.set RMVR ✅ |
1A 04 AGC time const |
S/R | get/set_agc_time_constant |
[commands] |
(none) |
1A 05 0070 REF adjust |
S/R | get/set_ref_adjust |
[commands] |
(none) |
1A 05 0089/0090 USB/LAN MOD lvl |
S/R | get/set_usb_mod_level/…lan_mod_level |
[commands] |
(none — audio-path; see memory note) |
1A 05 0091-0094 DATA OFF/1/2/3 MOD |
S/R | get/set_data_off_mod_input … data3 |
[commands] |
(none; live-critical, see MEMORY.md) |
1A 05 0112 CI-V Transceive |
S/R | get/set_civ_transceive |
[commands] |
(config; not checked) |
1A 05 0114 CI-V Output (ANT) |
S/R | get/set_civ_output_ant |
[commands] |
(config) |
1A 05 0158/0159/0162 date/time/UTC |
S/R | get/set_system_date/…time/…utc_offset |
system_settings |
system_date.read✅ system_time.read✅ (READ-only) |
1A 05 0290/0291 NB depth/width |
S/R | get/set_nb_depth/…nb_width |
[commands] |
(none) |
1A 05 0292 VOX delay |
S/R | get/set_vox_delay |
[commands] |
(none) |
1A 05 0228 dot/dash ratio |
S/R | get/set_dash_ratio |
[commands] |
(none) |
1A 06 DATA mode w/ filter |
S/R | get/set_data_mode |
data_mode cap |
data_mode.presence ✅ |
1A 09 AF mute |
S/R | get/set_af_mute |
[commands] |
(none) |
1B 00/01 repeater/TSQL tone freq |
S/R | get/set_tone_freq/…tsql_freq |
[commands] |
(cap not declared — Gap D) |
1C 00 PTT (RX/TX) |
S/R | set_ptt |
ptt_on/off |
tx.ptt manual_required (TX-gated) |
1C 01 antenna tuner |
S/R | get/set_tuner_status |
tuner cap |
tuner.tune manual_required (TX-gated) |
1C 02 XFC (TX freq monitor) |
S/R | get/set_xfc_status |
xfc cap |
xfc.presence ✅ |
1C 03 read TX freq |
R | get_tx_freq_monitor |
[commands] |
(none) |
0E scan start/stop/resume |
S | scan_start/scan_stop/scan_set_* |
scan cap |
scan.presence ✅ |
21 00/01/02 RIT freq/on/ΔTX |
S/R | get/set_rit_frequency/…rit_status/…rit_tx_status |
rit,xit |
rit.set RMVR ✅ · xit.set RMVR ✅ |
25 main/sub band freq |
S/R | (via _dual_rx_runtime) get_selected_freq/unselected |
[commands] |
(covered by vfo_slot.set) |
26 operating mode+filter (both) |
S/R | (via dual_rx) get_selected_mode/unselected |
[commands] |
— |
27 10 scope ON/OFF |
S/R | enable_scope/disable_scope |
scope cap |
scope.capture manual_required |
27 11 scope data output |
S/R | scope_stream |
[commands] |
(streamed) |
27 12 main/sub scope |
S/R | get/set_scope_receiver |
scope | scope_receiver.set RMVR ✅ |
27 13 single/dual scope |
S/R | get/set_scope_dual |
scope | scope_dual.set RMVR ❌ FAIL |
27 14 scope mode |
S/R | get/set_scope_mode |
scope | scope_mode.set RMVR ✅ |
27 15 span |
S/R | get/set_scope_span |
scope | scope_span.set RMVR ✅ |
27 16 edge number |
S/R | get/set_scope_edge |
scope | scope_edge.set RMVR ✅ |
27 17 scope hold |
S/R | get/set_scope_hold |
scope | scope_hold.set RMVR ✅ |
27 19 reference level |
S/R | get/set_scope_ref |
scope [scope] |
scope_ref.set RMVR ✅ |
27 1A sweep speed |
S/R | get/set_scope_speed |
scope | scope_speed.set RMVR ✅ |
27 1B scope during TX |
S/R | get/set_scope_during_tx |
scope | scope_during_tx.set RMVR ✅ |
27 1C center type |
S/R | get/set_scope_center_type |
scope | scope_center_type.set RMVR ✅ |
27 1D VBW |
S/R | get/set_scope_vbw |
scope | scope_vbw.set RMVR ❌ FAIL |
27 1E fixed edge freqs |
S/R | get/set_scope_fixed_edge |
scope | scope_fixed_edge.read READ ✅ |
27 1F RBW |
S/R | get/set_scope_rbw |
scope | scope_rbw.set RMVR ❌ FAIL |
27 20 marker position |
S/R | — | — | (NEW — see Gap C) |
Intentionally OUT OF SCOPE (device front panel / non-operator — nothing to mirror into a browser UI)¶
13speech synthesizer (voice readout — front panel),14 19LCD backlight,1A 02memory keyer contents (CW message store — feature, not a live control).1A 05menu tail (0022–0309, hundreds of params): beep/tone-control EQ, RTTY/PSK decode, recorder, keyboard/mouse, network (DHCP/IP/ports), display (LCD/LED/meter type/screen saver), scope cosmetics (waterfall colour/size/averaging/fixed-edge memory), antenna memory, USB SEND/keying routing. These are Set-mode configuration, not operator controls — out of harness scope by design (same policy as FTX-1's EX menu / Table 3).1ETX band-edge enumeration (regulatory band table — read-only metadata).- Memory channel family (
08/09/0A/0B/1A 00) — backend methods exist (get/set_memory_mode,memory_write/to_vfo/clear,get/set_memory_contents) but memory ops are a deferred UI feature, not a CAT round-trip surface.
Gap lists (priority-ordered)¶
A. UNDER-DECLARED — backend implements, profile/registry can't round-trip it¶
The IC-7610 driver is very complete (≈150 get_/set_ methods). The recurring gap: a method exists and the profile declares either a [capabilities] feature or a [commands] string, but no RMVR check is registered — so the control is invisible to validation beyond, at best, a *.presence ping. Highest-value missing RMVRs (all backend + profile present, just no registry entry):
| Control | Backend (verified) | Why it matters | Ticket |
|---|---|---|---|
RF power 14 0A |
get/set_rf_power |
core TX control, fully readable/writable, zero round-trip | NEW |
MIC gain 14 0B |
get/set_mic_gain |
primary TX-audio control | NEW |
COMP level 14 0E |
get/set_compressor_level |
only compressor.presence exists (on/off), level unchecked |
NEW |
NR level / NB level 14 06/14 12 |
get/set_nr_level/…nb_level |
on/off has RMVR; the level (the operator-visible slider) does not | NEW |
CW pitch 14 09 |
get/set_cw_pitch |
RMVR-friendly (300–900 Hz), CW op control | NEW |
notch filter pos / manual-notch width 14 0D/16 57 |
get/set_notch_filter, …manual_notch_width |
notch.set only round-trips the on/off, not position/width |
NEW |
monitor / drive / anti-VOX gain 14 15/14 14/14 17 |
resp. methods | drive_gain only *.presence; monitor/anti-VOX no check |
NEW |
PBT inner/outer 14 07/08 |
get/set_pbt_inner/outer |
pbt.presence only; pos is RMVR-able (0–255) |
NEW |
AGC time constant 16 12/1A 04 |
get/set_agc, get/set_agc_time_constant |
agc.set round-trips speed; the 1A 04 selected-filter time-constant path is unchecked |
NEW |
AF mute 1A 09 |
get/set_af_mute |
clean boolean RMVR, declared in [commands], no check |
NEW |
USB/LAN/DATA MOD inputs 1A 05 0089-0094 |
get/set_usb_mod_level,…lan_mod_level,…data_off/1/2/3_mod_input |
live-critical — DATA-OFF MOD=LAN is the documented fix for the "web TX = noise" class of bug (see MEMORY.md); these have methods + [commands] strings but no round-trip guard |
NEW |
tuning step / band edge 10/02 |
get/set_tuning_step, get_band_edge_freq |
tuning_step.presence/band_edge.presence only — step is a clean RMVR |
NEW (low) |
Recommended fix (one ticket per cluster): add RMVR_SAFE_WRITE checks to the registry (_levels.py/_dsp.py/_tx.py/_audio.py) gated on the already-declared capabilities; for the MOD-input cluster, a dedicated _audio/_system round-trip since it guards a known field-failure mode.
B. VALIDATION GAPS — implemented + declared, but presence-only (no round-trip)¶
These have a capability and a registered check, but the check is *.presence (confirms the command answers) rather than set-then-verify. All pass live; upgrading to RMVR would catch silent write-failures.
| Capability | Backend | Live | Recommendation | Ticket |
|---|---|---|---|---|
compressor 16 44 |
get/set_compressor |
compressor.presence ✅ |
RMVR (toggle on/off) | NEW |
break_in 16 47 |
get/set_break_in |
break_in.presence ✅ |
RMVR (OFF/SEMI/FULL) | NEW |
filter_shape 16 56 |
get/set_filter_shape |
filter_shape.presence ✅ |
RMVR (SHARP/SOFT) | NEW |
ssb_tx_bw 16 58 |
get/set_ssb_tx_bandwidth |
ssb_tx_bw.presence ✅ |
RMVR (WIDE/MID/NAR) | NEW |
twin_peak 16 4F |
get/set_twin_peak_filter |
twin_peak.presence ✅ |
RMVR (guarded: only valid in RTTY 2125/170) | NEW (low) |
digisel/ip_plus 16 4E/16 65 |
resp. | *.presence ✅ |
RMVR booleans | NEW (low) |
apf/pbt/drive_gain |
resp. | *.presence ✅ |
level RMVR (folds into Gap A) | NEW |
data_mode 1A 06 |
get/set_data_mode |
data_mode.presence ✅ |
RMVR | NEW (low) |
main_sub_tracking 16 5E |
get/set_main_sub_tracking |
*.presence ✅ |
RMVR boolean | NEW (low) |
xfc 1C 02 |
get/set_xfc_status |
xfc.presence ✅ |
RMVR boolean | NEW (low) |
monitor 16 45 |
get/set_monitor |
monitor.presence ✅ |
RMVR boolean | NEW (low) |
C. MISSING BACKEND — documented operator command, no backend method¶
The driver is near-complete; genuinely-missing operator-facing commands are few. The rest of the manual's unimplemented commands are device-front-panel/config (listed OUT OF SCOPE above).
| CI-V cmd | Function | Value | Ticket |
|---|---|---|---|
27 20 |
Scope marker position (Filter-center vs Carrier-point) | Low — scope cosmetic but a live-changeable scope control, not a Set-mode-only param | NEW (low) |
13 speech / 1A 02 memory keyer / memory channel family |
voice readout · CW keyer store · memory ch | Feature/front-panel — track as a UI feature, not a CAT gap | — (out of scope) |
D. MISMATCH / WRONG — declared/checked but behaves differently (the live FAILs)¶
| CI-V cmd | check_id | Issue | Ticket |
|---|---|---|---|
27 13 single/dual |
scope_dual.set ❌ |
Set does not round-trip on the IC-7610. Per MOR-664 the dual-scope write needs the dual-RX precondition (the radio rejects/ignores the single→dual flip unless the second receiver is active), so read-back never matches the requested value. | MOR-664 |
27 1D VBW |
scope_vbw.set ❌ |
Verified asymmetry: get_scope_vbw passes receiver= into _get_scope_vbw_cmd (runtime/_scope_runtime.py:410-421), but set_scope_vbw calls _scope_set_vbw_cmd(narrow, …) with no receiver (:423-429). When the scope source is SUB the set lands on the wrong RX and read-back mismatches. |
MOR-664 |
27 1F RBW |
scope_rbw.set ❌ |
Same receiver-prefix asymmetry: get_scope_rbw takes receiver= (:473-484) while set_scope_rbw (:486-492) omits it. |
MOR-664 |
(note) 27 1E fixed edge |
scope_fixed_edge.read ✅ |
Already mitigated for the read path — get_scope_fixed_edge injects a <range><edge> selector (range 1, edge 1) or the IC-7610 NAKs the read (fixed by MOR-662). Documented here so the pattern (selector/receiver-prefix on 0x27 sub-reads) is visible. |
— (fixed) |
(note) 16 42/43, 1B 00/01 tone |
— | Repeater-tone / TSQL / tone-freq have backend methods + [commands] strings but the repeater_tone/tsql capabilities are NOT declared in [capabilities].features (IC-7610 is HF/6m, no FM-repeater CTCSS surfaced) — so no tone check runs. Correct intent, but the dangling [commands] entries are dead weight; either drop them or gate behind an explicit "no FM repeater" note. |
NEW (doc/cleanup, low) |
The 3 red FAILs all trace to MOR-664 (already filed). MOR-659/660/661/662/663/665 were the live-found-and-fixed scope/read issues from the same session and are not re-filed.
Ticket coverage summary¶
- 3 live FAILs (
scope_dual.set/scope_vbw.set/scope_rbw.set) → MOR-664 (receiver-prefix on0x27 1D/1Fset + dual-RX precondition on0x27 13). Verified in code (set-path omits thereceiver=the get-path uses). - Gap A (under-declared, no RMVR): ~12 clusters of fully-implemented controls — headline = RF power / MIC gain / COMP level / NR+NB level / CW pitch / MOD-input routing. All NEW.
- Gap B (presence-only → RMVR upgrade): ~11 capabilities, headline = compressor / break_in / filter_shape / ssb_tx_bw. All NEW.
- Gap C (missing backend): only
27 20marker position is a real (low-value) operator gap. NEW. Everything else is front-panel/feature, out of scope. - Gap D (mismatch): the 3 FAILs (MOR-664) + a doc-cleanup note on the dead tone
[commands]entries (NEW, low).