feat: record CW QSOs again

CW was excluded because SmartSDR does not route the operator's own sidetone
through DAX, so the recording sounds one-sided. The audio path is open all the
same and the other station IS captured — which is the half worth keeping.

The frontend had ONE list serving two purposes: which modes may be recorded, and
which modes the voice keyer may transmit on. Adding CW to it would have let the
DVK key the rig with speech on a CW frequency. They are now two lists: PHONE_MODES
for the keyer, RECORDABLE_MODES (phone + CW) for the recorder.

Digital modes stay out of both: their audio is a modem tone nobody will replay.
This commit is contained in:
2026-07-30 23:13:21 +02:00
parent b6465ddc9d
commit e152ef0ee0
3 changed files with 23 additions and 10 deletions
+9 -6
View File
@@ -7107,14 +7107,17 @@ func (a *App) qsoRecDir() string {
// be e-mailed later), and auto-sends it to the contacted operator when enabled // be e-mailed later), and auto-sends it to the contacted operator when enabled
// and an e-mail is known. Called right after a QSO is inserted (manual + UDP); // and an e-mail is known. Called right after a QSO is inserted (manual + UDP);
// q must have its ID set. // q must have its ID set.
// recordableMode reports whether a QSO mode is worth an audio recording // recordableMode reports whether a QSO mode is worth an audio recording: voice
// only voice (SSB/AM/FM) and CW. Digital modes (FT8/FT4/RTTY/PSK/JT…) carry no // and CW. Digital modes (FT8/FT4/RTTY/PSK/JT…) carry only modem tones, which
// useful audio, so they are never recorded. // nobody will ever replay, so they are never recorded.
//
// CW was excluded for a while because SmartSDR does not route the operator's own
// sidetone through DAX, making the recording sound one-sided. The audio path is
// open all the same and the other station IS captured — which is the half worth
// keeping. Operator's call.
func recordableMode(mode string) bool { func recordableMode(mode string) bool {
switch strings.ToUpper(strings.TrimSpace(mode)) { switch strings.ToUpper(strings.TrimSpace(mode)) {
// CW is intentionally excluded: SmartSDR doesn't route CW audio through DAX, case "SSB", "USB", "LSB", "AM", "FM", "DV", "CW":
// so the recording is empty/useless. Phone modes only.
case "SSB", "USB", "LSB", "AM", "FM", "DV":
return true return true
} }
return false return false
+4 -2
View File
@@ -3,10 +3,12 @@
"version": "0.22.4", "version": "0.22.4",
"date": "", "date": "",
"en": [ "en": [
"FlexRadio: in split, OpsLog stays on the receive slice instead of following the transmitter." "FlexRadio: in split, OpsLog stays on the receive slice instead of following the transmitter.",
"QSO recording works on CW again. Your own sidetone is not in the file, the other station is."
], ],
"fr": [ "fr": [
"FlexRadio : en split, OpsLog reste sur la slice de réception au lieu de suivre l'émission." "FlexRadio : en split, OpsLog reste sur la slice de réception au lieu de suivre l'émission.",
"L'enregistrement des QSO fonctionne à nouveau en CW. Votre propre signal d'écoute n'est pas dans le fichier, celui du correspondant l'est."
] ]
}, },
{ {
+10 -2
View File
@@ -137,7 +137,15 @@ const DEFAULT_MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALV
// Modes the QSO recorder captures (phone only). Mirrors recordableMode() in // Modes the QSO recorder captures (phone only). Mirrors recordableMode() in
// app.go — digital modes carry no useful audio, and CW has no DAX audio on Flex, // app.go — digital modes carry no useful audio, and CW has no DAX audio on Flex,
// so neither is recorded (no REC badge / timer for them). // so neither is recorded (no REC badge / timer for them).
const RECORDABLE_MODES = new Set(['SSB','USB','LSB','AM','FM','DV']); // Modes a VOICE keyer may transmit on. Not a recording list — the DVK plays
// speech, and sending it on CW or a data slot keys the rig with a human voice.
const PHONE_MODES = new Set(['SSB','USB','LSB','AM','FM','DV']);
// Modes worth recording. Phone, plus CW: the audio path stays open in CW, so
// the other station's signal is captured. Your own sidetone is not in it —
// SmartSDR does not route it through DAX — and that is accepted. Digital modes
// stay out: their audio is a modem tone nobody will ever replay.
const RECORDABLE_MODES = new Set([...PHONE_MODES, 'CW']);
const emptyDetails: DetailsState = { const emptyDetails: DetailsState = {
state: '', cnty: '', address: '', state: '', cnty: '', address: '',
@@ -1233,7 +1241,7 @@ export default function App() {
useEffect(() => { dvkAutoCqSecsRef.current = dvkAutoCqSecs; localStorage.setItem('opslog.dvkAutoCqSecs', String(dvkAutoCqSecs)); }, [dvkAutoCqSecs]); useEffect(() => { dvkAutoCqSecsRef.current = dvkAutoCqSecs; localStorage.setItem('opslog.dvkAutoCqSecs', String(dvkAutoCqSecs)); }, [dvkAutoCqSecs]);
// The DVK is a VOICE keyer — transmitting it on CW/FT8/RTTY would key the rig // The DVK is a VOICE keyer — transmitting it on CW/FT8/RTTY would key the rig
// with speech on a data slot. Only allow it on phone modes. // with speech on a data slot. Only allow it on phone modes.
const isPhoneMode = (m: string) => RECORDABLE_MODES.has((m || '').toUpperCase()); const isPhoneMode = (m: string) => PHONE_MODES.has((m || '').toUpperCase());
const modeRef = useRef(mode); const modeRef = useRef(mode);
useEffect(() => { modeRef.current = mode; }, [mode]); useEffect(() => { modeRef.current = mode; }, [mode]);
function stopDvkAutoCq() { dvkAutoCqSlotRef.current = -1; dvkAutoCqGenRef.current++; } function stopDvkAutoCq() { dvkAutoCqSlotRef.current = -1; dvkAutoCqGenRef.current++; }