chore: release v0.27.12
This commit is contained in:
+134
-146
@@ -1,7 +1,7 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Flame, Gauge, Hash, Loader2, Lock,
|
||||
ChevronUp, Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radar, Radio, RadioTower, RefreshCw, Satellite, Send, SatelliteDish, Settings, SlidersHorizontal, SpellCheck, Square, Star, Terminal, Trash2, Unlock, X, Zap,
|
||||
ChevronLeft, ChevronUp, Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radar, Radio, RadioTower, RefreshCw, Satellite, Send, SatelliteDish, Settings, SlidersHorizontal, SpellCheck, Square, Star, Terminal, Trash2, Unlock, X, Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
GetFlexState, FlexAmpOperate,
|
||||
GetPSKReporterStatus, GetLiveOpenings, GetChaseNew,
|
||||
QSLViaRepairStatus, RepairQSLVia, DismissQSLViaRepair,
|
||||
GetAutoCallStatus, SetAutoCall, SetAutoCallOnly, TakeAutoCallTarget, ResetAutoCall,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList , withIOTARef, withRDARef } from '@/lib/awardRefs';
|
||||
@@ -119,9 +120,9 @@ import { SendSpotModal, type RecentSpotQSO } from '@/components/SendSpotModal';
|
||||
import { WinkeyerPanel, type WKStatus, type WKMacro } from '@/components/WinkeyerPanel';
|
||||
import { RotorCompass } from '@/components/RotorCompass';
|
||||
import { GridSquareMap } from '@/components/GridSquareMap';
|
||||
import { loadAutoCall, shouldAutoCall, autoCallKey, type AutoCallSettings } from '@/lib/autocall';
|
||||
import { loadClusterMacros, visibleClusterMacros } from '@/lib/clusterMacros';
|
||||
import { DecodesPanel, type Decode as DecodeRow, type TxMsg as TxMsgRow } from '@/components/DecodesPanel';
|
||||
import { PSKReporterPanel } from '@/components/PSKReporterPanel';
|
||||
import { subscribeRotorHeading, pokeRotorHeading } from '@/lib/rotorHeading';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { formatDateTimeUTC } from '@/lib/dateFormat';
|
||||
@@ -2565,136 +2566,34 @@ export default function App() {
|
||||
|
||||
// ── Auto-call ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Answers a decode without the operator clicking it. The DECISION lives in
|
||||
// lib/autocall (one pure function, so the dangerous part can be read and
|
||||
// argued with); this is only the plumbing that runs it and keys the radio.
|
||||
//
|
||||
// Re-read when Preferences closes, like every other setting edited there.
|
||||
// The DECISION is in the BACKEND (internal/autocall), because it keys a
|
||||
// transmitter: it has to behave the same whether this tab is open or the
|
||||
// window is minimised, and every rule it applies — seven calls, three missed
|
||||
// periods, the ladder — is a Go test rather than something only the air can
|
||||
// check. What is left here is the switch, the readout, and handing over the
|
||||
// station the operator clicks.
|
||||
|
||||
// The named command buttons, re-read when Preferences closes like every other
|
||||
// setting edited there.
|
||||
const [clusterMacros, setClusterMacros] = useState(loadClusterMacros);
|
||||
useEffect(() => { if (!showSettings) setClusterMacros(loadClusterMacros()); }, [showSettings]);
|
||||
const clusterMacrosShown = useMemo(() => visibleClusterMacros(clusterMacros), [clusterMacros]);
|
||||
// Auto-call is withdrawn — it duplicated DXHunter, which answers decodes from
|
||||
// the same shack. This flag is the single place that says so at runtime.
|
||||
const AUTO_CALL_ENABLED = false;
|
||||
const [autoCall, setAutoCall] = useState<AutoCallSettings>(loadAutoCall);
|
||||
useEffect(() => { if (!showSettings) setAutoCall(loadAutoCall()); }, [showSettings]);
|
||||
// When each callsign was last answered, so a station still calling CQ is not
|
||||
// re-answered every slot while the QSO it started is still running.
|
||||
const autoCalledRef = useRef<Map<string, number>>(new Map());
|
||||
// Decodes are scanned once. Without this the same decode is reconsidered on
|
||||
// every status refresh, and a cooldown that has just expired would fire again
|
||||
// on a decode minutes old.
|
||||
const autoSeenRef = useRef<Set<string>>(new Set());
|
||||
// Set when a call goes out, so nothing else fires until the receiver's own
|
||||
// status catches up and `busy` can be trusted again.
|
||||
const autoHoldUntilRef = useRef(0);
|
||||
// The station auto-call is currently working, and when it started.
|
||||
//
|
||||
// This is OpsLog's OWN record of "a QSO is running", and it exists because
|
||||
// deriving that from the sender's Status was not enough: the moment
|
||||
// WSJT-X/JTDX drops the DX call or the Enable-Tx flag between overs — which
|
||||
// they do — the exchange looks finished and the next CQ gets answered,
|
||||
// interleaving two and then three QSOs on one slice. A lock we set ourselves
|
||||
// cannot be cleared by a flag we do not control.
|
||||
//
|
||||
// Released when that station's QSO is logged, when the operator halts or
|
||||
// takes over by clicking a decode, and by the watchdog below.
|
||||
const autoTargetRef = useRef<{ call: string; at: number } | null>(null);
|
||||
// An exchange abandoned mid-way must not lock auto-call out for ever: four
|
||||
// minutes covers a repeated FT8 QSO and still frees the next period soon
|
||||
// enough to matter.
|
||||
const AUTO_TARGET_MAX_MS = 240_000;
|
||||
// Per receiver, when the carrier was last up. Feeds the stale-exchange
|
||||
// backstop below.
|
||||
const lastTxAtRef = useRef<Map<string, number>>(new Map());
|
||||
const [autoCallStatus, setAutoCallStatus] = useState<any>({ enabled: false, target: '', calls: 0, max: 0, misses: 0, max_miss: 0, stopped: false, reason: '' });
|
||||
useEffect(() => {
|
||||
// AUTO-CALL IS WITHDRAWN — DXHunter already answers decodes, and two
|
||||
// programs doing it from one shack key over each other. See lib/autocall.ts.
|
||||
//
|
||||
// Returning here rather than deleting the loop: the decision it implements
|
||||
// is the delicate part, argued over and tested, and worth keeping intact.
|
||||
// The guard is what matters — an operator whose stored preference still
|
||||
// says "enabled" must not have their transmitter keyed by a feature they
|
||||
// can no longer see, let alone switch off.
|
||||
if (!AUTO_CALL_ENABLED) return;
|
||||
if (!autoCall.enabled) return;
|
||||
const now = Date.now();
|
||||
// A QSO is in progress somewhere if ANY receiver is transmitting or still
|
||||
// holding a DX call it has not finished with. Both matter: between overs the
|
||||
// carrier is down but the exchange is not over, and calling someone else
|
||||
// then is exactly the "it never stops" behaviour.
|
||||
const busy = Object.values(txStates).some((tx) => {
|
||||
if (tx?.transmitting) return true;
|
||||
// A DX call still set means the exchange is not finished — but ONLY while
|
||||
// the sender still intends to transmit. The watchdog stops transmission
|
||||
// and leaves the DX call behind, and reading the call alone left auto-call
|
||||
// waiting for a QSO that had already been given up on, for ever.
|
||||
if (!tx?.dx_call?.trim()) return false;
|
||||
if (tx.tx_enabled === false) return false;
|
||||
// Backstop for a sender that never reports the toggle: an exchange with no
|
||||
// transmission for three minutes is over, whatever the DX call still says.
|
||||
// Measured from the last TIME THE CARRIER WAS UP — Status itself arrives
|
||||
// every second and so can never go stale.
|
||||
const last = lastTxAtRef.current.get(tx.instance ?? '');
|
||||
return last === undefined || now - last < 180_000;
|
||||
})
|
||||
// The hold closes the gap between sending a Reply and the receiver saying
|
||||
// it has acted on it — about a second. Without it the OTHER instance still
|
||||
// looks idle in that window and gets a call of its own.
|
||||
|| now < autoHoldUntilRef.current;
|
||||
// Our own lock, evaluated after the watchdog so an abandoned exchange does
|
||||
// not hold the transmitter shut.
|
||||
if (autoTargetRef.current && now - autoTargetRef.current.at > AUTO_TARGET_MAX_MS) {
|
||||
LogUIError('auto-call', `giving up on ${autoTargetRef.current.call} — nothing logged in four minutes`, '');
|
||||
autoTargetRef.current = null;
|
||||
}
|
||||
const locked = busy || autoTargetRef.current !== null;
|
||||
for (const d of decodes) {
|
||||
const seenKey = `${d.call}|${d.ms ?? d.at}|${d.instance ?? ''}`;
|
||||
if (autoSeenRef.current.has(seenKey)) continue;
|
||||
// A Replay's resent history is display-only: answering a line the far
|
||||
// end already dropped would fail anyway, and doing it at startup — the
|
||||
// moment replays arrive — would be a transmitter firing on old news.
|
||||
if ((d as any).is_new === false) { autoSeenRef.current.add(seenKey); continue; }
|
||||
// Only decodes from the CURRENT period are worth answering: replying to a
|
||||
// slot that has closed asks the far end to match a decode it has dropped.
|
||||
if (now - Date.parse(d.at) > 30_000) { autoSeenRef.current.add(seenKey); continue; }
|
||||
const e = spotStatus[`${d.call}|${d.band ?? ''}|${(d.mode ?? '').toUpperCase()}`];
|
||||
// NOT marked seen until its status has resolved. Statuses land a few
|
||||
// hundred milliseconds after the decode, so consuming it on first sight
|
||||
// would throw away almost every decode unjudged — the effect re-runs when
|
||||
// spotStatus changes, and this is what lets it look again.
|
||||
if (!e) continue;
|
||||
autoSeenRef.current.add(seenKey);
|
||||
const verdict = shouldAutoCall(autoCall, d, e as any, {
|
||||
busy: locked,
|
||||
calledAt: autoCalledRef.current,
|
||||
now,
|
||||
myCall: station.callsign,
|
||||
});
|
||||
if (!verdict.call) continue;
|
||||
autoCalledRef.current.set(d.call.toUpperCase(), now);
|
||||
autoHoldUntilRef.current = now + 12_000; // an FT8 slot, near enough
|
||||
autoTargetRef.current = { call: d.call.toUpperCase(), at: now };
|
||||
// Same reason as a manual click: put the transmitter on the decode's band
|
||||
// before answering, or a second slice answers on the wrong one.
|
||||
FlexTXOnBand(d.band ?? '').catch(() => {});
|
||||
// Logged, always: an automatic transmission with no record of WHY is the
|
||||
// one thing an operator cannot argue with after the fact.
|
||||
LogUIError('auto-call', `calling ${d.call} — ${verdict.reason}`, '');
|
||||
AnswerDecode(
|
||||
d.instance ?? '', d.ms ?? 0, d.snr, d.dt ?? 0,
|
||||
d.audio_hz ?? 0, d.mode_raw || d.mode || '', d.msg_raw ?? d.msg ?? '', !!d.low_conf,
|
||||
).catch((e2: any) => setError(String(e2?.message ?? e2)));
|
||||
onCallsignInput(d.call, { force: true });
|
||||
break; // one per pass: a period can hold several, and we work one station
|
||||
}
|
||||
// The seen-set is bounded by the same half hour the decode list keeps.
|
||||
if (autoSeenRef.current.size > 20000) autoSeenRef.current.clear();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [decodes, spotStatus, autoCall, txStates]);
|
||||
GetAutoCallStatus().then(setAutoCallStatus).catch(() => {});
|
||||
// Pushed by the engine on every decision, and polled as well: the push is
|
||||
// what makes the counter move the instant a call goes out, the poll is what
|
||||
// recovers a status missed while the window was asleep.
|
||||
const off = EventsOn('autocall:status', (s: any) => setAutoCallStatus(s));
|
||||
const id = window.setInterval(() => { GetAutoCallStatus().then(setAutoCallStatus).catch(() => {}); }, 3000);
|
||||
return () => { off(); window.clearInterval(id); };
|
||||
}, []);
|
||||
const toggleAutoCall = () => {
|
||||
const next = !autoCallStatus?.enabled;
|
||||
setAutoCallStatus((s: any) => ({ ...s, enabled: next }));
|
||||
SetAutoCall(next).catch((e: any) => setError(String(e?.message ?? e)));
|
||||
};
|
||||
|
||||
// Staged like the cluster's, so a period arriving as one burst of fifty
|
||||
// packets costs one status lookup and one render, not fifty of each.
|
||||
const pendingDecodesRef = useRef<DecodeRow[]>([]);
|
||||
@@ -2846,6 +2745,14 @@ export default function App() {
|
||||
const [showChaseNew, setShowChaseNew] = useState(() => localStorage.getItem('opslog.showChaseNew') !== '0');
|
||||
const refreshChaseNew = useCallback(() => { GetChaseNew().then(setChaseNewOn).catch(() => {}); }, []);
|
||||
useEffect(() => { refreshChaseNew(); }, [refreshChaseNew]);
|
||||
// The PSK Reporter panel beside the decodes. Open/closed is remembered; the
|
||||
// TARGET is whichever station is being worked, so it follows a click on a
|
||||
// decode and the DX call the digital application reports, and nothing has to
|
||||
// be selected twice.
|
||||
const [pskPanelOpen, setPskPanelOpen] = useState(() => localStorage.getItem('opslog.pskPanel') === '1');
|
||||
useEffect(() => { try { localStorage.setItem('opslog.pskPanel', pskPanelOpen ? '1' : '0'); } catch { /* private mode */ } }, [pskPanelOpen]);
|
||||
const [pskTarget, setPskTarget] = useState('');
|
||||
const [pskTargetMode, setPskTargetMode] = useState('');
|
||||
const [showBeamOnMap, setShowBeamOnMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
|
||||
// Compact rotor widget (Settings → Rotator): dial + SP/LP only. RotorCompass
|
||||
// already draws exactly that when it is given neither presets nor onStop —
|
||||
@@ -3836,11 +3743,6 @@ export default function App() {
|
||||
setTxState(m as TxMsgRow);
|
||||
if (m?.instance) {
|
||||
setTxStates((prev) => ({ ...prev, [m.instance]: m as TxMsgRow }));
|
||||
// When this receiver last actually TRANSMITTED, which is not the same as
|
||||
// when it last spoke: Status arrives about once a second whether the
|
||||
// carrier is up or not, so it can never say how long an exchange has
|
||||
// been stalled.
|
||||
if (m.transmitting) lastTxAtRef.current.set(m.instance, Date.now());
|
||||
}
|
||||
// The period history takes only real transmissions — Status repeats
|
||||
// itself once a second whether the carrier is up or not.
|
||||
@@ -3957,11 +3859,6 @@ export default function App() {
|
||||
try {
|
||||
await LogUDPLoggedADIF(text);
|
||||
await refresh();
|
||||
// The QSO auto-call started has finished — release the lock so the next
|
||||
// CQ can be answered. Matched on the callsign: a QSO logged from
|
||||
// somewhere else must not free a run that is still going.
|
||||
const logged = /<call:d+(?::[^>]*)?>([^<s]+)/i.exec(text)?.[1]?.toUpperCase();
|
||||
if (logged && autoTargetRef.current?.call === logged) autoTargetRef.current = null;
|
||||
} catch (e: any) {
|
||||
const msg = String(e?.message ?? e);
|
||||
// A re-broadcast of an already-logged QSO (Log4OM/WSJT-X) is benign —
|
||||
@@ -5182,6 +5079,8 @@ export default function App() {
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('logview.title'), action: 'help.log' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('help.discord'), action: 'help.discord' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('help.donate'), action: 'help.donate', accent: true },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('help.about'), action: 'help.about' },
|
||||
@@ -5226,7 +5125,8 @@ export default function App() {
|
||||
case 'help.sendlog': sendLogToDeveloper(); break;
|
||||
// Opens in the system browser, NOT the app WebView: a payment page must show
|
||||
// the address bar and padlock the donor knows how to check.
|
||||
case 'help.donate': BrowserOpenURL('https://www.paypal.com/donate/?hosted_button_id=PDMY7KV99K38S'); break;
|
||||
case 'help.discord': BrowserOpenURL('https://discord.gg/8ZsPDmH9q2'); break;
|
||||
case 'help.donate': BrowserOpenURL('https://www.paypal.com/donate/?hosted_button_id=PDMY7KV99K38S'); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6348,7 +6248,73 @@ export default function App() {
|
||||
// The FT decodes panel, built in ONE place: it is offered both as a tab and as
|
||||
// a Main-view pane, and two copies of this call would be two sets of props to
|
||||
// keep in step.
|
||||
// The dial the decodes are being read against. It has to be the DECODER's,
|
||||
// not the rig's: a report at 14.074.850 is only "+850 Hz in his passband"
|
||||
// measured from the same dial the digital application is using, and on split
|
||||
// or with a transverter the rig's own frequency is a different number.
|
||||
const pskDialHz = useMemo(() => {
|
||||
for (const d of decodes) if (d.dial_hz && d.dial_hz > 0) return d.dial_hz;
|
||||
return txState?.freq_hz ?? 0;
|
||||
}, [decodes, txState?.freq_hz]);
|
||||
|
||||
// Stations WE are decoding that are calling the same DX — the competition at
|
||||
// this end, which PSK Reporter cannot see: it carries who was heard, never
|
||||
// who they were calling.
|
||||
const pskCallers = useMemo(() => {
|
||||
if (!pskTarget) return [] as string[];
|
||||
const want = pskTarget.toUpperCase() + ' ';
|
||||
const seen = new Set<string>();
|
||||
for (const d of decodes) {
|
||||
const msg = (d.msg ?? '').toUpperCase().trim();
|
||||
if (!msg.startsWith(want)) continue;
|
||||
const caller = msg.slice(want.length).trim().split(/\s+/)[0];
|
||||
// "CQ" cannot be a caller, and neither can our own transmission coming
|
||||
// back as a decode of ourselves.
|
||||
if (caller && caller !== 'CQ' && caller !== (station.callsign ?? '').toUpperCase()) seen.add(caller);
|
||||
}
|
||||
return [...seen];
|
||||
}, [decodes, pskTarget, station.callsign]);
|
||||
|
||||
// Follow the station the digital application says it is calling. A decode
|
||||
// clicked here sets the target directly (see onCall); this covers the QSO
|
||||
// started from the other side — WSJT-X's own double-click, or auto-call.
|
||||
useEffect(() => {
|
||||
const dx = (txState?.dx_call ?? '').toUpperCase().trim();
|
||||
if (dx && dx !== pskTarget) {
|
||||
setPskTarget(dx);
|
||||
setPskTargetMode(txState?.mode ?? '');
|
||||
}
|
||||
}, [txState?.dx_call, txState?.mode, pskTarget]);
|
||||
|
||||
const renderDecodesPanel = () => (
|
||||
<div className="flex h-full min-h-0">
|
||||
<div className="flex-1 min-w-0 flex flex-col min-h-0">
|
||||
{renderDecodesList()}
|
||||
</div>
|
||||
{pskPanelOpen ? (
|
||||
<PSKReporterPanel
|
||||
target={pskTarget}
|
||||
mode={pskTargetMode}
|
||||
dialHz={pskDialHz}
|
||||
callers={pskCallers.length}
|
||||
callerCalls={pskCallers}
|
||||
onCollapse={() => setPskPanelOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
// Collapsed to a strip rather than removed: a panel with no way back is
|
||||
// one an operator loses, and the button has to say what it opens.
|
||||
<button type="button" onClick={() => setPskPanelOpen(true)} title={t('psk.show')}
|
||||
className="w-7 shrink-0 border-l border-border bg-card hover:bg-muted flex flex-col items-center gap-2 py-2 text-muted-foreground hover:text-foreground">
|
||||
<ChevronLeft className="size-4" />
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider [writing-mode:vertical-rl]">
|
||||
{t('psk.title')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderDecodesList = () => (
|
||||
<DecodesPanel
|
||||
decodes={decodes}
|
||||
txMsgs={txMsgs}
|
||||
@@ -6359,18 +6325,31 @@ export default function App() {
|
||||
// compare with", never "the rig is on no band".
|
||||
rigBand={catState.connected ? (catState.band || '') : ''}
|
||||
myCall={station.callsign}
|
||||
// A click ANSWERS the station: it hands the decode back to WSJT-X/MSHV as
|
||||
// a Reply, which is the same thing as double-clicking the line in their
|
||||
// own window.
|
||||
// A DOUBLE click answers the station: it hands the decode back to
|
||||
// WSJT-X/MSHV as a Reply, the same thing as double-clicking the line in
|
||||
// their own window. A single click only selects it — see onSelect below,
|
||||
// and the cluster, where the two gestures already mean this.
|
||||
//
|
||||
// Deliberately NOT a rig tune, unlike a cluster spot. On FT8 the whole
|
||||
// band is inside one passband, so moving the dial changes nothing about
|
||||
// who gets answered — and it would only fight the digital application
|
||||
// for the VFO. The entry is still filled, so the QSO can be logged here.
|
||||
// One click: take the station, transmit nothing. The entry is filled and
|
||||
// the panels follow it, exactly as clicking a cluster spot does — so a
|
||||
// row can be inspected, looked up and read about without keying up.
|
||||
onSelect={(d) => {
|
||||
setPskTarget((d.call ?? '').toUpperCase());
|
||||
setPskTargetMode(d.mode ?? '');
|
||||
onCallsignInput(d.call, { force: true });
|
||||
}}
|
||||
onCall={(d) => {
|
||||
// The operator has picked a station: that is now the QSO in progress, so
|
||||
// auto-call must not answer someone else over the top of it.
|
||||
autoTargetRef.current = { call: (d.call ?? '').toUpperCase(), at: Date.now() };
|
||||
// The station being answered is also the one worth analysing: the PSK
|
||||
// Reporter panel follows the click rather than asking for a second one.
|
||||
setPskTarget((d.call ?? '').toUpperCase());
|
||||
setPskTargetMode(d.mode ?? '');
|
||||
// Auto-call adopts the station: the click chooses WHO, the watchdogs
|
||||
// still decide how long it is called for.
|
||||
TakeAutoCallTarget(d.call ?? '', d.band ?? '', d.mode ?? '').catch(() => {});
|
||||
onCallsignInput(d.call, { force: true });
|
||||
// With two slices on two bands, the Reply reaches the right INSTANCE but
|
||||
// the radio still transmits on whichever slice holds the TX flag. Move
|
||||
@@ -6389,7 +6368,6 @@ export default function App() {
|
||||
// buffer goes too or the next flush would put back what was just cleared.
|
||||
onClear={(instance) => {
|
||||
if (!instance) {
|
||||
autoTargetRef.current = null;
|
||||
pendingDecodesRef.current = [];
|
||||
setDecodes([]);
|
||||
setTxMsgs([]);
|
||||
@@ -6407,10 +6385,20 @@ export default function App() {
|
||||
// An empty instance lets the backend fall back to whichever application
|
||||
// last reported its status — the normal single-receiver case.
|
||||
onHalt={(instance) => {
|
||||
// Halt means stop, including whatever auto-call had started.
|
||||
autoTargetRef.current = null;
|
||||
// Halt means stop, including whatever auto-call had started — and it
|
||||
// clears the engine's state, so a target it had given up on is not
|
||||
// still sitting there when the operator switches it back on.
|
||||
ResetAutoCall().catch(() => {});
|
||||
HaltDecodeTx(instance, false).catch((e: any) => setError(String(e?.message ?? e)));
|
||||
}}
|
||||
autoCallOn={!!autoCallStatus?.enabled}
|
||||
onToggleAutoCall={toggleAutoCall}
|
||||
autoCall={autoCallStatus}
|
||||
autoCallOnly={autoCallStatus?.only ?? ''}
|
||||
onSetAutoCallOnly={(list) => {
|
||||
setAutoCallStatus((st: any) => ({ ...st, only: list.toUpperCase() }));
|
||||
SetAutoCallOnly(list).catch((e: any) => setError(String(e?.message ?? e)));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user