fix: Antenna Genius show only available antennas per band

This commit is contained in:
2026-07-09 15:52:04 +02:00
parent e487aa78f3
commit 5ae2bad549
5 changed files with 66 additions and 9 deletions
+40 -4
View File
@@ -1,14 +1,30 @@
import { Antenna, X } from 'lucide-react';
import { useState } from 'react';
import { Antenna, X, ListFilter } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
export type AGAntenna = { index: number; name: string };
export type AGAntenna = { index: number; name: string; bands?: number };
export type AGStatus = {
connected: boolean; host?: string; last_error?: string;
port_a: number; port_b: number; tx_a?: boolean; tx_b?: boolean;
antennas: AGAntenna[];
};
// AG_BAND_BITS maps a band name to its bit in the 4O3A Antenna Genius band
// bitmask (standard HF+6m order, bit 0 = 160m). Used to show only the antennas
// configured for the current band, like the native app. If the mapping ever
// proves off on real hardware, the debug log (antgenius: antenna raw …) shows the
// device's actual mask so it can be corrected.
const AG_BAND_BITS: Record<string, number> = {
'160m': 1 << 0, '80m': 1 << 1, '60m': 0, '40m': 1 << 2, '30m': 1 << 3,
'20m': 1 << 4, '17m': 1 << 5, '15m': 1 << 6, '12m': 1 << 7, '10m': 1 << 8, '6m': 1 << 9,
};
function bandBit(band?: string): number {
if (!band) return 0;
return AG_BAND_BITS[band.trim().toLowerCase()] ?? 0;
}
// Format an antenna name: first letter uppercase, the rest lowercase
// (e.g. "DX COMMANDER" → "Dx commander").
function pretty(name: string): string {
@@ -22,13 +38,28 @@ function pretty(name: string): string {
// a port-A button (left) and port-B button (right). Colours: green = selected on
// port A, blue = selected on port B, red (pulsing) = that port is transmitting.
// Clicking an already-selected port deselects it (port → None).
export function AntGeniusPanel({ status, onActivate, onClose }: {
export function AntGeniusPanel({ status, onActivate, onClose, band }: {
status: AGStatus;
onActivate: (port: number, antenna: number) => void; // antenna 0 = deselect
onClose: () => void;
band?: string; // current operating band (e.g. "20m") for the band filter
}) {
const { t } = useI18n();
const list = status.antennas ?? [];
const allAntennas = status.antennas ?? [];
// Band filter (persisted, default ON): show only antennas whose band mask
// includes the current band — matching the native 4O3A app. Antennas with an
// unknown mask (0) always show, and if the filter would hide EVERYTHING (e.g. a
// band we can't map, or masks the device didn't send) we fall back to the full
// list so the panel is never mysteriously empty.
const [bandFilter, setBandFilter] = useState(() => localStorage.getItem('opslog.agBandFilter') !== '0');
const toggleBandFilter = () => setBandFilter((v) => { const n = !v; localStorage.setItem('opslog.agBandFilter', n ? '1' : '0'); return n; });
const bit = bandBit(band);
let list = allAntennas;
if (bandFilter && bit !== 0) {
const filtered = allAntennas.filter((a) => !a.bands || (a.bands & bit) !== 0);
if (filtered.length > 0) list = filtered;
}
const PortBtn = ({ port, index, active, tx }: { port: 1 | 2; index: number; active: boolean; tx: boolean }) => {
const letter = port === 1 ? 'A' : 'B';
@@ -57,6 +88,11 @@ export function AntGeniusPanel({ status, onActivate, onClose }: {
<Antenna className={cn('size-4', status.connected ? 'text-success drop-shadow-[0_0_3px_rgba(16,185,129,0.55)]' : 'text-muted-foreground')} />
<span className="text-xs font-bold uppercase tracking-[0.18em] text-foreground/80">Antenna Genius</span>
<span className="flex-1" />
<button type="button" onClick={toggleBandFilter}
title={bandFilter ? t('agp.filterOnHint', { band: band || '' }) : t('agp.filterOffHint')}
className={cn('transition-colors', bandFilter ? 'text-primary' : 'text-muted-foreground/50 hover:text-muted-foreground')}>
<ListFilter className="size-3.5" />
</button>
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider">
<span className={cn('size-1.5 rounded-full', status.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)] animate-pulse' : 'bg-danger')} />
<span className={status.connected ? 'text-success' : 'text-danger'}>{status.connected ? t('agp.online') : t('agp.offline')}</span>