fix: stop losing decodes, hanging on exit, and wedging the rig link
Three faults an operator's log finally made visible, plus the interface work that came out of the same session. Reliability: - UDP events were dropped on backpressure without a word. A period hands over twenty-odd decodes at once, and one slow write to the radio was enough to fill the queue — so a decode simply never appeared, and the only detector was the operator comparing the panel with JTDX. The drop is now counted and logged, panadapter spots went to their own goroutine so the radio can no longer hold the decode stream up, and the queue is deep enough for a full period. - The CAT manager waited for its poll loop with a bare <-done. A loop wedged in a serial read then blocked every later restart inside Start, before it could even try to connect: the rig stayed dead, no line was written anywhere, and only killing the process recovered it. The wait is bounded at ten seconds and says what it abandoned and why the next connect may fail. - Shutdown had no logging at all, so a hang left nothing to go on and a process the operator had to kill — which then blocked the restart after an update. Every step is logged, and a watchdog forces the exit if one of them never returns. Auto-call: - A QSO in progress is now held by OpsLog itself rather than inferred from the sender's Status. The moment WSJT-X/JTDX dropped the DX call or the Enable-Tx flag between overs, the exchange looked finished and the next CQ was answered, interleaving two and then three QSOs on one slice. Released on log, on halt, on taking over, and by a watchdog. Cluster console: - Replies to a command were buried under the spot flood; a Replies toggle hides the DX spots, which the list above already shows. - Twelve named command buttons beside the input, configured in Settings -> Cluster; a button with no command is not drawn. - Following the tail is now an explicit switch, and sending a command re-arms it. It used to measure "am I at the bottom" AFTER committing the new lines, so a ten-line reply looked like the operator had scrolled up and was never followed — the one case it exists for. Awards: - An award can name NO field. The matching controls disappear with it and only hand-assigned references count, which is the only thing that can feed a reference like WWBOTA. A test pins that nothing else is scanned. - WWBOTA added to the catalogue with its 31 342 references. Elsewhere: the rotor widget's Stop button acknowledges the press like the direction presets already did, and the docked band map can be switched to fit-to-band from its own header.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// Cluster command macros — a named button for a command you would otherwise
|
||||
// retype.
|
||||
//
|
||||
// Twelve slots, fixed. A list you can grow needs add/remove/reorder controls and
|
||||
// a decision about what an empty row means; twelve boxes you fill in need
|
||||
// neither, and nobody has thirteen cluster commands they use daily. A slot with
|
||||
// no command is simply not drawn, so the toolbar is as long as the operator made
|
||||
// it and no longer.
|
||||
//
|
||||
// Stored through writeUiPref like every other portable preference, so the
|
||||
// buttons travel with data/ rather than living in one browser profile.
|
||||
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
|
||||
export type ClusterMacro = {
|
||||
label: string; // what the button says
|
||||
cmd: string; // what is sent to the master server
|
||||
};
|
||||
|
||||
export const CLUSTER_MACRO_COUNT = 12;
|
||||
export const clusterMacrosKey = 'opslog.clusterMacros';
|
||||
|
||||
export const emptyClusterMacros = (): ClusterMacro[] =>
|
||||
Array.from({ length: CLUSTER_MACRO_COUNT }, () => ({ label: '', cmd: '' }));
|
||||
|
||||
// loadClusterMacros always returns exactly CLUSTER_MACRO_COUNT entries, whatever
|
||||
// was stored: a saved list from a build with fewer slots must not leave the
|
||||
// editor rendering undefined rows.
|
||||
export function loadClusterMacros(): ClusterMacro[] {
|
||||
const out = emptyClusterMacros();
|
||||
try {
|
||||
const raw = localStorage.getItem(clusterMacrosKey);
|
||||
if (!raw) return out;
|
||||
const v = JSON.parse(raw);
|
||||
if (!Array.isArray(v)) return out;
|
||||
for (let i = 0; i < CLUSTER_MACRO_COUNT && i < v.length; i++) {
|
||||
out[i] = {
|
||||
label: String(v[i]?.label ?? '').slice(0, 24),
|
||||
cmd: String(v[i]?.cmd ?? '').slice(0, 120),
|
||||
};
|
||||
}
|
||||
} catch { /* a corrupt preference is not worth failing the panel over */ }
|
||||
return out;
|
||||
}
|
||||
|
||||
export function saveClusterMacros(macros: ClusterMacro[]): void {
|
||||
writeUiPref(clusterMacrosKey, JSON.stringify(macros));
|
||||
}
|
||||
|
||||
// visibleClusterMacros drops the slots that would send nothing. The COMMAND is
|
||||
// what decides: a slot with a label and no command is a button that lies, and a
|
||||
// command with no label still has something to show — its own text.
|
||||
export function visibleClusterMacros(macros: ClusterMacro[]): ClusterMacro[] {
|
||||
return macros
|
||||
.filter((m) => m.cmd.trim() !== '')
|
||||
.map((m) => ({ label: m.label.trim() || m.cmd.trim(), cmd: m.cmd.trim() }));
|
||||
}
|
||||
Reference in New Issue
Block a user