feat: serial-line CW keyer (DTR=CW / RTS=PTT) — SCU-17 / N1MM style
Adds a 4th CW keyer engine alongside K1EL WinKeyer, Icom CI-V and Flex CWX:
OpsLog bit-bangs the Morse itself on a serial control line (DTR keys CW, RTS
keys PTT — swappable), the hardware-keying method N1MM/WSJT use with a Yaesu
SCU-17 and generic interfaces. No WinKeyer chip needed.
- internal/winkeyer/serialcw.go: serialKeyer — worker goroutine, morse table,
WPM + Farnsworth timing (live speed changes), abortable, PTT lead-in/tail/hang.
- winkeyer.Config gains Type ("k1el"|"serial") + CWKeyLine ("dtr"|"rts");
Manager routes Connect/Send/Stop/SetSpeed/Backspace/Disconnect to it.
- app.go: keyWKCWLine setting; WinkeyerConnect sets Type from the engine.
- Settings -> CW Keyer: new "Serial port (DTR=CW/RTS=PTT)" engine with COM port,
key-line selector, speed/Farnsworth/lead-in/tail/PTT. Macros, auto-CQ and
<LOGQSO> reuse the WinKeyer path unchanged.
UNTESTED on hardware (no SCU-17 to hand): polarity assumes N1MM convention
(line HIGH = active). Weight not yet honoured; Farnsworth is.
This commit is contained in:
@@ -2130,7 +2130,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' : s.engine === 'flex' ? 'flex' : 'winkeyer');
|
||||
setWkEngine(s.engine === 'icom' ? 'icom' : s.engine === 'flex' ? 'flex' : s.engine === 'serial' ? 'serial' : 'winkeyer');
|
||||
} catch { /* keyer not configured */ }
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1074,13 +1074,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
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; macros: WKMac[];
|
||||
use_ptt: boolean; serial_echo: boolean; cw_key_line: string; macros: WKMac[];
|
||||
};
|
||||
const [wk, setWk] = useState<WKSettings>({
|
||||
enabled: false, engine: 'winkeyer', esc_clears_call: true,
|
||||
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, macros: [],
|
||||
swap: false, autospace: true, use_ptt: false, serial_echo: true, cw_key_line: 'dtr', macros: [],
|
||||
});
|
||||
const [wkPorts, setWkPorts] = useState<string[]>([]);
|
||||
const setWkField = (patch: Partial<WKSettings>) => setWk((s) => ({ ...s, ...patch }));
|
||||
@@ -3023,7 +3023,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Select value={wk.engine} onValueChange={(v) => setWkField({ engine: v })}>
|
||||
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="winkeyer">WinKeyer (serial)</SelectItem>
|
||||
<SelectItem value="winkeyer">WinKeyer (K1EL, serial)</SelectItem>
|
||||
<SelectItem value="serial">Serial port (DTR=CW / RTS=PTT)</SelectItem>
|
||||
<SelectItem value="icom">Icom CI-V (rig keyer)</SelectItem>
|
||||
<SelectItem value="flex">FlexRadio (CWX)</SelectItem>
|
||||
<SelectItem value="tci" disabled>TCI (coming soon)</SelectItem>
|
||||
@@ -3076,6 +3077,61 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : wk.engine === 'serial' ? (
|
||||
<>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
OpsLog keys the Morse itself by toggling a serial control line — the "hardware CW keying" a Yaesu <strong>SCU-17</strong> and generic interfaces use (like N1MM: DTR = CW, RTS = PTT). No WinKeyer chip. Put the rig in CW mode and pick the interface's <strong>keying COM port</strong> below (this is the CW/PTT port, <em>not</em> the CAT port). Speed, Farnsworth, lead-in/tail and PTT apply; paddle mode, ratio and sidetone are WinKeyer-only.
|
||||
</p>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>Keying COM port</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={wk.port || '_'} onValueChange={(v) => setWkField({ port: v === '_' ? '' : v })}>
|
||||
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder="— COM port —" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
|
||||
{wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="ghost" size="sm" className="h-8 px-2" title="Reload ports"
|
||||
onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
|
||||
<ArrowDown className="size-3.5 rotate-90" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>CW key line</Label>
|
||||
<Select value={wk.cw_key_line === 'rts' ? 'rts' : 'dtr'} onValueChange={(v) => setWkField({ cw_key_line: v })}>
|
||||
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dtr">DTR = CW, RTS = PTT</SelectItem>
|
||||
<SelectItem value="rts">RTS = CW, DTR = PTT</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Speed (WPM)</Label>
|
||||
<Input type="number" min={5} max={99} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>Farnsworth</Label>
|
||||
<Input type="number" min={0} max={99} value={wk.farnsworth} onChange={(e) => setWkField({ farnsworth: num(e.target.value, 0) })} className="font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Lead-in (ms)</Label>
|
||||
<Input type="number" min={0} value={wk.lead_in_ms} onChange={(e) => setWkField({ lead_in_ms: num(e.target.value, 10) })} className="font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Tail (ms)</Label>
|
||||
<Input type="number" min={0} value={wk.tail_ms} onChange={(e) => setWkField({ tail_ms: num(e.target.value, 50) })} className="font-mono" />
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer pb-1.5 self-end">
|
||||
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> Key PTT line
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user