fix: <LOGQSO> now logs at its position in the macro (splits the macro there), and ESC/Stop before it completes cancels the pending log

This commit is contained in:
2026-07-21 09:54:54 +02:00
parent 3de47a8825
commit 408791ddf5
+51 -33
View File
@@ -811,6 +811,11 @@ export default function App() {
const wkEscClearsRef = useRef(true); const wkEscClearsRef = useRef(true);
const wkBusyRef = useRef(false); // live "keyer is sending" flag, for the <LOGQSO> wait-then-log const wkBusyRef = useRef(false); // live "keyer is sending" flag, for the <LOGQSO> wait-then-log
useEffect(() => { wkBusyRef.current = wkStatus.busy; }, [wkStatus.busy]); useEffect(() => { wkBusyRef.current = wkStatus.busy; }, [wkStatus.busy]);
// Generation token for an in-flight macro send: bumped on every new send AND on
// any abort (ESC / Stop / callsign cleared). wkSend checks it before each CW
// segment and before every <LOGQSO> log, so aborting mid-macro stops sending AND
// skips the log that hasn't happened yet.
const wkSendGenRef = useRef(0);
useEffect(() => { useEffect(() => {
const connected = cwSource === 'icom' ? (catState.backend === 'icom' && catState.connected) const connected = cwSource === 'icom' ? (catState.backend === 'icom' && catState.connected)
: cwSource === 'flex' ? (catState.backend === 'flex' && catState.connected) : cwSource === 'flex' ? (catState.backend === 'flex' && catState.connected)
@@ -2125,44 +2130,59 @@ export default function App() {
return out.replace(/\s+/g, ' ').trim(); return out.replace(/\s+/g, ' ').trim();
} }
async function wkSend(rawText: string) { async function wkSend(rawText: string) {
const gen = ++wkSendGenRef.current; // this send supersedes any previous; abort bumps it too
setWkSent(''); setWkSent('');
const resolved = resolveCW(rawText);
// Trailing word space so two macros fired back-to-back don't run together in
// the keyer buffer ("CQ" + "TEST" → "CQTEST"). The keyer keys a space as a
// word gap at the CURRENT speed, so it scales with WPM automatically.
const keyed = resolved ? resolved + ' ' : resolved;
const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms)); const sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
if (cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex') { const aborted = () => gen !== wkSendGenRef.current;
// The rig keyer (Icom 0x17 / Flex CWX) gives no busy echo we track, so show // Cancellable wait: sleeps up to ms, but bails out immediately on abort.
// the text we sent and, for <LOGQSO>, wait the estimated send duration const waitMs = async (ms: number) => {
// before logging. const end = Date.now() + ms;
setWkSent(resolved); while (Date.now() < end && !aborted()) await sleep(50);
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : IcomSendCW; };
await sendFn(keyed).catch((e) => setError(String(e?.message ?? e))); // Split the macro on <LOGQSO> so the log happens AT the token's position, not
if (doLog) { await sleep(Math.round(estimateCwMs(resolved, wkWpm)) + 600); void save(); } // only at the very end: "TU <LOGQSO> QRZ" sends "TU", logs, then sends "QRZ".
return; // Each split boundary (every part except the last) is one <LOGQSO>.
const parts = rawText.split(/<LOGQSO>/i);
const isRig = cwSourceRef.current === 'icom' || cwSourceRef.current === 'flex';
for (let p = 0; p < parts.length; p++) {
if (aborted()) return; // ESC / Stop before this segment → stop sending, don't log
const resolved = resolveCW(parts[p]);
if (resolved) {
// Trailing word space so back-to-back sends don't run together in the keyer
// buffer ("CQ"+"TEST" → "CQTEST"); the keyer keys it as a word gap at the
// current WPM, so it scales automatically.
const keyed = resolved + ' ';
setWkSent(resolved);
if (isRig) {
// The rig keyer (Icom 0x17 / Flex CWX) gives no busy echo we track, so
// wait the ESTIMATED send duration before moving on / logging.
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : IcomSendCW;
await sendFn(keyed).catch((e) => setError(String(e?.message ?? e)));
await waitMs(Math.round(estimateCwMs(resolved, wkWpm)) + (p < parts.length - 1 ? 200 : 600));
} else {
await WinkeyerSend(keyed).catch((e) => setError(String(e?.message ?? e)));
// Wait for the keyer to actually finish this segment before we log /
// continue. We'd like to watch busy rise then fall, but over a
// remote/serial-over-IP link that echo can lag tens of seconds, so cap
// the wait at the ESTIMATED send duration + slack: proceed when busy
// clears OR the estimate elapses, whichever comes first.
const capMs = Math.round(estimateCwMs(resolved, wkWpm) * 1.4) + 2500;
for (let i = 0; i < 20 && !wkBusyRef.current && !aborted(); i++) await sleep(50); // ≤1s to start
const deadline = Date.now() + capMs;
while (wkBusyRef.current && !aborted() && Date.now() < deadline) await sleep(50);
}
}
if (aborted()) return; // aborted while this segment was sending → don't log
// A <LOGQSO> token follows every part except the last → log the contact here.
if (p < parts.length - 1) void save();
} }
await WinkeyerSend(keyed).catch((e) => setError(String(e?.message ?? e)));
// <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
// finished sending — so the QSO isn't logged (and the form cleared) while CW
// is still going out. We'd like to wait for the busy flag to rise then fall,
// but over a remote/serial-over-IP link that status echo can lag by tens of
// seconds, which used to delay logging ~50s. So cap the wait at the ESTIMATED
// send duration (text length × WPM) plus slack: log when busy clears OR the
// estimate elapses, whichever comes first.
if (!doLog) return;
const capMs = Math.round(estimateCwMs(resolved, wkWpm) * 1.4) + 2500; // slack for keyer buffering/lag
for (let i = 0; i < 20 && !wkBusyRef.current; i++) await sleep(50); // ≤1s for sending to start
const deadline = Date.now() + capMs;
while (wkBusyRef.current && Date.now() < deadline) await sleep(50);
void save();
} }
// stopAutoCall cancels any running auto-call loop. // stopAutoCall cancels any running auto-call loop.
function stopAutoCall() { autoCallMacroRef.current = -1; autoCallGenRef.current++; } function stopAutoCall() { autoCallMacroRef.current = -1; autoCallGenRef.current++; }
// stopKeyerTx aborts the CW being sent RIGHT NOW, routed to the active engine // stopKeyerTx aborts the CW being sent RIGHT NOW, routed to the active engine
// (was WinKeyer-only in a few places, so a Flex CWX / Icom macro kept going). // (was WinKeyer-only in a few places, so a Flex CWX / Icom macro kept going).
function stopKeyerTx() { function stopKeyerTx() {
wkSendGenRef.current++; // cancel any in-flight macro send (and its pending <LOGQSO> log)
if (cwSourceRef.current === 'icom') IcomStopCW().catch(() => {}); if (cwSourceRef.current === 'icom') IcomStopCW().catch(() => {});
else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {}); else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {});
else WinkeyerStop().catch(() => {}); else WinkeyerStop().catch(() => {});
@@ -2902,6 +2922,7 @@ export default function App() {
// ESC didn't stop the Icom or Flex keyer). // ESC didn't stop the Icom or Flex keyer).
if (keyerLive) { if (keyerLive) {
stopAutoCall(); stopAutoCall();
wkSendGenRef.current++; // abort an in-flight macro send so a pending <LOGQSO> won't fire
if (cwSourceRef.current === 'icom') IcomStopCW().catch(() => {}); if (cwSourceRef.current === 'icom') IcomStopCW().catch(() => {});
else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {}); else if (cwSourceRef.current === 'flex') FlexStopCW().catch(() => {});
else WinkeyerStop().catch(() => {}); else WinkeyerStop().catch(() => {});
@@ -4479,10 +4500,7 @@ export default function App() {
}} }}
onSend={wkSend} onSend={wkSend}
onSendMacro={wkSendMacro} onSendMacro={wkSendMacro}
onStop={() => { stopAutoCall(); onStop={() => { stopAutoCall(); stopKeyerTx(); }}
if (cwSource === 'icom') IcomStopCW().catch(() => {});
else if (cwSource === 'flex') FlexStopCW().catch(() => {});
else WinkeyerStop().catch(() => {}); }}
onClose={() => wkSetEnabled(false)} onClose={() => wkSetEnabled(false)}
sendOnType={wkSendOnType} sendOnType={wkSendOnType}
onToggleSendOnType={wkToggleSendOnType} onToggleSendOnType={wkToggleSendOnType}