fix: bug where scope is now showing on IC7300
This commit is contained in:
@@ -366,6 +366,11 @@ export default function App() {
|
||||
|
||||
// CAT — receives live rig state via Wails events.
|
||||
const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
|
||||
// Configured CAT backend ('icom' USB vs 'icom-net'): the live catState.backend
|
||||
// is "icom" for BOTH, so we track the settings value to tell them apart (used to
|
||||
// hide the rig ON/OFF buttons on USB, where the interface is unpowered when the
|
||||
// rig is off so power-ON can't work).
|
||||
const [catBackend, setCatBackend] = useState('');
|
||||
const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 });
|
||||
const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false });
|
||||
const [agStatus, setAgStatus] = useState<AGStatus>({ connected: false, port_a: 0, port_b: 0, antennas: [] });
|
||||
@@ -1352,6 +1357,7 @@ export default function App() {
|
||||
try {
|
||||
const c = await GetCATSettings();
|
||||
if (c.digital_default) digitalDefaultRef.current = c.digital_default;
|
||||
setCatBackend(c.backend ?? '');
|
||||
} catch {}
|
||||
}, []);
|
||||
const loadLists = useCallback(async () => {
|
||||
@@ -3154,7 +3160,7 @@ export default function App() {
|
||||
case 'icom':
|
||||
return (
|
||||
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
|
||||
<IcomPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
<IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
case 'netcontrol':
|
||||
@@ -4245,7 +4251,7 @@ export default function App() {
|
||||
is an Icom. */}
|
||||
{catState.backend === 'icom' && (
|
||||
<TabsContent value="icom" className="flex-1 min-h-0 p-0">
|
||||
<IcomPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
<IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
|
||||
@@ -58,6 +58,21 @@ const BANDS: { l: string; hz: number }[] = [
|
||||
// SSB by frequency and the rig's data variant for digital modes.
|
||||
const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM'];
|
||||
|
||||
// Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the
|
||||
// same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18
|
||||
// dB stepped attenuator; the IC-7300/705/7100 have a single 20 dB attenuator; the
|
||||
// IC-9700 a single 10 dB. Offering the wrong steps = a dead button (the rig NAKs
|
||||
// e.g. 6 dB on a 7300). Default to the common single 20 dB for unknown models.
|
||||
function attOptions(model?: string): { v: string; l: string }[] {
|
||||
const m = (model ?? '').toUpperCase();
|
||||
const OFF = { v: '0', l: 'OFF' };
|
||||
if (/(7610|7700|7800|7850|7851)/.test(m)) {
|
||||
return [OFF, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }];
|
||||
}
|
||||
if (m.includes('9700')) return [OFF, { v: '10', l: '10dB' }];
|
||||
return [OFF, { v: '20', l: '20dB' }]; // IC-7300 / IC-705 / IC-7100 / default
|
||||
}
|
||||
|
||||
// fmtVFO renders a Hz frequency the way an Icom front panel does:
|
||||
// MHz "." 3-digit-kHz "." 2-digit-(10 Hz). 21032000 → "21.032.00".
|
||||
function fmtVFO(hz?: number): string {
|
||||
@@ -534,7 +549,7 @@ function ScopePanadapter() {
|
||||
// Unlike the Flex (which pushes state), the Icom is polled: meters/TX state are
|
||||
// read every cache cycle; DSP set-controls are optimistic and reconcile on the
|
||||
// next poll. Front-panel knob changes for DSP show after ↻ Refresh.
|
||||
export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void } = {}) {
|
||||
export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (rst: string) => void; isNetwork?: boolean } = {}) {
|
||||
const { t } = useI18n();
|
||||
const [st, setSt] = useState<IcomState>(ZERO);
|
||||
const [cat, setCat] = useState<any>(null); // RigState (freq/mode/split) for the VFO display
|
||||
@@ -637,8 +652,12 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
|
||||
{st.split ? <span className="rounded-md bg-warning/20 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning">Split</span> : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Radio power ON / OFF. Manual by design — the app never wakes the rig
|
||||
on connect; ON sends the wake preamble then the rig boots ~15 s. */}
|
||||
{/* Radio power ON / OFF — NETWORK only. Over USB the CI-V interface is
|
||||
unpowered while the rig is off, so power-ON can't reach it (OFF works
|
||||
but ON doesn't); hiding both avoids a dead button. On the network the
|
||||
rig's LAN server stays alive in standby, so both work. */}
|
||||
{isNetwork && (
|
||||
<>
|
||||
<button type="button" onClick={() => IcomSetPower(true).catch(() => {})} title={t('icmp.powerOnHint')}
|
||||
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
|
||||
@@ -647,6 +666,8 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
|
||||
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
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -804,7 +825,7 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
|
||||
onChange={(v) => set({ preamp: parseInt(v) }, () => IcomSetPreamp(parseInt(v)))} />
|
||||
</Row>
|
||||
<Row label="Att">
|
||||
<Segmented value={String(st.att)} options={[{ v: '0', l: 'OFF' }, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }]}
|
||||
<Segmented value={String(st.att)} options={attOptions(cat?.rig)}
|
||||
onChange={(v) => set({ att: parseInt(v) }, () => IcomSetAtt(parseInt(v)))} />
|
||||
</Row>
|
||||
<Row label={t('icmp.filter')}>
|
||||
|
||||
@@ -761,6 +761,20 @@ function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Known Icom models paired with their FACTORY default CI-V address. Picking a
|
||||
// model sets icom_addr so the backend identifies it (civ.ModelName) and the UI
|
||||
// adapts (e.g. the attenuator steps differ by model). Keep in lockstep with
|
||||
// civ.ModelName in internal/cat/civ/civ.go. `addr` is decimal.
|
||||
const ICOM_MODELS: { name: string; addr: number }[] = [
|
||||
{ name: 'IC-705', addr: 0xA4 },
|
||||
{ name: 'IC-7300', addr: 0x94 },
|
||||
{ name: 'IC-7610', addr: 0x98 },
|
||||
{ name: 'IC-7700', addr: 0x88 },
|
||||
{ name: 'IC-7800', addr: 0x80 },
|
||||
{ name: 'IC-9100', addr: 0x7C },
|
||||
{ name: 'IC-9700', addr: 0xA2 },
|
||||
];
|
||||
|
||||
export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
|
||||
@@ -2054,12 +2068,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.icomModel')}</Label>
|
||||
<Select
|
||||
value={ICOM_MODELS.find((m) => m.addr === (catCfg.icom_addr ?? 0x98))?.name ?? '_custom'}
|
||||
onValueChange={(v) => { const m = ICOM_MODELS.find((x) => x.name === v); if (m) setCatCfg((s) => ({ ...s, icom_addr: m.addr })); }}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{ICOM_MODELS.map((m) => <SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>)}
|
||||
<SelectItem value="_custom">{t('cat.icomModelOther')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.civAddr')}</Label>
|
||||
<Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')}
|
||||
onChange={(e) => { const n = parseInt(e.target.value.replace(/[^0-9a-fA-F]/g, ''), 16); setCatCfg((s) => ({ ...s, icom_addr: (n >= 0 && n <= 0xFF) ? n : s.icom_addr })); }} />
|
||||
<p className="text-xs text-muted-foreground">{t('cat.civHint')}</p>
|
||||
</div>
|
||||
<p className="col-span-2 text-xs text-muted-foreground">{t('cat.civHint')}</p>
|
||||
</>
|
||||
)}
|
||||
{catCfg.backend === 'icom-net' && (
|
||||
@@ -2069,6 +2095,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Input placeholder="192.168.1.60" value={catCfg.icom_net_host ?? ''}
|
||||
onChange={(e) => setCatCfg((s) => ({ ...s, icom_net_host: e.target.value }))} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.icomModel')}</Label>
|
||||
<Select
|
||||
value={ICOM_MODELS.find((m) => m.addr === (catCfg.icom_addr ?? 0x98))?.name ?? '_custom'}
|
||||
onValueChange={(v) => { const m = ICOM_MODELS.find((x) => x.name === v); if (m) setCatCfg((s) => ({ ...s, icom_addr: m.addr })); }}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{ICOM_MODELS.map((m) => <SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>)}
|
||||
<SelectItem value="_custom">{t('cat.icomModelOther')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.civAddr')}</Label>
|
||||
<Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')}
|
||||
|
||||
@@ -137,7 +137,7 @@ const en: Dict = {
|
||||
'cat.icomNetAudio': 'Stream RX audio over the network (experimental)',
|
||||
'cat.icomNetAudioHint': 'Play the rig’s received audio through your Listening device (Settings → Audio) over the 50003 stream. Experimental — the audio framing is pending on-rig verification; leave off if control misbehaves.',
|
||||
'cat.omnirigRig': 'OmniRig rig slot', 'cat.flexIp': 'FlexRadio IP', 'cat.port': 'Port', 'cat.flexSpots': 'Show cluster spots on the panadapter', 'cat.flexSpotsHint': "(spots from OpsLog's DX cluster appear on the radio, auto-expire after 30 min)",
|
||||
'cat.icomPort': 'Icom CI-V port', 'cat.selectCom': 'Select COM port', 'cat.noPorts': 'No ports found', 'cat.baud': 'Baud rate', 'cat.civAddr': 'CI-V address (hex)', 'cat.civHint': 'IC-7610 = 98, IC-7300 = 94, IC-9700 = A2, IC-705 = A4. Set "CI-V USB Echo Back" OFF and CI-V baud to match on the rig.',
|
||||
'cat.icomPort': 'Icom CI-V port', 'cat.selectCom': 'Select COM port', 'cat.noPorts': 'No ports found', 'cat.baud': 'Baud rate', 'cat.icomModel': 'Rig model', 'cat.icomModelOther': 'Other (custom address)', 'cat.civAddr': 'CI-V address (hex)', 'cat.civHint': 'Pick your model to set the CI-V address automatically (or choose "Other" and type it). Set "CI-V USB Echo Back" OFF and CI-V baud to match on the rig.',
|
||||
'cat.tciHost': 'TCI host', 'cat.tciHint': 'Enable the TCI server in ExpertSDR2/EESDR (Options → TCI). Default port 40001. Use 127.0.0.1 when OpsLog runs on the same PC.', 'cat.tciSpots': 'Show cluster spots on the panorama', 'cat.tciSpotsHint': "(spots from OpsLog's DX cluster appear on the SDR panadapter)",
|
||||
'cat.pollMs': 'Poll interval (ms)', 'cat.delayMs': 'CAT delay (ms)', 'cat.digitalDefault': 'Default digital mode (when rig reports DIG)', 'cat.modeBeforeFreq': 'Set mode before frequency', 'cat.modeBeforeFreqHint': '(older rigs that drop the mode after a band change)',
|
||||
'cat.omnirigHint': 'Configure your rig (COM port, baud rate, model) in OmniRig\'s own settings GUI first. OpsLog will read whichever Rig slot you select here. Set CAT delay above 0 if your rig drops commands sent back-to-back (some older Kenwood/Yaesu). OmniRig only reports generic "DIG" for digital modes — Default digital mode is the specific mode OpsLog will surface (and log).',
|
||||
@@ -332,7 +332,7 @@ const fr: Dict = {
|
||||
'cat.icomNetAudio': 'Diffuser l’audio RX par le réseau (expérimental)',
|
||||
'cat.icomNetAudioHint': 'Écoute l’audio reçu du poste sur ton périphérique d’écoute (Réglages → Audio) via le flux 50003. Expérimental — le format audio reste à vérifier sur le poste ; laisse désactivé si le contrôle se comporte mal.',
|
||||
'cat.omnirigRig': 'Slot OmniRig', 'cat.flexIp': 'IP FlexRadio', 'cat.port': 'Port', 'cat.flexSpots': 'Afficher les spots cluster sur le panadapter', 'cat.flexSpotsHint': "(les spots du cluster DX d'OpsLog apparaissent sur la radio, expirent après 30 min)",
|
||||
'cat.icomPort': 'Port CI-V Icom', 'cat.selectCom': 'Choisir un port COM', 'cat.noPorts': 'Aucun port trouvé', 'cat.baud': 'Débit (baud)', 'cat.civAddr': 'Adresse CI-V (hex)', 'cat.civHint': 'IC-7610 = 98, IC-7300 = 94, IC-9700 = A2, IC-705 = A4. Mets « CI-V USB Echo Back » sur OFF et fais correspondre le débit CI-V sur le poste.',
|
||||
'cat.icomPort': 'Port CI-V Icom', 'cat.selectCom': 'Choisir un port COM', 'cat.noPorts': 'Aucun port trouvé', 'cat.baud': 'Débit (baud)', 'cat.icomModel': 'Modèle de poste', 'cat.icomModelOther': 'Autre (adresse perso)', 'cat.civAddr': 'Adresse CI-V (hex)', 'cat.civHint': 'Choisis ton modèle pour fixer l’adresse CI-V automatiquement (ou « Autre » et saisis-la). Mets « CI-V USB Echo Back » sur OFF et fais correspondre le débit CI-V sur le poste.',
|
||||
'cat.tciHost': 'Hôte TCI', 'cat.tciHint': 'Active le serveur TCI dans ExpertSDR2/EESDR (Options → TCI). Port par défaut 40001. Utilise 127.0.0.1 si OpsLog tourne sur le même PC.', 'cat.tciSpots': 'Afficher les spots cluster sur le panorama', 'cat.tciSpotsHint': "(les spots du cluster DX d'OpsLog apparaissent sur le panadapter SDR)",
|
||||
'cat.pollMs': 'Intervalle de poll (ms)', 'cat.delayMs': 'Délai CAT (ms)', 'cat.digitalDefault': 'Mode numérique par défaut (quand le poste indique DIG)', 'cat.modeBeforeFreq': 'Régler le mode avant la fréquence', 'cat.modeBeforeFreqHint': '(anciens postes qui perdent le mode après un changement de bande)',
|
||||
'cat.omnirigHint': "Configure d'abord ton poste (port COM, débit, modèle) dans l'interface de réglages d'OmniRig. OpsLog lira le slot Rig que tu choisis ici. Mets le délai CAT au-dessus de 0 si ton poste perd des commandes envoyées coup sur coup (certains anciens Kenwood/Yaesu). OmniRig ne rapporte qu'un « DIG » générique pour les modes numériques — le mode numérique par défaut est le mode précis qu'OpsLog affichera (et loggera).",
|
||||
|
||||
@@ -210,17 +210,25 @@ func (b *IcomSerial) Connect() error {
|
||||
go b.netScopeFeeder(sc.ScopeChan(), b.readerDone)
|
||||
}
|
||||
|
||||
// Best-effort model identification: ask the rig for its own CI-V address.
|
||||
// Best-effort model identification: ask the rig for its own CI-V address. The
|
||||
// 0x19 ID read returns the rig's FACTORY default address (e.g. 0x94 for an
|
||||
// IC-7300) regardless of the address it's currently OPERATING on — so it's the
|
||||
// reliable model signal even when the user runs the rig at a non-default CI-V
|
||||
// address (a common trick to make CAT "just work" at 0x98).
|
||||
idAddr := b.rigAddr // fallback: the configured address if the ID read fails
|
||||
if err := b.write(civ.CmdReadID, civ.SubPTT); err == nil {
|
||||
if f, err := b.recv(icomReadTimeout, func(d civ.Decoded) bool {
|
||||
return d.Cmd == civ.CmdReadID && len(d.Data) >= 2 && d.Data[0] == 0x00
|
||||
}); err == nil {
|
||||
b.model = civ.ModelName(f.Data[1])
|
||||
idAddr = f.Data[1]
|
||||
}
|
||||
}
|
||||
// Dual-scope rigs (IC-7610/9700) prefix each waveform frame with a main/sub
|
||||
// selector byte; single-scope rigs (IC-7300…) do not.
|
||||
b.dualScope = b.rigAddr == 0x98 || b.rigAddr == 0xA2
|
||||
// selector byte; single-scope rigs (IC-7300…) do not. Decide from the
|
||||
// IDENTIFIED model, NOT the configured address: an IC-7300 run at 0x98 must
|
||||
// still parse single-scope frames (this was the "scope blank on the 7300" bug).
|
||||
b.dualScope = idAddr == 0x98 || idAddr == 0xA2
|
||||
// Defer the DSP snapshot until the rig actually answers CI-V. Over the network
|
||||
// the rig may still be booting (or off) at Connect, so an immediate readDSP
|
||||
// would time out and leave every control at 0 / off with no retry. ReadState
|
||||
@@ -590,7 +598,7 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if rawN < 4 {
|
||||
if rawN < 24 {
|
||||
rawN++
|
||||
applog.Printf("icom scope raw #%d: len=%d data=[% X]", rawN, len(f.Data), f.Data)
|
||||
}
|
||||
@@ -696,6 +704,13 @@ func (b *IcomSerial) assembleSweep(regions map[byte][]byte, total byte) {
|
||||
// and 0x27 0x11 turns the waveform data OUTPUT over CI-V on. While on, the reader
|
||||
// routes every 0x27 frame to scopeLoop.
|
||||
func (b *IcomSerial) SetScope(on bool) error {
|
||||
if on {
|
||||
// Context for the scope-diagnostic log: which rig + whether we expect the
|
||||
// dual-scope (main/sub) frame layout. If the IC-7300 (single scope) streams
|
||||
// but nothing shows, compare its `icom scope raw` frames against this.
|
||||
applog.Printf("icom scope: enable on rig=%q addr=0x%02X dualScope=%v (expect %s frame layout)",
|
||||
b.model, b.rigAddr, b.dualScope, map[bool]string{true: "27 00 [MS] [seq] [total] …", false: "27 00 [seq] [total] …"}[b.dualScope])
|
||||
}
|
||||
// Some firmwares don't ack 0x27 sets; a timeout here isn't fatal, so log and
|
||||
// continue rather than abort the second command.
|
||||
if err := b.exec(civ.CmdScope, civ.SubScopeOnOff, boolByte(on)); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user