diff --git a/app.go b/app.go index 408d5b4..8f492a5 100644 --- a/app.go +++ b/app.go @@ -10359,6 +10359,36 @@ func (a *App) IcomSendCW(text string) error { return err } +// FlexSendCW keys a CW message through the FlexRadio CWX keyer (SmartSDR), so a +// Flex needs no WinKeyer / SmartCAT. Text is already variable-resolved by the UI. +func (a *App) FlexSendCW(text string) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + err := a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SendCW(text) }) + if err != nil { + applog.Printf("flex cw: FlexSendCW(%q) failed: %v", text, err) + } + return err +} + +// FlexStopCW clears the CWX buffer, aborting whatever is being keyed. +func (a *App) FlexStopCW() error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.StopCW() }) +} + +// FlexSetKeySpeed sets the CW keyer speed in WPM (the CWX keyer uses the radio's +// CW speed). +func (a *App) FlexSetKeySpeed(wpm int) error { + if a.cat == nil { + return fmt.Errorf("cat not initialized") + } + return a.cat.FlexDo(func(fc cat.FlexController) error { return fc.SetCWSpeed(wpm) }) +} + // IcomStopCW aborts the CW message currently being sent. func (a *App) IcomStopCW() error { if a.cat == nil { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a8a81e..7319f51 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,6 +36,7 @@ import { GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts, GetWinkeyerStatus, WinkeyerConnect, WinkeyerDisconnect, WinkeyerSend, WinkeyerStop, WinkeyerSetSpeed, WinkeyerBackspace, IcomSendCW, IcomStopCW, IcomSetKeySpeed, IcomSetBreakIn, GetIcomState, + FlexSendCW, FlexStopCW, FlexSetKeySpeed, GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop, StartCWDecoder, StopCWDecoder, SetCWDecoderPitch, ChatAvailable, GetChatHistory, SendChatMessage, GetOnlineOperators, @@ -775,7 +776,7 @@ export default function App() { // CI-V 0x17 (no extra hardware — sends over the CAT connection). Macros, // auto-call and are shared; only the transport differs. const [wkEngine, setWkEngine] = useState('winkeyer'); - const cwSource: 'winkeyer' | 'icom' = wkEngine === 'icom' ? 'icom' : 'winkeyer'; + const cwSource: 'winkeyer' | 'icom' | 'flex' = wkEngine === 'icom' ? 'icom' : wkEngine === 'flex' ? 'flex' : 'winkeyer'; const cwSourceRef = useRef(cwSource); useEffect(() => { cwSourceRef.current = cwSource; }, [cwSource]); // CW break-in (0=OFF, 1=SEMI, 2=FULL) — must be on for the rig's 0x17 keyer to @@ -804,7 +805,9 @@ export default function App() { const wkBusyRef = useRef(false); // live "keyer is sending" flag, for the wait-then-log useEffect(() => { wkBusyRef.current = wkStatus.busy; }, [wkStatus.busy]); useEffect(() => { - const connected = cwSource === 'icom' ? (catState.backend === 'icom' && catState.connected) : wkStatus.connected; + const connected = cwSource === 'icom' ? (catState.backend === 'icom' && catState.connected) + : cwSource === 'flex' ? (catState.backend === 'flex' && catState.connected) + : wkStatus.connected; wkActiveRef.current = wkEnabled && connected; }, [wkEnabled, wkStatus.connected, cwSource, catState.backend, catState.connected]); useEffect(() => { wkEscClearsRef.current = wkEscClears; }, [wkEscClears]); @@ -2036,7 +2039,7 @@ export default function App() { setWkMacros((s.macros ?? []) as WKMacro[]); setWkEscClears(s.esc_clears_call !== false); setWkSendOnType(!!s.send_on_type); - setWkEngine(s.engine === 'icom' ? 'icom' : 'winkeyer'); + setWkEngine(s.engine === 'icom' ? 'icom' : s.engine === 'flex' ? 'flex' : 'winkeyer'); } catch { /* keyer not configured */ } }, []); @@ -2134,11 +2137,13 @@ export default function App() { const keyed = resolved ? resolved + ' ' : resolved; const doLog = //i.test(rawText); // resolveCW strips the token (unknown var → "") const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms)); - if (cwSourceRef.current === 'icom') { - // The rig's keyer gives no busy echo back, so show the text we sent and, - // for , wait the estimated send duration before logging. + if (cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex') { + // The rig keyer (Icom 0x17 / Flex CWX) gives no busy echo we track, so show + // the text we sent and, for , wait the estimated send duration + // before logging. setWkSent(resolved); - await IcomSendCW(keyed).catch((e) => setError(String(e?.message ?? e))); + const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : IcomSendCW; + await sendFn(keyed).catch((e) => setError(String(e?.message ?? e))); if (doLog) { await sleep(Math.round(estimateCwMs(resolved, wkWpm)) + 600); void save(); } return; } @@ -2174,7 +2179,7 @@ export default function App() { // the wait at the ESTIMATED send time (not the busy flag alone): over a // remote/serial-over-IP link the "busy" status lags badly and stays stuck // true for tens of seconds, which made the next CQ fire ~120s late. - if (cwSourceRef.current === 'icom') { + if (cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex') { await sleep(Math.round(estimateCwMs(resolveCW(m.text), wkWpm)) + 300); } else { const capMs = Math.round(estimateCwMs(resolveCW(m.text), wkWpm) * 1.4) + 2500; @@ -2212,7 +2217,10 @@ export default function App() { writeUiPref('opslog.wkAutoCallSecs', String(v)); } // send-on-type: key the typed chars verbatim (no variable substitution). - function wkSendRaw(chars: string) { WinkeyerSend(chars).catch(() => {}); } + function wkSendRaw(chars: string) { + if (cwSourceRef.current === 'flex') { FlexSendCW(chars).catch(() => {}); return; } + WinkeyerSend(chars).catch(() => {}); + } function wkBackspace() { WinkeyerBackspace().catch(() => {}); } function wkToggleSendOnType(on: boolean) { setWkSendOnType(on); saveWk({ send_on_type: on }); } @@ -4406,8 +4414,8 @@ export default function App() { {wkEnabled && (
{ setWkWpm(w); saveWk({ wpm: w }); if (cwSource === 'icom') IcomSetKeySpeed(w).catch(() => {}); + else if (cwSource === 'flex') FlexSetKeySpeed(w).catch(() => {}); else WinkeyerSetSpeed(w).catch(() => {}); }} onSend={wkSend} onSendMacro={wkSendMacro} - onStop={() => { stopAutoCall(); if (cwSource === 'icom') IcomStopCW().catch(() => {}); else WinkeyerStop().catch(() => {}); }} + onStop={() => { stopAutoCall(); + if (cwSource === 'icom') IcomStopCW().catch(() => {}); + else if (cwSource === 'flex') FlexStopCW().catch(() => {}); + else WinkeyerStop().catch(() => {}); }} onClose={() => wkSetEnabled(false)} sendOnType={wkSendOnType} onToggleSendOnType={wkToggleSendOnType} diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 03fc63e..1ad03d6 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -2807,6 +2807,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan WinKeyer (serial) Icom CI-V (rig keyer) + FlexRadio (CWX) TCI (coming soon) @@ -2837,6 +2838,26 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
+ ) : wk.engine === 'flex' ? ( + <> +

+ 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). +

+ {(!catCfg.enabled || catCfg.backend !== 'flex') && ( +

+ + + Your CAT backend is set to {catCfg.enabled ? (catCfg.backend || 'none') : 'disabled'}. Flex CWX needs the CAT backend set to FlexRadio and connected — change it under Settings → CAT interface, otherwise sending CW will fail. + +

+ )} +
+
+ + setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" /> +
+
+ ) : ( <>
diff --git a/frontend/src/components/WinkeyerPanel.tsx b/frontend/src/components/WinkeyerPanel.tsx index f25a794..816500e 100644 --- a/frontend/src/components/WinkeyerPanel.tsx +++ b/frontend/src/components/WinkeyerPanel.tsx @@ -26,7 +26,7 @@ interface Props { wpm: number; macros: WKMacro[]; sent: string; // text echoed back by the keyer as it transmits - source: 'winkeyer' | 'icom'; // CW output engine (chosen in Settings → CW Keyer) + source: 'winkeyer' | 'icom' | 'flex'; // CW output engine (chosen in Settings → CW Keyer) breakIn?: number; // Icom CW break-in: 0=OFF, 1=SEMI, 2=FULL onSetBreakIn?: (mode: number) => void; onSelectPort: (p: string) => void; @@ -101,14 +101,16 @@ export function WinkeyerPanel({ {/* CW output engine (chosen in Settings → CW Keyer). */} - {source === 'icom' ? 'Icom CW' : 'WinKeyer'} + {source === 'icom' ? 'Icom CW' : source === 'flex' ? 'Flex CWX' : 'WinKeyer'}
- {source === 'icom' ? ( + {source === 'icom' || source === 'flex' ? ( - {connected ? t('wkp.civReady') : t('wkp.civOffline')} + {source === 'flex' + ? (connected ? t('wkp.cwxReady') : t('wkp.cwxOffline')) + : (connected ? t('wkp.civReady') : t('wkp.civOffline'))} ) : !connected ? ( <> @@ -183,7 +185,7 @@ export function WinkeyerPanel({