feat(tci): key CW through the radio's own macro keyer
The sixth CW engine, and the one the TCI backend was left without. A SunSDR
keys from the WebSocket that already carries the CAT: no WinKeyer, no second
serial port, nothing to wire.
CW_MACROS:0,<text>; send
CW_MACROS_SPEED:<wpm>; speed
CW_MACROS_STOP; abort
Confirmed against the TCI command table in ars-ka0s/eesdr-tci — the source that
settled SPOT. CW_MACROS rather than TCI 2.0's CW_MSG: it exists from 1.6, and
OpsLog resolves its own variables, so the before/after callsign fields have
nothing to carry.
Commas and semicolons are stripped before sending. They are the protocol's own
separators: a comma would become another argument, a semicolon would end the
command with the message half keyed and the rest parsed as a command.
Only the MACRO speed is set, not the paddle keyer's — an operator who set their
paddle to 28 wpm did not ask for it to change because a macro went out at 25.
And there is no backspace: TCI can stop a message but cannot un-type one, so the
type-ahead correction the Flex engine offers is absent rather than faked.
This commit is contained in:
+11
-3
@@ -41,6 +41,7 @@ import {
|
||||
IcomSendCW, YaesuSendCW, YaesuStopCW, SetYaesuKeySpeed, IcomStopCW, IcomSetKeySpeed, IcomSetBreakIn, GetIcomState,
|
||||
KenwoodSendCW, KenwoodStopCW, SetKenwoodKeySpeed,
|
||||
FlexSendCW, FlexStopCW, FlexSetKeySpeed, FlexBackspaceCW,
|
||||
TCISendCW, TCIStopCW, TCISetKeySpeed,
|
||||
GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop,
|
||||
StartCWDecoder, StopCWDecoder, SetCWDecoderPitch,
|
||||
ChatAvailable, GetChatHistory, SendChatMessage, GetOnlineOperators,
|
||||
@@ -1490,7 +1491,7 @@ export default function App() {
|
||||
// CI-V 0x17 (no extra hardware — sends over the CAT connection). Macros,
|
||||
// auto-call and <LOGQSO> are shared; only the transport differs.
|
||||
const [wkEngine, setWkEngine] = useState<string>('winkeyer');
|
||||
const cwSource: 'winkeyer' | 'icom' | 'flex' | 'yaesu' | 'kenwood' = wkEngine === 'icom' ? 'icom' : wkEngine === 'flex' ? 'flex' : wkEngine === 'yaesu' ? 'yaesu' : wkEngine === 'kenwood' ? 'kenwood' : 'winkeyer';
|
||||
const cwSource: 'winkeyer' | 'icom' | 'flex' | 'yaesu' | 'kenwood' | 'tci' = wkEngine === 'icom' ? 'icom' : wkEngine === 'flex' ? 'flex' : wkEngine === 'yaesu' ? 'yaesu' : wkEngine === 'kenwood' ? 'kenwood' : wkEngine === 'tci' ? 'tci' : 'winkeyer';
|
||||
// Setting the CW speed has to reach the keyer that is ACTUALLY sending, and
|
||||
// both the CW panel and the Yaesu console can ask for it. With DTR/RTS line
|
||||
// keying the PC does the timing, so the rig's internal keyer speed changes
|
||||
@@ -1504,6 +1505,7 @@ export default function App() {
|
||||
else if (src === 'flex') FlexSetKeySpeed(w).catch(() => {});
|
||||
else if (src === 'yaesu') SetYaesuKeySpeed(w).catch(() => {});
|
||||
else if (src === 'kenwood') SetKenwoodKeySpeed(w).catch(() => {});
|
||||
else if (src === 'tci') TCISetKeySpeed(w).catch(() => {});
|
||||
else WinkeyerSetSpeed(w).catch(() => {});
|
||||
// The rig's own keyer follows too whenever a Yaesu is on CAT, even when it is
|
||||
// not the sending engine: its front panel and OpsLog then agree.
|
||||
@@ -1548,6 +1550,7 @@ export default function App() {
|
||||
: cwSource === 'flex' ? (catState.backend === 'flex' && catState.connected)
|
||||
: cwSource === 'yaesu' ? (catState.backend === 'yaesu' && catState.connected)
|
||||
: cwSource === 'kenwood' ? (catState.backend === 'kenwood' && catState.connected)
|
||||
: cwSource === 'tci' ? (catState.backend === 'tci' && catState.connected)
|
||||
: wkStatus.connected;
|
||||
wkActiveRef.current = wkEnabled && connected;
|
||||
}, [wkEnabled, wkStatus.connected, cwSource, catState.backend, catState.connected]);
|
||||
@@ -3968,7 +3971,7 @@ export default function App() {
|
||||
// segment AFTER the <LOGQSO> (which logs and clears the form) still expands its
|
||||
// variables correctly.
|
||||
const parts = rawText.split(/<LOGQSO>/i).map((pt) => resolveCW(pt));
|
||||
const isRig = cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex' || cwSourceRef.current === 'yaesu' || cwSourceRef.current === 'kenwood';
|
||||
const isRig = cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex' || cwSourceRef.current === 'yaesu' || cwSourceRef.current === 'kenwood' || cwSourceRef.current === 'tci';
|
||||
for (let p = 0; p < parts.length; p++) {
|
||||
if (aborted()) return; // ESC / Stop before this segment → stop sending, don't log
|
||||
const resolved = parts[p];
|
||||
@@ -3980,7 +3983,7 @@ export default function App() {
|
||||
// current WPM, so it scales automatically.
|
||||
const keyed = resolved + ' ';
|
||||
setWkSent(resolved);
|
||||
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : cwSourceRef.current === 'icom' ? IcomSendCW : cwSourceRef.current === 'yaesu' ? YaesuSendCW : cwSourceRef.current === 'kenwood' ? KenwoodSendCW : null;
|
||||
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : cwSourceRef.current === 'icom' ? IcomSendCW : cwSourceRef.current === 'yaesu' ? YaesuSendCW : cwSourceRef.current === 'kenwood' ? KenwoodSendCW : cwSourceRef.current === 'tci' ? TCISendCW : null;
|
||||
if (sendFn) await sendFn(keyed).catch((e) => setError(String(e?.message ?? e)));
|
||||
else await WinkeyerSend(keyed).catch((e) => setError(String(e?.message ?? e)));
|
||||
// WAIT for THIS segment's CW to finish before moving on — so a <LOGQSO>
|
||||
@@ -4023,6 +4026,7 @@ export default function App() {
|
||||
else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'yaesu') YaesuStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'kenwood') KenwoodStopCW().catch(() => {});
|
||||
else if (cwSourceRef.current === 'tci') TCIStopCW().catch(() => {});
|
||||
else WinkeyerStop().catch(() => {});
|
||||
}
|
||||
// runAutoCall sends macro i, waits for the keyer to finish, waits the chosen
|
||||
@@ -4071,6 +4075,10 @@ export default function App() {
|
||||
// send-on-type: key the typed chars verbatim (no variable substitution).
|
||||
function wkSendRaw(chars: string) {
|
||||
if (cwSourceRef.current === 'flex') { FlexSendCW(chars).catch(() => {}); return; }
|
||||
// TCI keys the character as a macro of its own. There is no un-typing it
|
||||
// afterwards — the radio can stop the message but not shorten it — so the
|
||||
// backspace below leaves the TCI engine alone rather than pretending.
|
||||
if (cwSourceRef.current === 'tci') { TCISendCW(chars).catch(() => {}); return; }
|
||||
WinkeyerSend(chars).catch(() => {});
|
||||
}
|
||||
function wkBackspace() {
|
||||
|
||||
@@ -4867,7 +4867,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<SelectItem value="yaesu">{t('wk.engYaesu')}</SelectItem>
|
||||
<SelectItem value="kenwood">{t('wk.engKenwood')}</SelectItem>
|
||||
<SelectItem value="flex">{t('wk.engFlex')}</SelectItem>
|
||||
<SelectItem value="tci" disabled>{t('wk.engTci')}</SelectItem>
|
||||
<SelectItem value="tci">{t('wk.engTci')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -4928,6 +4928,22 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : wk.engine === 'tci' ? (
|
||||
<>
|
||||
{(!catCfg.enabled || catCfg.backend !== 'tci') && (
|
||||
<p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
|
||||
<span aria-hidden>⚠</span>
|
||||
<span>{t('wk.catWarnTci', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground -mt-1">{t('wk.tciHint')}</p>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('wk.speed')}</Label>
|
||||
<Input type="number" min={5} max={60} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : wk.engine === 'flex' ? (
|
||||
<>
|
||||
{(!catCfg.enabled || catCfg.backend !== 'flex') && (
|
||||
|
||||
@@ -26,7 +26,7 @@ interface Props {
|
||||
wpm: number;
|
||||
macros: WKMacro[];
|
||||
sent: string; // text echoed back by the keyer as it transmits
|
||||
source: 'winkeyer' | 'icom' | 'flex' | 'yaesu' | 'kenwood'; // CW output engine (chosen in Settings → CW Keyer)
|
||||
source: 'winkeyer' | 'icom' | 'flex' | 'yaesu' | 'kenwood' | 'tci'; // 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;
|
||||
@@ -109,16 +109,16 @@ export function WinkeyerPanel({
|
||||
<Radio className="size-4 text-primary shrink-0" />
|
||||
{/* CW output engine (chosen in Settings → CW Keyer). */}
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground shrink-0">
|
||||
{source === 'icom' ? 'Icom CW' : source === 'flex' ? 'Flex CWX' : source === 'yaesu' ? 'Yaesu CW' : source === 'kenwood' ? 'Kenwood CW' : 'WinKeyer'}
|
||||
{source === 'icom' ? 'Icom CW' : source === 'flex' ? 'Flex CWX' : source === 'yaesu' ? 'Yaesu CW' : source === 'kenwood' ? 'Kenwood CW' : source === 'tci' ? 'TCI CW' : 'WinKeyer'}
|
||||
</span>
|
||||
<span className={cn('size-2 rounded-full', connected ? (status.busy ? 'bg-warning animate-pulse' : 'bg-success') : 'bg-muted-foreground/40')}
|
||||
title={connected ? (status.busy ? t('wkp.sending') : t('wkp.connectedV', { version: status.version })) : t('wkp.disconnected')} />
|
||||
<div className="flex-1" />
|
||||
{source === 'icom' || source === 'flex' || source === 'yaesu' || source === 'kenwood' ? (
|
||||
{source === 'icom' || source === 'flex' || source === 'yaesu' || source === 'kenwood' || source === 'tci' ? (
|
||||
<span className="text-[11px] font-medium text-muted-foreground">
|
||||
{source === 'flex'
|
||||
? (connected ? t('wkp.cwxReady') : t('wkp.cwxOffline'))
|
||||
: source === 'yaesu' || source === 'kenwood'
|
||||
: source === 'yaesu' || source === 'kenwood' || source === 'tci'
|
||||
? (connected ? t('wkp.rigReady') : t('wkp.rigOffline'))
|
||||
: (connected ? t('wkp.civReady') : t('wkp.civOffline'))}
|
||||
</span>
|
||||
|
||||
@@ -128,6 +128,8 @@ const en: Dict = {
|
||||
'mx.tipDxConf': 'Entity confirmed (other callsign)', 'mx.tipDxWork': 'Entity worked (other callsign)',
|
||||
'mx.tipNone': 'Never worked', 'mx.tipClick': 'click to list the QSOs',
|
||||
'icmp.scopeNoStream': 'This radio does not send its scope over CI-V — its own screen still works.',
|
||||
'wk.catWarnTci': 'The CW keyer is set to the radio, but the CAT backend is "{backend}". Pick TCI in Settings → CAT, or another keying engine.',
|
||||
'wk.tciHint': "Keying goes through the radio's own macro keyer, over the link already open — no WinKeyer and no second serial port. The radio can stop a message but cannot un-type one, so there is no type-ahead correction here.",
|
||||
'gen.miles': 'Distances in miles', 'gen.milesHint': '(instead of kilometres)',
|
||||
'qslm.qrzTitle': 'Open this callsign on QRZ.com',
|
||||
'qslm.lotwDetail': 'QSL details',
|
||||
@@ -626,6 +628,8 @@ const fr: Dict = {
|
||||
'mx.tipDxConf': 'Entité confirmée (autre indicatif)', 'mx.tipDxWork': 'Entité contactée (autre indicatif)',
|
||||
'mx.tipNone': 'Jamais contacté', 'mx.tipClick': 'cliquer pour lister les QSO',
|
||||
'icmp.scopeNoStream': "Cette radio n'envoie pas son scope en CI-V — son propre écran fonctionne toujours.",
|
||||
'wk.catWarnTci': "Le manipulateur CW est réglé sur la radio, mais le backend CAT est « {backend} ». Choisissez TCI dans Réglages → CAT, ou un autre moteur de manipulation.",
|
||||
'wk.tciHint': "La manipulation passe par le keyer à macros de la radio, sur la liaison déjà ouverte — ni WinKeyer ni second port série. La radio sait interrompre un message mais pas en effacer la fin, donc pas de correction en frappe anticipée ici.",
|
||||
'gen.miles': 'Distances en miles', 'gen.milesHint': '(au lieu des kilomètres)',
|
||||
'qslm.qrzTitle': 'Ouvrir cet indicatif sur QRZ.com',
|
||||
'qslm.lotwDetail': 'Détails QSL',
|
||||
|
||||
Vendored
+6
@@ -1279,6 +1279,12 @@ export function SyncFolderNow():Promise<number>;
|
||||
|
||||
export function SyncPOTAHunterLog(arg1:boolean,arg2:boolean):Promise<main.POTASyncResult>;
|
||||
|
||||
export function TCISendCW(arg1:string):Promise<void>;
|
||||
|
||||
export function TCISetKeySpeed(arg1:number):Promise<void>;
|
||||
|
||||
export function TCIStopCW():Promise<void>;
|
||||
|
||||
export function TailLogFile(arg1:number):Promise<string>;
|
||||
|
||||
export function TestCloudlogUpload():Promise<string>;
|
||||
|
||||
@@ -2498,6 +2498,18 @@ export function SyncPOTAHunterLog(arg1, arg2) {
|
||||
return window['go']['main']['App']['SyncPOTAHunterLog'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function TCISendCW(arg1) {
|
||||
return window['go']['main']['App']['TCISendCW'](arg1);
|
||||
}
|
||||
|
||||
export function TCISetKeySpeed(arg1) {
|
||||
return window['go']['main']['App']['TCISetKeySpeed'](arg1);
|
||||
}
|
||||
|
||||
export function TCIStopCW() {
|
||||
return window['go']['main']['App']['TCIStopCW']();
|
||||
}
|
||||
|
||||
export function TailLogFile(arg1) {
|
||||
return window['go']['main']['App']['TailLogFile'](arg1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user