fix: autocall in remote not working properly
This commit is contained in:
@@ -4207,13 +4207,30 @@ func (a *App) saveQSORecording(q *qso.QSO) {
|
||||
return
|
||||
}
|
||||
applog.Printf("qso-rec: saved %s", path)
|
||||
// Auto-send the recording once the file exists.
|
||||
if es, _ := a.GetEmailSettings(); es.Enabled && es.AutoSend && strings.TrimSpace(qc.Email) != "" {
|
||||
_ = a.sendRecordingEmail(qc, path)
|
||||
// Auto-send the recording once the file exists. Give the operator visible
|
||||
// feedback either way — silently skipping (no e-mail known) was confusing.
|
||||
if es, _ := a.GetEmailSettings(); es.Enabled && es.AutoSend {
|
||||
if strings.TrimSpace(qc.Email) == "" {
|
||||
applog.Printf("qso-rec: %s not e-mailed — no recipient address known", qc.Callsign)
|
||||
a.toast(fmt.Sprintf("Recording saved — no e-mail for %s, not sent", qc.Callsign))
|
||||
} else if err := a.sendRecordingEmail(qc, path); err != nil {
|
||||
a.toast(fmt.Sprintf("Recording e-mail to %s failed", qc.Callsign))
|
||||
} else {
|
||||
a.toast(fmt.Sprintf("Recording e-mailed to %s", qc.Callsign))
|
||||
a.markRecordingSent(qc.ID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// toast emits a transient message the frontend shows as a toast notification.
|
||||
// Used for background events (auto-send results) the UI can't otherwise surface.
|
||||
func (a *App) toast(msg string) {
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "toast", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeFilename makes a callsign safe for a filename (slashes etc.).
|
||||
func sanitizeFilename(s string) string {
|
||||
s = strings.ToUpper(strings.TrimSpace(s))
|
||||
@@ -4687,6 +4704,28 @@ func (a *App) fillTemplate(tmpl string, q qso.QSO) string {
|
||||
return r.Replace(tmpl)
|
||||
}
|
||||
|
||||
// markRecordingSent stamps the QSO so the UI (and the operator) knows its audio
|
||||
// recording was e-mailed — exposed as the APP_OPSLOG_RECORDING_SENT extra and a
|
||||
// "Recording sent" column in Recent QSOs. Re-reads the row first so a concurrent
|
||||
// status update (eQSL/upload) isn't clobbered by writing back the whole row.
|
||||
func (a *App) markRecordingSent(id int64) {
|
||||
if a.qso == nil || id == 0 {
|
||||
return
|
||||
}
|
||||
q, err := a.qso.GetByID(a.ctx, id)
|
||||
if err != nil {
|
||||
applog.Printf("qso-rec: mark sent: load %d: %v", id, err)
|
||||
return
|
||||
}
|
||||
if q.Extras == nil {
|
||||
q.Extras = map[string]string{}
|
||||
}
|
||||
q.Extras["APP_OPSLOG_RECORDING_SENT"] = time.Now().UTC().Format("2006-01-02")
|
||||
if err := a.qso.Update(a.ctx, q); err != nil {
|
||||
applog.Printf("qso-rec: mark sent: update %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendRecordingEmail e-mails a QSO recording to the contacted operator.
|
||||
func (a *App) sendRecordingEmail(q qso.QSO, attachPath string) error {
|
||||
s, _ := a.GetEmailSettings()
|
||||
@@ -4732,7 +4771,11 @@ func (a *App) SendQSORecordingEmail(id int64) error {
|
||||
if _, e := os.Stat(path); e != nil {
|
||||
return fmt.Errorf("recording file missing: %s", name)
|
||||
}
|
||||
return a.sendRecordingEmail(q, path)
|
||||
if err := a.sendRecordingEmail(q, path); err != nil {
|
||||
return err
|
||||
}
|
||||
a.markRecordingSent(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── ClubLog Country File (cty.xml) exceptions ─────────────────────────
|
||||
|
||||
+47
-10
@@ -206,6 +206,16 @@ function rstCategory(mode: string): keyof RSTLists {
|
||||
if (['CW', 'RTTY', 'PSK31', 'PSK63', 'PSK', 'PSK125'].includes(m)) return 'cw';
|
||||
return 'phone';
|
||||
}
|
||||
// estimateCwMs roughly estimates how long the resolved text takes to send in CW
|
||||
// at wpm: ~10 dot-units per character + ~4 extra per word gap, 1 unit = 1200/wpm
|
||||
// ms (PARIS standard). Used to cap waits on the keyer's "busy" status, which can
|
||||
// lag by tens of seconds over a remote/serial-over-IP link.
|
||||
function estimateCwMs(resolved: string, wpm: number): number {
|
||||
const w = Math.max(5, wpm || 25);
|
||||
const chars = resolved.replace(/\s/g, '').length;
|
||||
const spaces = (resolved.match(/\s/g) || []).length;
|
||||
return ((chars * 10 + spaces * 4) * 1200) / w;
|
||||
}
|
||||
// rstOptions returns the valid report choices for a mode from the user's
|
||||
// editable lists (Settings → Modes), with a tiny fallback before they load.
|
||||
function rstOptions(mode: string, lists: RSTLists): string[] {
|
||||
@@ -1059,6 +1069,12 @@ export default function App() {
|
||||
return () => { offUploaded(); offDone(); offEqsl(); if (t) window.clearTimeout(t); };
|
||||
}, [refresh]);
|
||||
|
||||
// Backend-emitted toast messages (e.g. recording auto-send result/skip).
|
||||
useEffect(() => {
|
||||
const off = EventsOn('toast', (msg: any) => { if (msg) showToast(String(msg)); });
|
||||
return () => { off(); };
|
||||
}, [showToast]);
|
||||
|
||||
// Poll PstRotator for the live antenna heading (status bar). Cheap when the
|
||||
// rotator is disabled (the backend just reads settings and returns).
|
||||
useEffect(() => {
|
||||
@@ -1246,12 +1262,20 @@ export default function App() {
|
||||
// source behaves identically.
|
||||
function handleSpotClick(s: any) {
|
||||
const m = inferSpotMode(s.comment ?? '', s.freq_hz);
|
||||
if (catState.connected) {
|
||||
tuneRigCAT(s.freq_hz, m);
|
||||
} else {
|
||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||
if (s.band) setBand(s.band);
|
||||
// Reflect the spot's freq/band in the entry strip IMMEDIATELY (optimistic),
|
||||
// then tune the rig if CAT is connected. We must not wait for the CAT status
|
||||
// echo to update the display: on a remote Flex link that echo lags by seconds,
|
||||
// so the frequency used to only catch up once you nudged the VFO. noteManualEdit()
|
||||
// freezes the CAT-driven overwrite for 1.5s so a stale in-flight status can't
|
||||
// revert the optimistic value before the new freq is reported back.
|
||||
if (s.freq_hz && s.freq_hz > 0) {
|
||||
const mhz = (s.freq_hz / 1_000_000).toFixed(5);
|
||||
setFreqMhz(mhz);
|
||||
setRxFreqMhz(mhz);
|
||||
noteManualEdit();
|
||||
}
|
||||
if (s.band) setBand(s.band);
|
||||
if (catState.connected) tuneRigCAT(s.freq_hz, m);
|
||||
if (m) applyModeFromSpot(m);
|
||||
onCallsignInput(s.dx_call, { force: true });
|
||||
applySpotPOTA((s as any).pota_ref);
|
||||
@@ -1576,15 +1600,22 @@ export default function App() {
|
||||
}
|
||||
async function wkSend(rawText: string) {
|
||||
setWkSent('');
|
||||
const resolved = resolveCW(rawText);
|
||||
const doLog = /<LOGQSO>/i.test(rawText); // resolveCW strips the token (unknown var → "")
|
||||
await WinkeyerSend(resolveCW(rawText)).catch((e) => setError(String(e?.message ?? e)));
|
||||
await WinkeyerSend(resolved).catch((e) => setError(String(e?.message ?? e)));
|
||||
// <LOGQSO> (e.g. "BK 73 TU <LOGQSO>") logs the contact AFTER the keyer has
|
||||
// finished sending — wait for the busy flag to rise then fall, so the QSO
|
||||
// isn't logged (and the form cleared) while the CW is still going out.
|
||||
// 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 sleep = (ms: number) => new Promise((r) => window.setTimeout(r, ms));
|
||||
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
|
||||
for (let i = 0; i < 1200 && wkBusyRef.current; i++) await sleep(50); // ≤60s for it to finish
|
||||
const deadline = Date.now() + capMs;
|
||||
while (wkBusyRef.current && Date.now() < deadline) await sleep(50);
|
||||
void save();
|
||||
}
|
||||
// stopAutoCall cancels any running auto-call loop.
|
||||
@@ -1599,8 +1630,14 @@ export default function App() {
|
||||
const m = wkMacros[i];
|
||||
if (!m) break;
|
||||
await wkSend(m.text);
|
||||
// Wait for the message to finish before the gap+resend. Cap the wait at the
|
||||
// ESTIMATED send time (not the busy flag alone): over a remote/serial-over-IP
|
||||
// link the keyer's "busy" status lags badly and stays stuck true for tens of
|
||||
// seconds, which made the next CQ fire ~120s late — i.e. auto-call looked dead.
|
||||
const capMs = Math.round(estimateCwMs(resolveCW(m.text), wkWpm) * 1.4) + 2500;
|
||||
for (let k = 0; k < 20 && !wkBusyRef.current && gen === autoCallGenRef.current; k++) await sleep(50); // ≤1s to start
|
||||
for (let k = 0; k < 2400 && wkBusyRef.current && gen === autoCallGenRef.current; k++) await sleep(50); // ≤120s to finish
|
||||
const deadline = Date.now() + capMs;
|
||||
while (wkBusyRef.current && gen === autoCallGenRef.current && Date.now() < deadline) await sleep(50);
|
||||
if (gen !== autoCallGenRef.current) break;
|
||||
await sleep(Math.max(0, wkAutoCallSecsRef.current) * 1000); // the gap before the next call
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ export const COL_CATALOG: ColEntry[] = [
|
||||
{ group: 'eQSL', label: 'eQSL rcvd date', colId: 'eqsl_rcvd_date', headerName: 'eQSL R date', field: 'eqsl_rcvd_date' as any, width: 120, valueFormatter: (p) => fmtDateOnly(p.value) },
|
||||
// App-specific: when OpsLog e-mailed its own QSL card. Distinct from eQSL.cc.
|
||||
{ group: 'QSL', label: 'OpsLog QSL', colId: 'opslog_qsl_card_sent', headerName: 'OpsLog QSL', width: 100, cellClass: 'font-mono', valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return (e['APP_OPSLOG_QSL_SENT'] || e['APP_OPSLOG_QSL_CARD_SENT']) ? 'Y' : 'N'; }, defaultVisible: true },
|
||||
// App-specific: when the QSO's audio recording was e-mailed to the station.
|
||||
{ group: 'QSL', label: 'Recording sent', colId: 'opslog_recording_sent', headerName: 'Rec sent', width: 100, cellClass: 'font-mono', valueGetter: (p) => { const e = (p.data as any)?.extras ?? {}; return e['APP_OPSLOG_RECORDING_SENT'] ? 'Y' : 'N'; }, defaultVisible: false },
|
||||
|
||||
// ── Uploads (online logbooks) ──
|
||||
// ADIF models these as an "upload status/date" (= YOU pushed the QSO) and,
|
||||
|
||||
Reference in New Issue
Block a user