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:
2026-08-25 17:49:07 +02:00
parent bd540ef18b
commit 712d83a012
3 changed files with 90 additions and 31 deletions
+2 -2
View File
@@ -10,7 +10,7 @@
"PowerGenius XL: the amplifier's real state is read at startup. Its status frame carries no 'operate' field — the state is in 'state' — so on the direct GSCP link the flag was never read at all and OpsLog opened claiming STANDBY on an amp that was in line, with the first press of the button then commanding the state it was already in. IDLE means in line, not keyed.",
"Antenna Genius: an option to write the SELECTED antenna into MY_ANTENNA, under the name it carries on the switch, ahead of the band default from Operating conditions. Which of the two ports counts is decided by the antenna jack the radio is transmitting on (ANT1/ANT2 on a Flex), with the jack-to-port wiring set once in Preferences — neither device can report it. When the port cannot be told, the log keeps the band default rather than naming an antenna at random.",
"The awards tab beside the entry form (F3) now offers only the awards this station follows, and drops the ones switched off — the same list the Awards tab reads. It offered every award that existed, so a station chasing three of them picked references out of a list of twenty.",
"Kenwood / Elecraft CAT: the connection is a choice of three — USB / serial, RS-232 to Ethernet (a serial bridge carrying the same CAT), or the radio's own network protocol, which is named and refused rather than silently absent. Only the fields of the chosen one are shown. Before, the COM port and the network address sat side by side with nothing to say that the address wins whenever it is filled in, and the example address named port 4532 — Hamlib rigctld, a different protocol, and the one OpsLog itself serves under Share CAT."
"CAT settings ask two questions instead of one: WHICH RADIO, then HOW IT IS CONNECTED — USB / serial, RS-232 over Ethernet (a serial bridge carrying the same CAT), or the manufacturer's own network protocol. The old 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), and where an option does not exist for a radio it is now named and refused rather than silently absent. The example network address also named port 4532 — Hamlib rigctld, a different protocol, and the one OpsLog itself serves under Share CAT."
],
"fr": [
"Console Elecraft : les mesures d'émission sont lues dès que la RADIO se déclare en émission, et plus seulement quand OpsLog l'a mise en émission. Passer en émission par le PTT de façade, une pédale ou le bouton du micro laissait le panneau croire à une réception — et comme les barres de puissance et de ROS ne sont lues qu'en émission, elles ne l'étaient jamais pour qui manipule à la main.",
@@ -20,7 +20,7 @@
"Power Genius XL : l'état réel de l'amplificateur est lu au démarrage. Sa trame d'état ne contient pas de champ « operate » — l'état est dans « state » — si bien que sur la liaison GSCP directe l'indicateur n'était jamais lu : OpsLog s'ouvrait en annonçant STANDBY sur un ampli en ligne, et le premier appui commandait l'état dans lequel il se trouvait déjà. IDLE veut dire en ligne, pas en émission.",
"Antenna Genius : une option pour inscrire l'antenne SÉLECTIONNÉE dans MY_ANTENNA, sous le nom qu'elle porte sur le switch, avant l'antenne par défaut des conditions de trafic. C'est la prise d'antenne sur laquelle la radio émet (ANT1/ANT2 sur un Flex) qui décide du port retenu, le câblage prise→port se règlant une fois dans les préférences — aucun des deux appareils ne peut le dire. Quand le port ne peut pas être déterminé, le journal conserve l'antenne par défaut plutôt que d'en nommer une au hasard.",
"L'onglet des diplômes à côté de la saisie (F3) ne propose plus que les diplômes suivis par la station, et écarte ceux qui sont désactivés — la même liste que l'onglet Diplômes. Il proposait tous les diplômes existants : une station qui en chasse trois choisissait ses références dans une liste de vingt.",
"CAT Kenwood / Elecraft : la connexion devient un choix entre trois — USB / série, RS-232 vers Ethernet (un pont série qui transporte le même CAT), ou le protocole réseau propre à la radio, qui est nommé et refusé plutôt qu'absent en silence. Seuls les champs du mode retenu s'affichent. Auparavant, le port COM et l'adresse réseau coexistaient sans rien indiquer que l'adresse l'emporte dès qu'elle est renseignée, et l'exemple citait le port 4532 — Hamlib rigctld, un autre protocole, et celui qu'OpsLog propose lui-même sous « Partager le CAT »."
"Les réglages CAT posent deux questions au lieu d'une : QUELLE RADIO, puis COMMENT ELLE EST RELIÉE — USB / série, RS-232 sur Ethernet (un pont série qui transporte le même CAT), ou le protocole réseau propre au constructeur. L'ancienne liste mélangeait les deux (« Icom (USB) » et « Icom (réseau) » étaient deux entrées, tandis que Kenwood et Elecraft cachaient le même choix dans un champ plus bas), et là où une option n'existe pas pour une radio, elle est désormais nommée et refusée plutôt qu'absente en silence. L'exemple d'adresse réseau citait par ailleurs le port 4532 — Hamlib rigctld, un autre protocole, et celui qu'OpsLog propose lui-même sous « Partager le CAT »."
]
},
{
+86 -27
View File
@@ -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>
File diff suppressed because one or more lines are too long