feat(cat): share the rig over TCI as well as Hamlib — one or the other
internal/rigctld exists because Windows gives a COM port to ONE process: the moment OpsLog talks to the radio natively, nothing else can. It answers the programs that speak Hamlib NET rigctl. This answers the ones built around Expert Electronics' TCI instead — and it answers them whatever radio is connected, because it sits on the same backend-agnostic Rig interface. An operator with an Icom or a Yaesu can now hand a TCI-only program a working rig. One server or the other, never both. They answer the same questions about the same radio, nothing speaks both, and a second listener is only a second thing to go wrong. Written against the official TCI Protocol document (ExpertSDR3/TCI, 12 January 2024, MIT — downloaded and read, not recalled): the initialisation set of §4.1 in its documented order, and the argument order of every command from §4.2. A client will not proceed past connect without that block, which is why it is written out in full rather than stubbed. The one dangerous detail is the VFO mapping. TCI's channel A is where you LISTEN and channel B where you transmit — the opposite way round from OpsLog's RigState, which follows ADIF. Getting that backwards would put a station on the DX's own frequency, so it is pinned in both directions by a test, and RxFreq was added to the adapter rather than inferred. Writing channel B while the rig is simplex is ignored: the client asked to prepare a split transmit frequency, not to QSY, and a logger doing that on every spot click would drag the operator off the station they were listening to. A backend that cannot split still refuses out loud. Only changes are pushed. TCI clients redraw on each command, so re-sending an unchanged frequency four times a second makes a VFO readout flicker and fights the operator's own tuning. Nine tests, no socket needed — the protocol is the decision, not the transport.
This commit is contained in:
@@ -1403,7 +1403,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
yaesu_port: '', yaesu_baud: 38400, yaesu_low_lines: false, kenwood_low_lines: false, kenwood_port: '', kenwood_baud: 9600, kenwood_host: '', kenwood_data_mode: 'usb', xiegu_port: '', xiegu_baud: 19200, xiegu_addr: 0x70, xiegu_ptt_line: '',
|
||||
icom_port: '', icom_baud: 115200, icom_addr: 0x98, icom_net_host: '', icom_net_user: '', icom_net_pass: '', icom_net_audio: false,
|
||||
tci_host: '', tci_port: 40001, tci_spots: false, poll_ms: 250, delay_ms: 0,
|
||||
digital_default: 'FT8', share_enabled: false, share_port: 4532,
|
||||
digital_default: 'FT8', share_enabled: false, share_port: 4532, share_proto: 'rigctl', share_tci_port: 40001,
|
||||
ptt_hotkey_enabled: false, ptt_hotkey: '', ptt_hotkey_toggle: false,
|
||||
});
|
||||
// While true, the next key press is captured as the PTT hotkey.
|
||||
@@ -3149,15 +3149,47 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</label>
|
||||
<p className="text-[11px] text-muted-foreground">{t('cat.shareHint')}</p>
|
||||
{catCfg.share_enabled && (
|
||||
<div className="space-y-1 max-w-[200px]">
|
||||
<Label>{t('cat.sharePort')}</Label>
|
||||
<PortInput
|
||||
value={catCfg.share_port || 4532}
|
||||
fallback={4532}
|
||||
onChange={(n) => setCatCfg((s) => ({ ...s, share_port: n }))}
|
||||
/>
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
{/* One protocol or the other. They answer the same questions about
|
||||
the same radio, and no program speaks both — so this is a
|
||||
choice, not two switches. */}
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.shareProto')}</Label>
|
||||
<Select
|
||||
value={(catCfg as any).share_proto === 'tci' ? 'tci' : 'rigctl'}
|
||||
onValueChange={(v) => setCatCfg((s) => ({ ...s, share_proto: v } as any))}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[240px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="rigctl">{t('cat.shareRigctl')}</SelectItem>
|
||||
<SelectItem value="tci">{t('cat.shareTci')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1 max-w-[200px]">
|
||||
<Label>{t('cat.sharePort')}</Label>
|
||||
{(catCfg as any).share_proto === 'tci' ? (
|
||||
<PortInput
|
||||
value={(catCfg as any).share_tci_port || 40001}
|
||||
fallback={40001}
|
||||
onChange={(n) => setCatCfg((s) => ({ ...s, share_tci_port: n } as any))}
|
||||
/>
|
||||
) : (
|
||||
<PortInput
|
||||
value={catCfg.share_port || 4532}
|
||||
fallback={4532}
|
||||
onChange={(n) => setCatCfg((s) => ({ ...s, share_port: n }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{catCfg.share_enabled && (catCfg as any).share_proto === 'tci' && catCfg.backend === 'tci' && (
|
||||
// Both ends TCI: ExpertSDR is almost certainly already holding
|
||||
// 40001 on this machine, and our server would fail to bind. Worth
|
||||
// saying here rather than leaving it in the log.
|
||||
<p className="text-[11px] text-warning">{t('cat.shareTciClash')}</p>
|
||||
)}
|
||||
</div>
|
||||
{/* PTT hotkey — a keyboard key that keys the rig while OpsLog is focused.
|
||||
Uses the Audio → PTT method (CAT / RTS / DTR), falling back to CAT. */}
|
||||
|
||||
Reference in New Issue
Block a user