feat: Support for Antenna Genius
This commit is contained in:
+77
-2
@@ -19,6 +19,7 @@ import {
|
||||
RotatorGoTo, RotatorStop, GetRotatorHeading,
|
||||
GetDBConnectionInfo, GetLogbookRevision,
|
||||
GetUltrabeamStatus, SetUltrabeamDirection,
|
||||
GetAntGeniusStatus, GetAntGeniusSettings, AntGeniusActivate,
|
||||
OpenExternalURL,
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus, SendClusterCommand,
|
||||
ListClusterServers, ClusterSpotStatuses, SendClusterSpot,
|
||||
@@ -56,6 +57,8 @@ import { QSOEditModal } from '@/components/QSOEditModal';
|
||||
import { BandMap } from '@/components/BandMap';
|
||||
import { WorldMap, LocatorMap } from '@/components/MainMap';
|
||||
import { FlexPanel } from '@/components/FlexPanel';
|
||||
import { IcomPanel } from '@/components/IcomPanel';
|
||||
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
||||
import { FilterBuilder, type QueryFilter } from '@/components/FilterBuilder';
|
||||
import { AwardsPanel } from '@/components/AwardsPanel';
|
||||
import { RecentQSOsGrid } from '@/components/RecentQSOsGrid';
|
||||
@@ -332,6 +335,12 @@ export default function App() {
|
||||
const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
|
||||
const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 });
|
||||
const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false });
|
||||
const [agStatus, setAgStatus] = useState<AGStatus>({ connected: false, port_a: 0, port_b: 0, antennas: [] });
|
||||
const [agEnabled, setAgEnabled] = useState(false);
|
||||
// Per-port optimistic selection that the status poll must not revert until the
|
||||
// device confirms it (or it expires) — otherwise a stale poll right after a
|
||||
// click reverts the UI and the click looks like it did nothing.
|
||||
const agPending = useRef<{ a?: { v: number; t: number }; b?: { v: number; t: number } }>({});
|
||||
const [dbConn, setDbConn] = useState<{ backend: string; label: string } | null>(null);
|
||||
// Mode OpsLog shows when the rig reports generic DIG_U/DIG_L. OmniRig
|
||||
// can't tell us if it's FT8 vs FT4 vs RTTY, so the user picks the default
|
||||
@@ -966,6 +975,7 @@ export default function App() {
|
||||
|
||||
// Portable UI toggles (mirrored to the DB via writeUiPref / syncPortablePrefs).
|
||||
const [showRotor, setShowRotor] = useState(() => localStorage.getItem('opslog.showRotor') !== '0');
|
||||
const [showAntGenius, setShowAntGenius] = useState(() => localStorage.getItem('opslog.showAntGenius') !== '0');
|
||||
const [showBeamOnMap, setShowBeamOnMap] = useState(() => localStorage.getItem('opslog.showBeamOnMap') !== '0');
|
||||
|
||||
// Award code → scanned field (e.g. POTA→pota_ref, WWFF→wwff). Used to route
|
||||
@@ -1061,6 +1071,38 @@ export default function App() {
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
|
||||
// Poll the Antenna Genius switch for active antenna per port + the list.
|
||||
// Re-read the enabled flag each tick so toggling it in Settings makes the
|
||||
// top-bar icon appear/disappear without an app restart.
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = async () => {
|
||||
try { const en: any = await GetAntGeniusSettings(); if (alive) setAgEnabled(!!en?.enabled); } catch {}
|
||||
try {
|
||||
const s = (await GetAntGeniusStatus()) as AGStatus;
|
||||
if (!alive || !s) return;
|
||||
const now = Date.now();
|
||||
const pend = agPending.current;
|
||||
// Keep an optimistic selection until the device confirms it or it ages out.
|
||||
if (pend.a) { if (now > pend.a.t || s.port_a === pend.a.v) delete pend.a; else s.port_a = pend.a.v; }
|
||||
if (pend.b) { if (now > pend.b.t || s.port_b === pend.b.v) delete pend.b; else s.port_b = pend.b.v; }
|
||||
// Only update when something actually changed — avoids re-rendering the
|
||||
// widget every 1.5s (which made buttons flicker on hover).
|
||||
setAgStatus((prev) => (JSON.stringify(prev) === JSON.stringify(s) ? prev : s));
|
||||
} catch {}
|
||||
};
|
||||
tick();
|
||||
const id = window.setInterval(tick, 1500);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
const agActivate = (port: number, antenna: number) => {
|
||||
// Optimistic: reflect the change immediately and pin it for ~3s so the next
|
||||
// poll (which may still carry the old cached value) can't revert it.
|
||||
agPending.current[port === 1 ? 'a' : 'b'] = { v: antenna, t: Date.now() + 3000 };
|
||||
setAgStatus((s) => ({ ...s, ...(port === 1 ? { port_a: antenna } : { port_b: antenna }) }));
|
||||
AntGeniusActivate(port, antenna).catch((e) => setError(String(e?.message ?? e)));
|
||||
};
|
||||
|
||||
// RX band auto-follows the TX band (only differs for cross-band work).
|
||||
useEffect(() => { setBandRx(band); }, [band]);
|
||||
|
||||
@@ -2921,6 +2963,21 @@ export default function App() {
|
||||
>
|
||||
<Compass className="size-4" />
|
||||
</button>
|
||||
{agEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { const v = !showAntGenius; setShowAntGenius(v); writeUiPref('opslog.showAntGenius', v ? '1' : '0'); }}
|
||||
title={showAntGenius ? 'Antenna Genius — shown · click to hide' : 'Antenna Genius · click to show'}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
showAntGenius ? 'border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<Antenna className="size-4" />
|
||||
{showAntGenius && agStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-emerald-500" />}
|
||||
</button>
|
||||
)}
|
||||
{chatAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -3219,7 +3276,7 @@ export default function App() {
|
||||
{/* Reserved free space to the right. The WinKeyer CW keyer and/or the
|
||||
Digital Voice Keyer take this slot when enabled (Log4OM-style);
|
||||
otherwise it shows the QRZ profile photo. */}
|
||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath))) && (
|
||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled)) && (
|
||||
<div className="flex-1 min-w-0 min-h-0 flex gap-2.5 items-stretch">
|
||||
{chatShown && (
|
||||
// relative + absolute inner: the chat takes the row height (set by the
|
||||
@@ -3249,6 +3306,15 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAntGenius && agEnabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<AntGeniusPanel
|
||||
status={agStatus}
|
||||
onActivate={agActivate}
|
||||
onClose={() => { setShowAntGenius(false); writeUiPref('opslog.showAntGenius', '0'); }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{dvkEnabled && (
|
||||
<div className="w-[264px] shrink-0 min-h-0">
|
||||
<DvkPanel
|
||||
@@ -3316,7 +3382,7 @@ export default function App() {
|
||||
|
||||
{/* ===== CW decoder strip (only when enabled AND mode is CW) ===== */}
|
||||
{cwOn && (
|
||||
<div className="ml-2.5 mb-1 w-[45%] flex items-center gap-2 rounded-md border border-emerald-300/70 bg-emerald-50/60 px-2 py-1 text-xs">
|
||||
<div className="ml-2.5 my-1.5 w-[45%] flex items-center gap-2 rounded-md border border-emerald-300/70 bg-emerald-50/60 px-2 py-1.5 text-xs">
|
||||
<Ear className={cn('size-4 shrink-0', cwStatus.active ? 'text-emerald-600' : 'text-muted-foreground')} />
|
||||
{/* Input-level meter — if this stays flat with a strong signal, the RX
|
||||
audio device is wrong/silent rather than a decode problem. */}
|
||||
@@ -3383,6 +3449,7 @@ export default function App() {
|
||||
<TabsTrigger value="awards">Awards</TabsTrigger>
|
||||
<TabsTrigger value="bandmap">Band Map</TabsTrigger>
|
||||
{catState.backend === 'flex' && <TabsTrigger value="flex">FlexRadio</TabsTrigger>}
|
||||
{catState.backend === 'icom' && <TabsTrigger value="icom">Icom</TabsTrigger>}
|
||||
{/* Not a tab — QRZ blocks embedding, so this opens the call's
|
||||
QRZ.com page in the system browser. Styled like a trigger. */}
|
||||
<button
|
||||
@@ -3696,6 +3763,14 @@ export default function App() {
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* Icom CI-V receive-DSP control panel — only when the CAT backend
|
||||
is an Icom. */}
|
||||
{catState.backend === 'icom' && (
|
||||
<TabsContent value="icom" className="flex-1 min-h-0 p-0">
|
||||
<IcomPanel />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* Band Map: several bands shown side-by-side (panadapter-style
|
||||
strips). Pick bands with the chips; each strip is clickable to
|
||||
tune the rig. */}
|
||||
|
||||
Reference in New Issue
Block a user