fix: PGXL meters/auth/fan, TCI spots+click, PTT-over-CAT, CW <LOGQSO>

- PGXL: remote AUTH password; direct-link meter fallback (VITA-first) with
  250 ms poll + peak-hold; fan mode uses bare "fanmode=" (was ignored).
- TCI: spot colour sent as decimal ARGB (ExpertSDR dropped the hex string);
  spot click handled via CLICKED_ON_SPOT / RX_CLICKED_ON_SPOT to fill the entry.
- FlexRadio: clicking an OpsLog spot on the panadapter now applies the mode too.
- Filter presets: the trash icon deletes again (Radix pointer-down intercept).
- PTT hotkey: keys over CAT when a backend is active (was hijacked by a stale
  Audio-tab RTS/DTR serial setting); AltGr keys allowed; presses logged.
- CW macro <LOGQSO>: logs after the preceding CW is sent, not on the first letter.
This commit is contained in:
2026-08-04 15:55:10 +02:00
parent b3fdd0b7fa
commit 18f9b915c5
12 changed files with 277 additions and 81 deletions
+36 -30
View File
@@ -1945,15 +1945,23 @@ export default function App() {
const down = (e: KeyboardEvent) => {
const cfg = pttHotkeyRef.current;
if (!cfg.enabled || !cfg.code || e.code !== cfg.code) return;
if (e.ctrlKey || e.altKey || e.metaKey) return; // plain key only
// Allow AltGr (Ctrl+Alt together) — French/other layouts need it to reach
// keys like "\" — but never hijack a real Ctrl- or Win-shortcut.
if (e.metaKey || (e.ctrlKey && !e.altKey)) return;
e.preventDefault();
if (e.repeat) return;
// Surface the result: this feature was impossible to diagnose because every
// error was swallowed. The line names the key, the action and any failure.
const fail = (err: any) => { UILog(`ptt hotkey: ${e.code} FAILED: ${err}`).catch(() => {}); };
if (cfg.toggle) {
pttToggledRef.current = !pttToggledRef.current;
(pttToggledRef.current ? PTTHotkeyDown() : PTTHotkeyUp())?.catch?.(() => {});
const on = pttToggledRef.current;
UILog(`ptt hotkey: ${e.code} toggle → ${on ? 'KEY' : 'UNKEY'}`).catch(() => {});
(on ? PTTHotkeyDown() : PTTHotkeyUp())?.catch?.(fail);
} else if (!pttHeldRef.current) {
pttHeldRef.current = true;
PTTHotkeyDown().catch(() => {});
UILog(`ptt hotkey: ${e.code} down → KEY`).catch(() => {});
PTTHotkeyDown().catch(fail);
}
};
const up = (e: KeyboardEvent) => {
@@ -2837,36 +2845,34 @@ export default function App() {
const sendFn = cwSourceRef.current === 'flex' ? FlexSendCW : cwSourceRef.current === 'icom' ? IcomSendCW : cwSourceRef.current === 'yaesu' ? YaesuSendCW : null;
if (sendFn) await sendFn(keyed).catch((e) => setError(String(e?.message ?? e)));
else await WinkeyerSend(keyed).catch((e) => setError(String(e?.message ?? e)));
// Only WAIT for the CW to finish on the FINAL segment — auto-call needs the
// send done before its gap+resend. A segment a <LOGQSO> follows is NOT waited
// on: the keyer has already buffered the CW, so logging happens AT ONCE and
// never interrupts what's being sent. (Gating the log on a CW-length ESTIMATE
// made <LOGQSO> log up to ~10s late when the estimate ran long.)
if (isLast) {
if (isRig) {
await waitMs(Math.round(estimateCwMs(resolved, wkWpm)) + 600);
} else {
// WinKeyer / serial keyer: the message takes AT LEAST its own length to
// send, so the estimate is a floor and the busy signal only extends it.
//
// Waiting on busy alone made the auto-call gap start early: busy travels
// from the keyer to this window as an event, and if it had not arrived
// and cleared within the one-second start window the wait simply ended.
// A 6 s gap then ran from the START of the CQ — about 2 s of silence
// after a 4 s call, which is what an operator hears as "clearly not 6".
const estMs = Math.round(estimateCwMs(resolved, wkWpm));
const started = Date.now();
const capMs = Math.round(estMs * 1.4) + 2500;
const deadline = started + capMs;
// The message's own duration, first — this is the part that must not be skipped.
while (Date.now() - started < estMs && !aborted()) await sleep(50);
// Then let a still-sending keyer finish (slow link, long buffer), capped.
while (wkBusyRef.current && !aborted() && Date.now() < deadline) await sleep(50);
}
// WAIT for THIS segment's CW to finish before moving on — so a <LOGQSO>
// that follows logs AFTER the text before it (e.g. "TU") has actually been
// sent, not the instant it's buffered into the keyer. Buffering then logging
// "at once" made a macro ending in "TU <LOGQSO>" log on the very first
// letter. Auto-call also needs the send finished before its gap+resend.
if (isRig) {
await waitMs(Math.round(estimateCwMs(resolved, wkWpm)) + 600);
} else {
// WinKeyer / serial keyer: the message takes AT LEAST its own length to
// send, so the estimate is a floor and the busy signal only extends it.
//
// Waiting on busy alone made the auto-call gap start early: busy travels
// from the keyer to this window as an event, and if it had not arrived
// and cleared within the one-second start window the wait simply ended.
// A 6 s gap then ran from the START of the CQ — about 2 s of silence
// after a 4 s call, which is what an operator hears as "clearly not 6".
const estMs = Math.round(estimateCwMs(resolved, wkWpm));
const started = Date.now();
const capMs = Math.round(estMs * 1.4) + 2500;
const deadline = started + capMs;
// The message's own duration, first — this is the part that must not be skipped.
while (Date.now() - started < estMs && !aborted()) await sleep(50);
// Then let a still-sending keyer finish (slow link, long buffer), capped.
while (wkBusyRef.current && !aborted() && Date.now() < deadline) await sleep(50);
}
}
if (aborted()) return; // aborted while this segment was sending → don't log
if (logAfter) void save(); // log NOW — the buffered CW keeps sending
if (logAfter) void save(); // the preceding CW has finished → log at the token's spot
}
}
// stopAutoCall cancels any running auto-call loop.