feat: ESM (Enter Sends Message) for CW keyers, N1MM-style
With ESM enabled (Settings → CW Keyer), the keyer active and mode CW, Enter in the entry strip fires a macro by QSO stage instead of logging: - callsign field empty → F1 (CQ) - callsign entered (in call field) → F2 (report), then focus jumps to RST-sent - Enter in RST-sent / RST-rcvd → F3 (TU), which logs via its own <LOGQSO> Works for every keyer engine (WinKeyer / serial DTR-RTS / Flex CWX / Icom CI-V) since it routes through the shared macro sender. Enter in other fields still logs normally, and ESM off keeps the classic Enter-to-log behaviour. Backend: new `esm` flag on WinkeyerSettings (winkeyer.esm). Frontend: esmHandleEnter state machine keyed off data-esm markers on the call/RST blocks, a Settings checkbox + hint, and i18n EN/FR.
This commit is contained in:
+36
-3
@@ -840,6 +840,9 @@ export default function App() {
|
||||
const [wkSent, setWkSent] = useState(''); // rolling text the keyer echoes as it transmits
|
||||
const [wkEscClears, setWkEscClears] = useState(true); // ESC also clears the callsign
|
||||
const [wkSendOnType, setWkSendOnType] = useState(false); // key chars live as typed
|
||||
const [wkEsm, setWkEsm] = useState(false); // Enter-Sends-Message (N1MM-style CW flow)
|
||||
const wkEsmRef = useRef(false);
|
||||
useEffect(() => { wkEsmRef.current = wkEsm; }, [wkEsm]);
|
||||
// CW keyer output engine (persisted in the WinKeyer settings, chosen in
|
||||
// Settings → CW Keyer): the WinKeyer hardware, or the Icom rig's own keyer via
|
||||
// CI-V 0x17 (no extra hardware — sends over the CAT connection). Macros,
|
||||
@@ -2237,6 +2240,7 @@ export default function App() {
|
||||
setWkMacros((s.macros ?? []) as WKMacro[]);
|
||||
setWkEscClears(s.esc_clears_call !== false);
|
||||
setWkSendOnType(!!s.send_on_type);
|
||||
setWkEsm(!!s.esm);
|
||||
setWkEngine(s.engine === 'icom' ? 'icom' : s.engine === 'flex' ? 'flex' : s.engine === 'serial' ? 'serial' : 'winkeyer');
|
||||
} catch { /* keyer not configured */ }
|
||||
}, []);
|
||||
@@ -2443,6 +2447,32 @@ export default function App() {
|
||||
WinkeyerBackspace().catch(() => {});
|
||||
}
|
||||
function wkToggleSendOnType(on: boolean) { setWkSendOnType(on); saveWk({ send_on_type: on }); }
|
||||
function wkToggleEsm(on: boolean) { setWkEsm(on); saveWk({ esm: on }); }
|
||||
// ESM (Enter Sends Message): N1MM-style CW flow. When ESM is on, the CW keyer is
|
||||
// active and we're in CW, Enter fires a macro by QSO stage instead of logging:
|
||||
// • callsign empty → F1 (CQ)
|
||||
// • callsign entered → F2 (report), then focus jumps to RST-sent
|
||||
// • focus in RST-sent/rcvd → F3 (TU) — which logs IF the macro has <LOGQSO>
|
||||
// Returns true when it handled the Enter (so the caller skips the plain log).
|
||||
function esmHandleEnter(target: HTMLElement): boolean {
|
||||
if (!(wkEsmRef.current && wkActiveRef.current && (mode || '').toUpperCase().includes('CW'))) return false;
|
||||
const field = target.closest('[data-esm]')?.getAttribute('data-esm');
|
||||
if (field === 'rsttx' || field === 'rstrx') {
|
||||
wkSendMacro(2); // F3 (TU) — logs via its own <LOGQSO> if present
|
||||
return true;
|
||||
}
|
||||
if (field === 'call') {
|
||||
if (callsignValRef.current.trim() === '') {
|
||||
wkSendMacro(0); // F1 (CQ)
|
||||
return true;
|
||||
}
|
||||
wkSendMacro(1); // F2 (report)
|
||||
// Move focus to RST-sent so the next Enter fires F3.
|
||||
(document.querySelector('[data-esm="rsttx"] input') as HTMLInputElement | null)?.focus();
|
||||
return true;
|
||||
}
|
||||
return false; // any other field → normal behaviour (log on Enter)
|
||||
}
|
||||
|
||||
// Resolve slot status for any spot we haven't seen yet — debounced so we
|
||||
// don't hammer the backend at firehose rate. The mode passed to the
|
||||
@@ -3211,7 +3241,7 @@ export default function App() {
|
||||
// them as shared consts avoids duplicating the (large) per-field JSX +
|
||||
// handlers across the two layouts.
|
||||
const callsignBlock = (
|
||||
<div className="flex flex-col w-44">
|
||||
<div className="flex flex-col w-44" data-esm="call">
|
||||
<Label className="flex items-center gap-2 h-3.5" style={{ marginBottom: 6 }}>
|
||||
{t('field.callsign')}
|
||||
{lookupBusy && (
|
||||
@@ -3284,12 +3314,12 @@ export default function App() {
|
||||
</div>
|
||||
);
|
||||
const rstTxBlock = (
|
||||
<div className="flex flex-col w-20"><Label className="mb-1 h-3.5">{t('field.rstTx')}</Label>
|
||||
<div className="flex flex-col w-20" data-esm="rsttx"><Label className="mb-1 h-3.5">{t('field.rstTx')}</Label>
|
||||
<Combobox value={rstSent} options={rstOptions(mode, rstLists)} commitOnType onChange={(v) => { setRstSent(v); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
const rstRxBlock = (
|
||||
<div className="flex flex-col w-20"><Label className="mb-1 h-3.5">{t('field.rstRx')}</Label>
|
||||
<div className="flex flex-col w-20" data-esm="rstrx"><Label className="mb-1 h-3.5">{t('field.rstRx')}</Label>
|
||||
<Combobox value={rstRcvd} options={rstOptions(mode, rstLists)} commitOnType onChange={(v) => { setRstRcvd(v); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
@@ -4561,6 +4591,9 @@ export default function App() {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.target as HTMLElement).tagName === 'INPUT') {
|
||||
e.preventDefault();
|
||||
// ESM (Enter Sends Message): fire the stage-appropriate CW macro instead
|
||||
// of logging. Falls through to the normal log when ESM isn't active.
|
||||
if (esmHandleEnter(e.target as HTMLElement)) return;
|
||||
save();
|
||||
}
|
||||
// ESC is handled globally (stop CW + optional callsign reset).
|
||||
|
||||
@@ -1076,14 +1076,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
// WinKeyer CW keyer settings + macro editor.
|
||||
type WKMac = { label: string; text: string };
|
||||
type WKSettings = {
|
||||
enabled: boolean; engine: string; esc_clears_call: boolean;
|
||||
enabled: boolean; engine: string; esc_clears_call: boolean; esm: boolean;
|
||||
port: string; baud: number; wpm: number; weight: number;
|
||||
lead_in_ms: number; tail_ms: number; ratio: number; farnsworth: number;
|
||||
sidetone_hz: number; mode: string; swap: boolean; autospace: boolean;
|
||||
use_ptt: boolean; serial_echo: boolean; cw_key_line: string; cw_invert: boolean; macros: WKMac[];
|
||||
};
|
||||
const [wk, setWk] = useState<WKSettings>({
|
||||
enabled: false, engine: 'winkeyer', esc_clears_call: true,
|
||||
enabled: false, engine: 'winkeyer', esc_clears_call: true, esm: false,
|
||||
port: '', baud: 1200, wpm: 25, weight: 50, lead_in_ms: 10,
|
||||
tail_ms: 50, ratio: 50, farnsworth: 0, sidetone_hz: 600, mode: 'iambic_b',
|
||||
swap: false, autospace: true, use_ptt: false, serial_echo: true, cw_key_line: 'dtr', cw_invert: false, macros: [],
|
||||
@@ -3106,6 +3106,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Checkbox checked={wk.esc_clears_call} onCheckedChange={(c) => setWkField({ esc_clears_call: !!c })} />
|
||||
{t('wk.escClears')}
|
||||
</label>
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer col-span-3 pb-1.5">
|
||||
<Checkbox checked={wk.esm} onCheckedChange={(c) => setWkField({ esm: !!c })} className="mt-0.5" />
|
||||
<span>
|
||||
{t('wk.esm')}
|
||||
<span className="block text-xs text-muted-foreground">{t('wk.esmHint')}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{wk.engine === 'icom' ? (
|
||||
|
||||
@@ -143,6 +143,8 @@ const en: Dict = {
|
||||
// CW Keyer settings panel
|
||||
'wk.enable': 'Enable CW keyer (shows the keyer panel)', 'wk.engine': 'Keyer engine',
|
||||
'wk.escClears': 'ESC clears the callsign too (otherwise ESC only stops transmission)',
|
||||
'wk.esm': 'ESM — Enter Sends Message (CW)',
|
||||
'wk.esmHint': 'In CW with the keyer on, Enter fires a macro by QSO stage instead of logging: empty callsign → F1 (CQ); callsign entered → F2 (report) and focus jumps to RST; Enter in RST → F3 (TU), which logs if the macro contains <LOGQSO>.',
|
||||
'wk.engWinkeyer': 'WinKeyer (K1EL, serial)', 'wk.engSerial': 'Serial port (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (rig keyer)', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (coming soon)',
|
||||
'wk.icomNote': "Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).",
|
||||
'wk.flexNote': "FlexRadio keys CW through the radio's CWX keyer over the existing SmartSDR CAT connection — no WinKeyer or SmartCAT needed. It reuses the connection set in Settings → CAT, so there's nothing else to wire up here. Put a slice in CW mode. Only the speed is set from here; weight, sidetone and break-in are configured on the radio (break-in must be on for CW to actually transmit).",
|
||||
@@ -517,6 +519,8 @@ const fr: Dict = {
|
||||
// Panneau Manipulateur CW
|
||||
'wk.enable': 'Activer le manipulateur CW (affiche le panneau)', 'wk.engine': 'Moteur du manipulateur',
|
||||
'wk.escClears': "ÉCHAP efface aussi l'indicatif (sinon ÉCHAP arrête seulement la transmission)",
|
||||
'wk.esm': 'ESM — Entrée envoie le message (CW)',
|
||||
'wk.esmHint': "En CW avec le keyer actif, Entrée envoie un macro selon l'étape du QSO au lieu de loguer : indicatif vide → F1 (CQ) ; indicatif saisi → F2 (report) et le focus passe au RST ; Entrée dans le RST → F3 (TU), qui logue si le macro contient <LOGQSO>.",
|
||||
'wk.engWinkeyer': 'WinKeyer (K1EL, série)', 'wk.engSerial': 'Port série (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (keyer de la radio)', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (bientôt)',
|
||||
'wk.icomNote': "L'Icom CI-V manipule la CW via le keyer interne de la radio sur la connexion CAT existante (commande 0x17) — il réutilise le port COM CAT défini dans Réglages → CAT, rien d'autre à câbler ici. Mets la radio en mode CW. Poids, ratio, sidetone, mode paddle… se règlent sur la radio ; seule la vitesse est définie ici (KEY SPEED de la radio).",
|
||||
'wk.flexNote': "FlexRadio manipule la CW via le keyer CWX de la radio sur la connexion SmartSDR CAT existante — pas besoin de WinKeyer ni de SmartCAT. Il réutilise la connexion définie dans Réglages → CAT, rien d'autre à câbler ici. Mets une slice en mode CW. Seule la vitesse est définie ici ; poids, sidetone et break-in se règlent sur la radio (le break-in doit être activé pour que la CW parte vraiment).",
|
||||
|
||||
@@ -3085,6 +3085,7 @@ export namespace main {
|
||||
engine: string;
|
||||
esc_clears_call: boolean;
|
||||
send_on_type: boolean;
|
||||
esm: boolean;
|
||||
macros: WKMacro[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
@@ -3114,6 +3115,7 @@ export namespace main {
|
||||
this.engine = source["engine"];
|
||||
this.esc_clears_call = source["esc_clears_call"];
|
||||
this.send_on_type = source["send_on_type"];
|
||||
this.esm = source["esm"];
|
||||
this.macros = this.convertValues(source["macros"], WKMacro);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user