feat(cat): pick the radio, then how it is connected
The backend list mixed two different questions. 'Icom (USB)' and 'Icom (network)' were separate entries; Kenwood and Elecraft hid the same choice in a field further down; and Flex, TCI and OmniRig sat in the same list as if they were the same kind of answer. An operator picks a radio and then says how it is plugged in, so that is what the panel asks, in that order, and the two answers together choose the backend. Each brand offers only the connections it has, and a brand with one way in still shows it, greyed: 'there is no choice here' is an answer and an empty space is not. The stored backend names are unchanged — 'icom-net' is still 'icom-net' — so a settings file written by an older build keeps working, and brand+connection are DERIVED from it rather than held alongside it, which is what keeps them from drifting apart when something else writes the backend.
This commit is contained in:
@@ -1476,6 +1476,43 @@ const ICOM_MODELS: { name: string; addr: number }[] = [
|
||||
{ name: 'IC-9700', addr: 0xA2 },
|
||||
];
|
||||
|
||||
// The radios OpsLog talks to, by BRAND, and the ways each one can be reached.
|
||||
//
|
||||
// Three connections exist in the world and each brand has its own subset:
|
||||
// usb — a COM port, the radio's own USB or a serial cable
|
||||
// bridge — RS-232 carried over Ethernet by a serial bridge (ser2net, an
|
||||
// Ethernet-serial box): the SAME CAT bytes, a socket instead of wire
|
||||
// native — the manufacturer's own network protocol, a session of its own
|
||||
//
|
||||
// The backend NAME stored in settings is unchanged — 'icom-net' is still
|
||||
// 'icom-net' — because a settings file written by an older build has to keep
|
||||
// working. This table is only how the two questions map onto it.
|
||||
const CAT_BRANDS: { id: string; label: string; links: string[]; backend: (link: string) => string }[] = [
|
||||
{ id: 'elecraft', label: 'Elecraft K3 / K4', links: ['usb', 'bridge', 'native'], backend: () => 'elecraft' },
|
||||
{ id: 'kenwood', label: 'Kenwood', links: ['usb', 'bridge', 'native'], backend: () => 'kenwood' },
|
||||
{ id: 'yaesu', label: 'Yaesu', links: ['usb'], backend: () => 'yaesu' },
|
||||
{ id: 'icom', label: 'Icom', links: ['usb', 'native'], backend: (l) => (l === 'native' ? 'icom-net' : 'icom') },
|
||||
{ id: 'flex', label: 'FlexRadio (SmartSDR)', links: ['native'], backend: () => 'flex' },
|
||||
{ id: 'tci', label: 'Expert Electronics / SunSDR (TCI)', links: ['native'], backend: () => 'tci' },
|
||||
{ id: 'xiegu', label: 'Xiegu', links: ['usb'], backend: () => 'xiegu' },
|
||||
{ id: 'omnirig', label: 'OmniRig (any rig)', links: ['usb'], backend: () => 'omnirig' },
|
||||
];
|
||||
|
||||
// brandOfBackend reads the stored backend back into the two questions.
|
||||
function brandOfBackend(backend: string, kenwoodLink?: string): { brand: string; link: string } {
|
||||
switch (backend) {
|
||||
case 'icom-net': return { brand: 'icom', link: 'native' };
|
||||
case 'icom': return { brand: 'icom', link: 'usb' };
|
||||
case 'flex': return { brand: 'flex', link: 'native' };
|
||||
case 'tci': return { brand: 'tci', link: 'native' };
|
||||
case 'yaesu': return { brand: 'yaesu', link: 'usb' };
|
||||
case 'xiegu': return { brand: 'xiegu', link: 'usb' };
|
||||
case 'kenwood': case 'elecraft':
|
||||
return { brand: backend, link: kenwoodLink || 'usb' };
|
||||
default: return { brand: 'omnirig', link: 'usb' };
|
||||
}
|
||||
}
|
||||
|
||||
export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable }: Props) {
|
||||
const { t } = useI18n();
|
||||
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
|
||||
@@ -1520,6 +1557,25 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
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,
|
||||
});
|
||||
// Brand + connection, derived from the stored backend rather than held
|
||||
// separately: two sources for one fact drift apart the first time something
|
||||
// else writes the backend (loading a profile, an older settings file).
|
||||
const catBrand = brandOfBackend(catCfg.backend, (catCfg as any).kenwood_link).brand;
|
||||
const catLink = brandOfBackend(catCfg.backend, (catCfg as any).kenwood_link).link;
|
||||
const applyCatBrand = (id: string) => {
|
||||
const b = CAT_BRANDS.find((x) => x.id === id);
|
||||
if (!b) return;
|
||||
// Keep the connection when the new brand offers it, otherwise take its
|
||||
// first — picking Flex from Kenwood-over-USB has to land on something.
|
||||
const link = b.links.includes(catLink) ? catLink : b.links[0];
|
||||
setCatCfg((s) => ({ ...s, backend: b.backend(link), kenwood_link: link } as any));
|
||||
};
|
||||
const applyCatLink = (link: string) => {
|
||||
const b = CAT_BRANDS.find((x) => x.id === catBrand);
|
||||
if (!b || !b.links.includes(link)) return;
|
||||
setCatCfg((s) => ({ ...s, backend: b.backend(link), kenwood_link: link } as any));
|
||||
};
|
||||
|
||||
// While true, the next key press is captured as the PTT hotkey.
|
||||
const [capturingPtt, setCapturingPtt] = useState(false);
|
||||
const [rotors, setRotors] = useState<RotatorDevice[]>([]);
|
||||
@@ -3019,24 +3075,42 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{t('cat.enable')}
|
||||
</label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* BRAND, then CONNECTION.
|
||||
The backend list mixed the two: "Icom (USB)" and "Icom (network)"
|
||||
were separate entries, while Kenwood and Elecraft hid the same
|
||||
choice in a field further down. An operator picks a radio, then
|
||||
says how it is plugged in — so that is what the panel asks, in
|
||||
that order, and the two answers together choose the backend. */}
|
||||
<div className="grid grid-cols-2 gap-3 items-start">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.backend')}</Label>
|
||||
<Select value={catCfg.backend} onValueChange={(v) => setCatCfg((s) => ({ ...s, backend: v }))}>
|
||||
<Label>{t('cat.brand')}</Label>
|
||||
<Select value={catBrand} onValueChange={(v) => applyCatBrand(v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="omnirig">{t('cat.optOmnirig')}</SelectItem>
|
||||
<SelectItem value="flex">{t('cat.optFlex')}</SelectItem>
|
||||
<SelectItem value="yaesu">{t('cat.optYaesu')}</SelectItem>
|
||||
<SelectItem value="kenwood">{t('cat.optKenwood')}</SelectItem>
|
||||
<SelectItem value="elecraft">{t('cat.optElecraft')}</SelectItem>
|
||||
<SelectItem value="xiegu">{t('cat.optXiegu')}</SelectItem>
|
||||
<SelectItem value="icom">{t('cat.optIcom')}</SelectItem>
|
||||
<SelectItem value="icom-net">{t('cat.optIcomNet')}</SelectItem>
|
||||
<SelectItem value="tci">{t('cat.optTci')}</SelectItem>
|
||||
{CAT_BRANDS.map((b) => <SelectItem key={b.id} value={b.id}>{b.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.kwLink')}</Label>
|
||||
{/* One button per connection the brand actually has. A brand with
|
||||
a single way in still shows it, greyed: "there is no choice
|
||||
here" is an answer, and an empty space is not. */}
|
||||
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs w-full">
|
||||
{(CAT_BRANDS.find((b) => b.id === catBrand)?.links ?? ['usb']).map((l) => (
|
||||
<button key={l} type="button"
|
||||
disabled={(CAT_BRANDS.find((b) => b.id === catBrand)?.links ?? []).length < 2}
|
||||
onClick={() => applyCatLink(l)}
|
||||
className={cn('flex-1 px-2 py-1.5 font-medium border-l border-border first:border-l-0 disabled:opacity-70',
|
||||
catLink === l ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted')}>
|
||||
{l === 'usb' ? t('cat.kwLinkUsb') : l === 'bridge' ? t('cat.kwLinkBridge') : t('cat.kwLinkNative')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{catLink === 'native' && catBrand !== 'flex' && catBrand !== 'tci' && catBrand !== 'icom' && (
|
||||
<p className="text-xs text-warning">{t('cat.kwLinkNativeHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
{catCfg.backend === 'omnirig' && (
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.omnirigRig')}</Label>
|
||||
@@ -3185,21 +3259,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
The two were offered side by side with nothing to say which one
|
||||
the backend would use — it prefers the network address whenever
|
||||
the field is not empty, which is invisible from here. */}
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.kwLink')}</Label>
|
||||
<Select value={(catCfg as any).kenwood_link || 'usb'}
|
||||
onValueChange={(v) => setCatCfg((s) => ({ ...s, kenwood_link: v } as any))}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="usb">{t('cat.kwLinkUsb')}</SelectItem>
|
||||
<SelectItem value="bridge">{t('cat.kwLinkBridge')}</SelectItem>
|
||||
<SelectItem value="native">{t('cat.kwLinkNative')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{((catCfg as any).kenwood_link || 'usb') === 'native' && (
|
||||
<p className="text-xs text-warning">{t('cat.kwLinkNativeHint')}</p>
|
||||
)}
|
||||
</div>
|
||||
{(((catCfg as any).kenwood_link || 'usb') === 'usb') && (<>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.kenwoodPort')}</Label>
|
||||
|
||||
Reference in New Issue
Block a user