feat(cat): USB for digital modes, on every backend

A soundcard mode is USB with audio in the mic path, and that is what
most rigs need. Asking for the mode by NAME is the better answer on a
modern transceiver with a DATA position and the wrong one on an older
set, where the CAT layer resolves 'digital' to RTTY/FSK — OmniRig does
exactly that, per rig file, and there is no arguing with it from here.
The operator clicking an FT8 spot landed in FSK, which keys from a
mark/space generator and cannot pass FT8 at all.

Applied in SetCATMode, the one funnel every backend and every caller
goes through, so it holds for a spot click, the band map and the mode
selector alike. The QSO is still logged as FT8: this is what the radio
is put in, not what the contact was.
This commit is contained in:
2026-08-31 22:26:12 +02:00
parent 22e4266d38
commit de0771d797
5 changed files with 53 additions and 8 deletions
+37 -3
View File
@@ -116,7 +116,10 @@ const (
keyCATBackend = "cat.backend" // "omnirig" | "flex"
keyCATOmniRigNum = "cat.omnirig.rig" // 1 or 2
// Which VFO to believe when OmniRig names one. "" = trust the rig file.
keyCATOmniRigVFO = "cat.omnirig.vfo" // "" | "A" | "B"
keyCATOmniRigVFO = "cat.omnirig.vfo" // "" | "A" | "B"
// Put the RADIO in USB for a digital mode instead of asking for the mode by
// name. Every backend, because the reason is the radio, not the link.
keyCATDigiUSB = "cat.digi_usb"
keyCATFlexHost = "cat.flex.host" // FlexRadio IP (native backend)
keyCATFlexPort = "cat.flex.port" // FlexRadio TCP port (default 4992)
keyCATFlexSpots = "cat.flex.spots" // push cluster spots to the panadapter
@@ -473,7 +476,10 @@ type CATSettings struct {
// reports, "A"/"B" force one. Needed because that report is only as good as
// the .ini: an IC-7610 file was seen declaring VFO B permanently while the
// operator worked on the main VFO, so OpsLog wrote to A and read B.
OmniRigVFO string `json:"omnirig_vfo"` // "" | "A" | "B"
OmniRigVFO string `json:"omnirig_vfo"` // "" | "A" | "B"
// DigiAsUSB puts the radio in USB when a digital mode is selected, rather
// than naming the mode. What the QSO is LOGGED as never changes.
DigiAsUSB bool `json:"digi_as_usb"`
FlexHost string `json:"flex_host"` // FlexRadio IP (native backend)
FlexPort int `json:"flex_port"` // FlexRadio TCP port (default 4992)
FlexSpots bool `json:"flex_spots"` // push cluster spots to the panadapter
@@ -8235,7 +8241,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if a.settings == nil {
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDVKDax, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATKenwoodLink, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATKenwoodDataMode, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPttHotkeyEnabled, keyCATPttHotkey, keyCATPttHotkeyToggle, keyCATPollMs, keyCATDelayMs, keyCATOffsetOn, keyCATOffsetHz, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort, keyCATShareProto, keyCATShareTCIPort)
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDVKDax, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATXieguPort, keyCATXieguBaud, keyCATXieguAddr, keyCATXieguPTTLine, keyCATYaesuPort, keyCATYaesuBaud, keyCATKenwoodPort, keyCATKenwoodBaud, keyCATKenwoodHost, keyCATKenwoodLink, keyCATYaesuLowLines, keyCATKenwoodLowLines, keyCATKenwoodDataMode, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPttHotkeyEnabled, keyCATPttHotkey, keyCATPttHotkeyToggle, keyCATPollMs, keyCATDelayMs, keyCATOffsetOn, keyCATOffsetHz, keyCATDigitalDefault, keyCATShareEnabled, keyCATSharePort, keyCATShareProto, keyCATShareTCIPort, keyCATDigiUSB)
if err != nil {
return CATSettings{}, err
}
@@ -8336,6 +8342,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if v := strings.ToUpper(strings.TrimSpace(m[keyCATOmniRigVFO])); v == "A" || v == "B" {
out.OmniRigVFO = v
}
out.DigiAsUSB = m[keyCATDigiUSB] == "1"
if n, _ := strconv.Atoi(m[keyCATOmniRigNum]); n == 1 || n == 2 {
out.OmniRigNum = n
}
@@ -8444,6 +8451,7 @@ func (a *App) SaveCATSettings(s CATSettings) error {
keyCATBackend: s.Backend,
keyCATOmniRigNum: strconv.Itoa(s.OmniRigNum),
keyCATOmniRigVFO: strings.ToUpper(strings.TrimSpace(s.OmniRigVFO)),
keyCATDigiUSB: boolStr(s.DigiAsUSB),
keyCATFlexHost: strings.TrimSpace(s.FlexHost),
keyCATFlexPort: strconv.Itoa(s.FlexPort),
keyCATFlexSpots: flexSpots,
@@ -14828,6 +14836,7 @@ func (a *App) SetCATMode(mode string) error {
if a.cat == nil {
return fmt.Errorf("cat not initialized")
}
mode = a.catModeForRadio(mode)
err := a.cat.SetMode(mode)
if err != nil {
applog.Printf("cat: SetMode(%q) dispatch error: %v", mode, err)
@@ -14835,6 +14844,31 @@ func (a *App) SetCATMode(mode string) error {
return err
}
// catModeForRadio translates a LOGGED mode into what the RADIO should be put
// in, for operators who have asked for USB on digital.
//
// The soundcard modes — FT8, FT4, PSK, JS8, Q65… — are all USB with audio in
// the microphone path, and that is what most rigs need. Asking for the mode by
// name is the better answer on a modern transceiver with a DATA/PKT position,
// and the wrong one on an older set where the CAT layer resolves "digital" to
// RTTY/FSK: the operator lands in FSK, which keys the radio from a mark/space
// generator and cannot pass FT8 at all. OmniRig does exactly this, per rig
// file, and there is no arguing with it from here — so the option sends what
// the radio can actually do.
//
// This is the RADIO's mode only. The QSO is still logged as FT8: the mode is
// what the contact WAS, not what the front panel says.
func (a *App) catModeForRadio(mode string) string {
if strings.TrimSpace(mode) == "" || a.settingOr(keyCATDigiUSB, "") != "1" {
return mode
}
if qso.ModeClass(mode) != "DIG" {
return mode
}
applog.Printf("cat: %s → USB (digital modes set the radio to USB)", strings.ToUpper(mode))
return "USB"
}
// ── FlexRadio control tab (Phase 1: SmartSDR-style transmit controls) ──
// These are no-ops / errors unless the active CAT backend is a FlexRadio.
+4 -2
View File
@@ -8,7 +8,8 @@
"Outbound ADIF (forwarding a logged QSO to another logger such as Log4OM): the receive side is filled in when the contact was not split, so BAND_RX and FREQ_RX are present. The importer already did this; the logging paths did not, so what a QSO carried depended on which door it came in through.",
"HamQTH joins the places the other services already were: two Recent-QSOs columns (sent status and date), the QSO filter, and bulk edit — the last one so a log uploaded to HamQTH by hand can be marked as sent instead of being offered for upload all over again.",
"HamQTH whole-log upload: it reports itself in the console like every other action — the callsign it is scoped to, how many of the logbooks QSOs that leaves, the file size, and HamQTHs own reply — and says plainly that HamQTH imports the file in the background and e-mails any ADIF errors, so a site count that lags or stops short is explained rather than mysterious.",
"QSO editor: correcting a frequency now moves its band with it, TX and RX — a QSO fixed to 7.1 MHz no longer stays filed on 20m. The band is only touched when the frequency lands in a known allocation, so a half-typed number never blanks it."
"QSO editor: correcting a frequency now moves its band with it, TX and RX — a QSO fixed to 7.1 MHz no longer stays filed on 20m. The band is only touched when the frequency lands in a known allocation, so a half-typed number never blanks it.",
"CAT: an option to put the radio in USB for digital modes (Settings → CAT), for every backend. Clicking an FT8 spot on a rig whose CAT layer resolves “digital” to RTTY/FSK — OmniRig does, per rig file — landed the operator in FSK, which cannot pass FT8 at all. The QSO is still logged as FT8: only the radio changes."
],
"fr": [
"DX Cluster : un serveur déconnecté garde sa pastille et peut donc être reconnecté — le déconnecter le faisait disparaître avec le seul moyen dy revenir.",
@@ -16,7 +17,8 @@
"ADIF sortant (transfert dun QSO vers un autre log, Log4OM par exemple) : le côté réception est renseigné quand le contact n’était pas en split, donc BAND_RX et FREQ_RX sont présents. Limport le faisait déjà, pas les chemins de log — ce quun QSO transportait dépendait donc de la porte par laquelle il était entré.",
"HamQTH rejoint les endroits où les autres services étaient déjà : deux colonnes dans les QSO récents (statut et date denvoi), le filtre de QSO et l’édition groupée — cette dernière pour quun log envoyé à la main sur HamQTH puisse être marqué comme envoyé au lieu d’être reproposé à lenvoi.",
"Envoi du log complet HamQTH : il rend compte dans la console comme toutes les autres actions — lindicatif retenu, combien de QSO du journal cela représente, la taille du fichier et la réponse de HamQTH — et indique clairement que HamQTH importe le fichier en arrière-plan et envoie les erreurs ADIF par e-mail : un compteur en retard ou incomplet sur le site est ainsi expliqué au lieu d’être mystérieux.",
"Éditeur de QSO : corriger une fréquence déplace désormais sa bande avec elle, TX comme RX — un QSO corrigé à 7,1 MHz ne reste plus classé en 20m. La bande nest touchée que si la fréquence tombe dans une allocation connue : un nombre à moitié tapé ne lefface jamais."
"Éditeur de QSO : corriger une fréquence déplace désormais sa bande avec elle, TX comme RX — un QSO corrigé à 7,1 MHz ne reste plus classé en 20m. La bande nest touchée que si la fréquence tombe dans une allocation connue : un nombre à moitié tapé ne lefface jamais.",
"CAT : une option pour mettre la radio en USB sur les modes numériques (Réglages → CAT), pour tous les backends. Cliquer un spot FT8 sur un poste dont la couche CAT traduit « numérique » par RTTY/FSK — cest le cas dOmniRig, selon le fichier radio — faisait basculer lopérateur en FSK, incapable de passer du FT8. Le QSO reste enregistré en FT8 : seule la radio change."
]
},
{
+8 -1
View File
@@ -1612,7 +1612,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
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, offset_on: false, offset_hz: 0,
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,
ptt_hotkey_enabled: false, ptt_hotkey: '', ptt_hotkey_toggle: false, digi_as_usb: false,
});
// Brand + connection, derived from the stored backend rather than held
// separately: two sources for one fact drift apart the first time something
@@ -3840,6 +3840,13 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
</label> </>
)}
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('cat.digiUsbHint')}>
<Checkbox
checked={!!catCfg.digi_as_usb}
onCheckedChange={(c) => setCatCfg((s) => ({ ...s, digi_as_usb: !!c }))}
/>
{t('cat.digiUsb')}
</label>
{catCfg.backend === 'omnirig' && (
<>
<label className="flex items-center gap-2 text-sm cursor-pointer">
+2 -2
View File
@@ -384,7 +384,7 @@ const en: Dict = {
'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.offsetOn': 'Transverter offset', 'cat.offsetHint': '(the rig shows its IF; OpsLog logs, spots and tunes on the real band)', 'cat.offsetMhz': 'Offset (MHz)', 'cat.offsetExample': 'e.g. 116 for a 28 MHz IF on 144 MHz — negative is allowed',
'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.pollMs': 'Poll interval (ms)', 'cat.delayMs': 'CAT delay (ms)', 'cat.digitalDefault': 'Default digital mode (when rig reports DIG)', 'cat.digiUsb': 'Set the radio to USB for digital modes', 'cat.digiUsbHint': 'FT8, FT4, PSK… put the transceiver in USB instead of asking for the mode by name — for rigs whose CAT layer resolves “digital” to RTTY/FSK. The QSO is still logged as FT8.', '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).',
'cat.rotatorOk': "Packet sent — antenna should swing to 0° (north). If it didn't, check PstRotator host/port and that PstRotator's UDP listener is enabled.",
'cat.ubOk': 'Connected — the antenna responded with a status frame.',
@@ -896,7 +896,7 @@ const fr: Dict = {
'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 ladresse 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.offsetOn': 'Décalage transverter', 'cat.offsetHint': "(le poste affiche sa FI ; OpsLog logue, spotte et accorde sur la vraie bande)", 'cat.offsetMhz': 'Décalage (MHz)', 'cat.offsetExample': 'p. ex. 116 pour une FI 28 MHz sur 144 MHz — négatif accepté',
'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.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.digiUsb': 'Mettre la radio en USB pour les modes numériques', 'cat.digiUsbHint': 'FT8, FT4, PSK… placent le transceiver en USB au lieu de demander le mode par son nom — pour les postes dont la couche CAT traduit « numérique » par RTTY/FSK. Le QSO reste enregistré en FT8.', '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).",
'cat.rotatorOk': "Paquet envoyé — l'antenne devrait tourner vers 0° (nord). Sinon, vérifie l'hôte/port PstRotator et que l'écouteur UDP de PstRotator est activé.",
'cat.ubOk': "Connecté — l'antenne a répondu avec une trame de statut.",
+2
View File
@@ -2298,6 +2298,7 @@ export namespace main {
backend: string;
omnirig_rig: number;
omnirig_vfo: string;
digi_as_usb: boolean;
flex_host: string;
flex_port: number;
flex_spots: boolean;
@@ -2350,6 +2351,7 @@ export namespace main {
this.backend = source["backend"];
this.omnirig_rig = source["omnirig_rig"];
this.omnirig_vfo = source["omnirig_vfo"];
this.digi_as_usb = source["digi_as_usb"];
this.flex_host = source["flex_host"];
this.flex_port = source["flex_port"];
this.flex_spots = source["flex_spots"];