feat: Icom split auto
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
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 {
|
import {
|
||||||
GetIcomState, IcomRefresh,
|
GetIcomState, IcomRefresh,
|
||||||
IcomSetAFGain, IcomSetRFGain, IcomSetNB, IcomSetNBLevel, IcomSetNR, IcomSetNRLevel,
|
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.strokeStyle = '#7dd3fc'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round'; ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// VFO marker: translucent band + crisp line + top marker triangle. In centre
|
// VFO marker: you should ALWAYS see where you are. Exact position when the VFO
|
||||||
// mode the span tracks the VFO, so fall back to the exact centre when the
|
// is inside the span; the centre in CTR mode; clamped to the nearest edge with
|
||||||
// reported freq isn't inside the (just-updated) span — you always see where
|
// a sideways arrow in FIX mode when the fixed scope doesn't cover the VFO (so
|
||||||
// you are.
|
// you can tell which way to tune to get it back on-screen).
|
||||||
const inSpan = vfoHz > 0 && lowHz > 0 && highHz > lowHz && vfoHz >= lowHz && vfoHz <= highHz;
|
const haveVfo = vfoHz > 0 && lowHz > 0 && highHz > lowHz;
|
||||||
const markerX = inSpan ? ((vfoHz - lowHz) / (highHz - lowHz)) * w : (!fixedMode ? w / 2 : -1);
|
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) {
|
if (markerX >= 0) {
|
||||||
const x = markerX;
|
const x = markerX;
|
||||||
ctx.fillStyle = 'rgba(244,63,94,0.10)'; ctx.fillRect(x - 5, 0, 10, h);
|
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.strokeStyle = 'rgba(244,63,94,0.9)'; ctx.lineWidth = 1.25;
|
||||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke();
|
||||||
ctx.fillStyle = 'rgba(244,63,94,0.95)';
|
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
|
// 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 { t } = useI18n();
|
||||||
const [st, setSt] = useState<IcomState>(ZERO);
|
const [st, setSt] = useState<IcomState>(ZERO);
|
||||||
const [cat, setCat] = useState<any>(null); // RigState (freq/mode/split) for the VFO display
|
const [cat, setCat] = useState<any>(null); // RigState (freq/mode/split) for the VFO display
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const [tuning, setTuning] = useState(false);
|
const [tuning, setTuning] = useState(false);
|
||||||
const txRef = useRef(false);
|
const txRef = useRef(false);
|
||||||
const stRef = useRef<IcomState>(ZERO); stRef.current = st;
|
const stRef = useRef<IcomState>(ZERO); stRef.current = st;
|
||||||
@@ -510,11 +518,11 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
|
|||||||
GetCATState().then((c) => setCat(c ?? null)).catch(() => {});
|
GetCATState().then((c) => setCat(c ?? null)).catch(() => {});
|
||||||
};
|
};
|
||||||
const setMode = (m: string) => { setCat((c: any) => (c ? { ...c, mode: m } : c)); SetCATMode(m).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 () => {
|
const refresh = async () => {
|
||||||
setBusy(true);
|
|
||||||
try { await IcomRefresh(); } catch {}
|
try { await IcomRefresh(); } catch {}
|
||||||
await load();
|
await load();
|
||||||
setBusy(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
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">
|
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">
|
||||||
<Power className="size-3.5" /> ON
|
<Power className="size-3.5" /> ON
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => { if (window.confirm(t('icmp.powerOffConfirm'))) IcomSetPower(false).catch(() => {}); }} title={t('icmp.powerOffHint')}
|
<button type="button" onClick={() => IcomSetPower(false).catch(() => {})} title={t('icmp.powerOffHint')}
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-destructive/60 bg-destructive/10 px-2 py-1 text-xs font-bold text-destructive hover:bg-destructive/20">
|
className="inline-flex items-center gap-1 rounded-md border border-destructive/60 bg-destructive/10 px-2 py-1 text-xs font-bold text-destructive hover:bg-destructive/20">
|
||||||
<Power className="size-3.5" /> OFF
|
<Power className="size-3.5" /> OFF
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={refresh} disabled={busy}
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2 py-1 text-xs hover:bg-muted disabled:opacity-40">
|
|
||||||
<RefreshCw className={cn('size-3.5', busy && 'animate-spin')} /> {t('icmp.refresh')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
// edges and take the rest as the trace, then publish immediately.
|
||||||
// (USB splits this across 21 frames; the net rig sends it as one.)
|
// (USB splits this across 21 frames; the net rig sends it as one.)
|
||||||
if len(region) >= 11 {
|
if len(region) >= 11 {
|
||||||
low := civ.BCDToFreq(region[1:6])
|
// Net single-frame layout (IC-7610): region = [info 1B][freq1 5-BCD]
|
||||||
high := civ.BCDToFreq(region[6:11])
|
// [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:]...)
|
amp := append([]byte(nil), region[11:]...)
|
||||||
b.scopeMu.Lock()
|
b.scopeMu.Lock()
|
||||||
b.scopeLow, b.scopeHigh = low, high
|
b.scopeLow, b.scopeHigh = low, high
|
||||||
@@ -627,10 +641,10 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) {
|
|||||||
b.scopeMu.Unlock()
|
b.scopeMu.Unlock()
|
||||||
if firstLog {
|
if firstLog {
|
||||||
head := region
|
head := region
|
||||||
if len(head) > 24 {
|
if len(head) > 16 {
|
||||||
head = head[:24]
|
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
|
continue
|
||||||
@@ -1356,6 +1370,24 @@ func (b *IcomSerial) SetMicGain(p int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *IcomSerial) SetIcomSplit(on bool) 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 {
|
if err := b.exec(civ.CmdSplit, boolByte(on)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user