From 05fbb8a3bcec3581737e3504dee08e2a0206730e Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 7 Jul 2026 15:56:14 +0200 Subject: [PATCH] feat: Icom split auto --- frontend/src/components/IcomPanel.tsx | 36 +++++++++++++---------- internal/cat/icomserial.go | 42 +++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/IcomPanel.tsx b/frontend/src/components/IcomPanel.tsx index 71f0e74..571770c 100644 --- a/frontend/src/components/IcomPanel.tsx +++ b/frontend/src/components/IcomPanel.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { Radio, AudioLines, RefreshCw, Mic, Activity, SlidersHorizontal, Antenna, Filter, Power } from 'lucide-react'; +import { Radio, AudioLines, Mic, Activity, SlidersHorizontal, Antenna, Filter, Power } from 'lucide-react'; import { GetIcomState, IcomRefresh, IcomSetAFGain, IcomSetRFGain, IcomSetNB, IcomSetNBLevel, IcomSetNR, IcomSetNRLevel, @@ -394,19 +394,28 @@ function ScopePanadapter() { ctx.strokeStyle = '#7dd3fc'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round'; ctx.stroke(); ctx.restore(); - // VFO marker: translucent band + crisp line + top marker triangle. In centre - // mode the span tracks the VFO, so fall back to the exact centre when the - // reported freq isn't inside the (just-updated) span — you always see where - // you are. - const inSpan = vfoHz > 0 && lowHz > 0 && highHz > lowHz && vfoHz >= lowHz && vfoHz <= highHz; - const markerX = inSpan ? ((vfoHz - lowHz) / (highHz - lowHz)) * w : (!fixedMode ? w / 2 : -1); + // VFO marker: you should ALWAYS see where you are. Exact position when the VFO + // is inside the span; the centre in CTR mode; clamped to the nearest edge with + // a sideways arrow in FIX mode when the fixed scope doesn't cover the VFO (so + // you can tell which way to tune to get it back on-screen). + const haveVfo = vfoHz > 0 && lowHz > 0 && highHz > lowHz; + const inSpan = haveVfo && vfoHz >= lowHz && vfoHz <= highHz; + let markerX = -1; + let offEdge = 0; // -1 = VFO off the left edge, +1 = off the right + if (inSpan) markerX = ((vfoHz - lowHz) / (highHz - lowHz)) * w; + else if (!fixedMode) markerX = w / 2; + else if (haveVfo) { offEdge = vfoHz < lowHz ? -1 : 1; markerX = offEdge < 0 ? 1 : w - 1; } if (markerX >= 0) { const x = markerX; ctx.fillStyle = 'rgba(244,63,94,0.10)'; ctx.fillRect(x - 5, 0, 10, h); ctx.strokeStyle = 'rgba(244,63,94,0.9)'; ctx.lineWidth = 1.25; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); ctx.fillStyle = 'rgba(244,63,94,0.95)'; - ctx.beginPath(); ctx.moveTo(x - 4, 0); ctx.lineTo(x + 4, 0); ctx.lineTo(x, 6); ctx.closePath(); ctx.fill(); + if (offEdge === 0) { + ctx.beginPath(); ctx.moveTo(x - 4, 0); ctx.lineTo(x + 4, 0); ctx.lineTo(x, 6); ctx.closePath(); ctx.fill(); + } else { + const yh = 8; ctx.beginPath(); ctx.moveTo(x, yh - 5); ctx.lineTo(x + offEdge * 7, yh); ctx.lineTo(x, yh + 5); ctx.closePath(); ctx.fill(); + } } // Frequency scale. In fixed mode the rig reports usable edge frequencies, so @@ -500,7 +509,6 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void const { t } = useI18n(); const [st, setSt] = useState(ZERO); const [cat, setCat] = useState(null); // RigState (freq/mode/split) for the VFO display - const [busy, setBusy] = useState(false); const [tuning, setTuning] = useState(false); const txRef = useRef(false); const stRef = useRef(ZERO); stRef.current = st; @@ -510,11 +518,11 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void GetCATState().then((c) => setCat(c ?? null)).catch(() => {}); }; const setMode = (m: string) => { setCat((c: any) => (c ? { ...c, mode: m } : c)); SetCATMode(m).catch(() => {}); }; + // Initial one-shot read of the rig's DSP snapshot on mount (the 500ms poll only + // re-reads the cache; the backend also loads DSP on the first responsive read). const refresh = async () => { - setBusy(true); try { await IcomRefresh(); } catch {} await load(); - setBusy(false); }; useEffect(() => { @@ -601,14 +609,10 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void className="inline-flex items-center gap-1 rounded-md border border-success/60 bg-success/10 px-2 py-1 text-xs font-bold text-success hover:bg-success/20"> ON - - diff --git a/internal/cat/icomserial.go b/internal/cat/icomserial.go index 056c726..677e888 100644 --- a/internal/cat/icomserial.go +++ b/internal/cat/icomserial.go @@ -615,8 +615,22 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) { // edges and take the rest as the trace, then publish immediately. // (USB splits this across 21 frames; the net rig sends it as one.) if len(region) >= 11 { - low := civ.BCDToFreq(region[1:6]) - high := civ.BCDToFreq(region[6:11]) + // Net single-frame layout (IC-7610): region = [info 1B][freq1 5-BCD] + // [freq2 5-BCD][amplitude bytes]. The two freq fields depend on the + // scope mode: FIXED sends [low-edge][high-edge] (both absolute), CENTRE + // sends [centre][span]. Tell them apart by magnitude — a second value + // BIGGER than the first is a real high edge; a small one is a span + // (e.g. 14.200 MHz + 100 kHz → centred 14.150-14.250; 21.000 + + // 21.070 → fixed 21.000-21.070). Guessing wrong here gave the absurd + // 21.000-42.070 span (low + a 21 MHz "span"). + v1 := civ.BCDToFreq(region[1:6]) + v2 := civ.BCDToFreq(region[6:11]) + var low, high int64 + if v2 > v1 { + low, high = v1, v2 // absolute low/high edges (fixed edge set) + } else { + low, high = v1-v2/2, v1+v2/2 // centre + span (centre-on-VFO) + } amp := append([]byte(nil), region[11:]...) b.scopeMu.Lock() b.scopeLow, b.scopeHigh = low, high @@ -627,10 +641,10 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) { b.scopeMu.Unlock() if firstLog { head := region - if len(head) > 24 { - head = head[:24] + if len(head) > 16 { + head = head[:16] } - applog.Printf("icom scope (net 1-frame): region=%dB head=[% X] → edges %d..%d Hz points=%d", len(region), head, low, high, len(amp)) + applog.Printf("icom scope (net 1-frame): head=[% X] v1=%d v2=%d → %d..%d Hz points=%d", head, v1, v2, low, high, len(amp)) } } continue @@ -1356,6 +1370,24 @@ func (b *IcomSerial) SetMicGain(p int) error { } func (b *IcomSerial) SetIcomSplit(on bool) error { + if on { + // Enable split with the usual "work him up" TX offset: +1 kHz on CW, + // +5 kHz otherwise (SSB). Set the unselected (TX) VFO to RX+offset first, + // then turn split on. 0x25 0x01 + BCD sets the unselected VFO's frequency. + rx := b.curFreq + if rx <= 0 { + if hz, err := b.readFreq(); err == nil { + rx = hz + } + } + if rx > 0 { + offset := int64(5000) + if b.curModeByte == civ.ModeCW || b.curModeByte == civ.ModeCWR { + offset = 1000 + } + _ = b.exec(append([]byte{civ.CmdVfoFreq, civ.SubVfoUnselected}, civ.FreqToBCD(rx+offset)...)...) + } + } if err := b.exec(civ.CmdSplit, boolByte(on)); err != nil { return err }