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:
+51
-33
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user