From de0771d7974328f5228d83e898208716a69708ef Mon Sep 17 00:00:00 2001 From: rouggy Date: Mon, 31 Aug 2026 22:26:12 +0200 Subject: [PATCH] feat(cat): USB for digital modes, on every backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app.go | 40 +++++++++++++++++++++-- changelog.json | 6 ++-- frontend/src/components/SettingsModal.tsx | 9 ++++- frontend/src/lib/i18n.tsx | 4 +-- frontend/wailsjs/go/models.ts | 2 ++ 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 5e67e7a..dc8eda6 100644 --- a/app.go +++ b/app.go @@ -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. diff --git a/changelog.json b/changelog.json index 9881f64..e5331c9 100644 --- a/changelog.json +++ b/changelog.json @@ -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 logbook’s QSOs that leaves, the file size, and HamQTH’s 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 d’y revenir.", @@ -16,7 +17,8 @@ "ADIF sortant (transfert d’un 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. L’import le faisait déjà, pas les chemins de log — ce qu’un 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 d’envoi), le filtre de QSO et l’édition groupée — cette dernière pour qu’un log envoyé à la main sur HamQTH puisse être marqué comme envoyé au lieu d’être reproposé à l’envoi.", "Envoi du log complet HamQTH : il rend compte dans la console comme toutes les autres actions — l’indicatif 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 n’est touchée que si la fréquence tombe dans une allocation connue : un nombre à moitié tapé ne l’efface 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 n’est touchée que si la fréquence tombe dans une allocation connue : un nombre à moitié tapé ne l’efface 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 — c’est le cas d’OmniRig, selon le fichier radio — faisait basculer l’opérateur en FSK, incapable de passer du FT8. Le QSO reste enregistré en FT8 : seule la radio change." ] }, { diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 870ae5b..1d84f6d 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -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 )} + {catCfg.backend === 'omnirig' && ( <>