feat: Added Antenna selection to Flex FlexPanel
feat: New settings flexradio to select antennas per band
This commit is contained in:
+14
-1
@@ -13,7 +13,7 @@ import {
|
||||
GetStartupStatus, CheckForUpdate,
|
||||
WorkedBefore,
|
||||
SetCompactMode,
|
||||
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
|
||||
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna,
|
||||
GetSecretStatus, UnlockSecrets,
|
||||
RefreshCtyDat, DownloadAllReferenceLists,
|
||||
RotatorGoTo, RotatorStop, GetRotatorHeading,
|
||||
@@ -1431,6 +1431,19 @@ export default function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// FlexRadio: apply the per-band RX/TX antennas whenever the band changes (rig
|
||||
// QSY, spot click, manual band) — so the right antennas follow the frequency.
|
||||
// Skipped while the band is LOCKED (entering an old QSO off-air) and for non-Flex
|
||||
// backends. The backend no-ops if the band has no configured mapping.
|
||||
const lastAntBandRef = useRef('');
|
||||
useEffect(() => {
|
||||
if (catState.backend !== 'flex' || locks.band) return;
|
||||
const b = band.trim();
|
||||
if (!b || b === lastAntBandRef.current) return;
|
||||
lastAntBandRef.current = b;
|
||||
FlexApplyBandAntenna(b).catch(() => {});
|
||||
}, [band, catState.backend, locks.band]);
|
||||
|
||||
// Cluster live wiring: hydrate per-server status + saved server list,
|
||||
// then subscribe to push events.
|
||||
async function reloadClusterMeta() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||
FlexMox, FlexAmpOperate,
|
||||
GetPGXLStatus, PGXLSetFanMode,
|
||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute,
|
||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna,
|
||||
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
||||
FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay,
|
||||
FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter, FlexSetFilter,
|
||||
@@ -20,6 +20,7 @@ type FlexState = {
|
||||
mon: boolean; mon_level: number; mic_level: number;
|
||||
atu_status?: string; atu_memories: boolean;
|
||||
rx_avail: boolean; agc_mode?: string; agc_threshold: number; audio_level: number; mute: boolean;
|
||||
rx_ant?: string; tx_ant?: string; ant_list?: string[]; tx_ant_list?: string[];
|
||||
nb: boolean; nb_level: number; nr: boolean; nr_level: number; anf: boolean; anf_level: number;
|
||||
mode?: string;
|
||||
cw_speed: number; cw_pitch: number; cw_break_in_delay: number; cw_sidetone: boolean; cw_mon_level: number;
|
||||
@@ -357,6 +358,25 @@ export function FlexPanel() {
|
||||
|
||||
{/* RECEIVE */}
|
||||
<Card icon={AudioLines} title="Receive (active slice)" accent="#0891b2">
|
||||
{((st.ant_list?.length ?? 0) > 0 || (st.tx_ant_list?.length ?? 0) > 0) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">Ant</span>
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
<span className="text-[10px] text-muted-foreground">RX</span>
|
||||
<select disabled={rxOff} value={st.rx_ant ?? ''}
|
||||
onChange={(e) => change('rx_ant', e.target.value, () => FlexSetRXAntenna(e.target.value))}
|
||||
className="h-7 flex-1 min-w-0 rounded-md border border-input bg-background px-1.5 text-xs font-mono disabled:opacity-40">
|
||||
{(st.ant_list ?? []).map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
<span className="text-[10px] text-muted-foreground">TX</span>
|
||||
<select disabled={rxOff} value={st.tx_ant ?? ''}
|
||||
onChange={(e) => change('tx_ant', e.target.value, () => FlexSetTXAntenna(e.target.value))}
|
||||
className="h-7 flex-1 min-w-0 rounded-md border border-input bg-background px-1.5 text-xs font-mono disabled:opacity-40">
|
||||
{((st.tx_ant_list?.length ? st.tx_ant_list : st.ant_list) ?? []).map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">AGC</span>
|
||||
<Segmented value={(st.agc_mode || 'med').toLowerCase()} options={AGC} disabled={rxOff}
|
||||
|
||||
@@ -164,15 +164,16 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
|
||||
return out;
|
||||
};
|
||||
for (const az of beamAzimuths) {
|
||||
// Draw the lobe as a single FILLED shape that HUGS the great-circle
|
||||
// curve: bound it by the two edge radials (az ± half-beamwidth), each a
|
||||
// great-circle line, joined by a short far cap. Filling between the two
|
||||
// curved edges follows the arc instead of making a straight triangle.
|
||||
const edgeL = radial(az - half);
|
||||
const edgeR = radial(az + half);
|
||||
const lobe = [[from.lat, from.lon], ...edgeL, ...edgeR.slice().reverse()] as [number, number][];
|
||||
L.polygon(unwrapLon(lobe) as L.LatLngExpression[],
|
||||
{ stroke: false, fillColor: '#ff2d2d', fillOpacity: 0.18, smoothFactor: 0 }).addTo(wo);
|
||||
// Draw the lobe as a DENSE fan of translucent great-circle radials, not
|
||||
// a filled polygon: a polygon smears badly near the poles on Mercator
|
||||
// (a poleward beam degenerates into a giant triangle), while each radial
|
||||
// LINE stays clean at any azimuth. A small angular step makes the lines
|
||||
// overlap into a solid-looking lobe (no separate strokes), darker near
|
||||
// the antenna and fanning out toward the front.
|
||||
for (let b = az - half; b <= az + half + 0.001; b += 0.5) {
|
||||
const line = unwrapLon([[from.lat, from.lon], ...radial(b)]);
|
||||
L.polyline(line as L.LatLngExpression[], { color: '#ff2d2d', weight: 6, opacity: 0.10, smoothFactor: 0 }).addTo(wo);
|
||||
}
|
||||
|
||||
const cl = unwrapLon([[from.lat, from.lon], ...radial(az)]);
|
||||
// Dark casing under the boresight so the bright dashed line stays
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
TestLoTWUpload, ListTQSLStationLocations,
|
||||
ComputeStationInfo,
|
||||
GetUIPref, SetUIPref,
|
||||
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import type { profile as profileModels } from '../../wailsjs/go/models';
|
||||
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -174,13 +175,27 @@ type SectionId =
|
||||
| 'antenna'
|
||||
| 'antgenius'
|
||||
| 'pgxl'
|
||||
| 'flex'
|
||||
| 'audio';
|
||||
|
||||
type TreeNode =
|
||||
| { kind: 'group'; label: string; icon?: any; defaultOpen?: boolean; children: TreeNode[] }
|
||||
| { kind: 'item'; label: string; id: SectionId; disabled?: boolean };
|
||||
|
||||
const TREE: TreeNode[] = [
|
||||
// buildTree returns the settings sidebar. The FlexRadio item only appears when
|
||||
// the active CAT backend is a Flex (per-band antenna config is Flex-specific).
|
||||
function buildTree(flexAvailable: boolean): TreeNode[] {
|
||||
const hardware: TreeNode[] = [
|
||||
{ kind: 'item', label: 'CAT interface', id: 'cat' },
|
||||
{ kind: 'item', label: 'PstRotator', id: 'rotator' },
|
||||
{ kind: 'item', label: 'CW Keyer', id: 'winkeyer' },
|
||||
{ kind: 'item', label: 'UltraBeam', id: 'antenna' },
|
||||
{ kind: 'item', label: 'Antenna Genius', id: 'antgenius' },
|
||||
{ kind: 'item', label: 'Power Genius', id: 'pgxl' },
|
||||
...(flexAvailable ? [{ kind: 'item', label: 'FlexRadio', id: 'flex' } as TreeNode] : []),
|
||||
{ kind: 'item', label: 'Audio devices', id: 'audio' },
|
||||
];
|
||||
return [
|
||||
{
|
||||
kind: 'group', label: 'User Configuration', icon: User, defaultOpen: true, children: [
|
||||
{ kind: 'item', label: 'Station Information', id: 'station' },
|
||||
@@ -206,17 +221,10 @@ const TREE: TreeNode[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'group', label: 'Hardware Configuration', icon: Server, defaultOpen: true, children: [
|
||||
{ kind: 'item', label: 'CAT interface', id: 'cat' },
|
||||
{ kind: 'item', label: 'PstRotator', id: 'rotator' },
|
||||
{ kind: 'item', label: 'CW Keyer', id: 'winkeyer' },
|
||||
{ kind: 'item', label: 'UltraBeam', id: 'antenna' },
|
||||
{ kind: 'item', label: 'Antenna Genius', id: 'antgenius' },
|
||||
{ kind: 'item', label: 'Power Genius', id: 'pgxl' },
|
||||
{ kind: 'item', label: 'Audio devices', id: 'audio' },
|
||||
],
|
||||
kind: 'group', label: 'Hardware Configuration', icon: Server, defaultOpen: true, children: hardware,
|
||||
},
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
// Map section id → friendly name (used in breadcrumb / placeholders).
|
||||
const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
@@ -240,6 +248,7 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
antenna: 'UltraBeam',
|
||||
antgenius: 'Antenna Genius',
|
||||
pgxl: 'Power Genius',
|
||||
flex: 'FlexRadio',
|
||||
audio: 'Audio devices',
|
||||
};
|
||||
|
||||
@@ -248,12 +257,13 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
interface TreeProps {
|
||||
selected: SectionId;
|
||||
onSelect: (id: SectionId) => void;
|
||||
flexAvailable?: boolean;
|
||||
}
|
||||
|
||||
function Tree({ selected, onSelect }: TreeProps) {
|
||||
function Tree({ selected, onSelect, flexAvailable }: TreeProps) {
|
||||
return (
|
||||
<nav className="text-sm">
|
||||
{TREE.map((node, i) => (
|
||||
{buildTree(!!flexAvailable).map((node, i) => (
|
||||
<TreeNodeView key={i} node={node} depth={0} selected={selected} onSelect={onSelect} />
|
||||
))}
|
||||
</nav>
|
||||
@@ -585,6 +595,90 @@ function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
// FlexBandAntennasPanel — pick the RX/TX antenna per band. Applied automatically
|
||||
// when the band changes (frequency change / spot click). Antennas come live from
|
||||
// the connected FlexRadio; bands come from Lists → Bands.
|
||||
function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
|
||||
const [rxList, setRxList] = useState<string[]>([]);
|
||||
const [txList, setTxList] = useState<string[]>([]);
|
||||
const [map, setMap] = useState<Record<string, { rx: string; tx: string }>>({});
|
||||
const [msg, setMsg] = useState('');
|
||||
useEffect(() => {
|
||||
GetFlexState().then((s: any) => {
|
||||
setRxList((s?.ant_list ?? []) as string[]);
|
||||
setTxList(((s?.tx_ant_list?.length ? s.tx_ant_list : s?.ant_list) ?? []) as string[]);
|
||||
}).catch(() => {});
|
||||
GetFlexBandAntennas().then((m: any) => setMap(m ?? {})).catch(() => {});
|
||||
}, []);
|
||||
const set = (band: string, side: 'rx' | 'tx', v: string) => {
|
||||
const key = band.toUpperCase();
|
||||
setMap((m) => {
|
||||
const cur = m[key] ?? { rx: '', tx: '' };
|
||||
const next = { ...m, [key]: { ...cur, [side]: v } };
|
||||
SaveFlexBandAntennas(next as any)
|
||||
.then(() => { setMsg('Saved'); window.setTimeout(() => setMsg(''), 1200); })
|
||||
.catch((e: any) => setMsg(String(e?.message ?? e)));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">FlexRadio — per-band antennas</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose the RX and TX antenna for each band. They're applied automatically when the band
|
||||
changes (frequency change or clicking a spot).
|
||||
</p>
|
||||
</div>
|
||||
{rxList.length === 0 && (
|
||||
<div className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-md px-3 py-2 max-w-xl">
|
||||
No antennas reported yet — make sure the FlexRadio is connected (CAT interface), then reopen this panel.
|
||||
</div>
|
||||
)}
|
||||
{bands.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No bands configured — add them in Lists → Bands.</p>
|
||||
) : (
|
||||
<div className="rounded-md border border-border overflow-hidden max-w-xl">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/40 text-muted-foreground text-xs">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">Band</th>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">RX antenna</th>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">TX antenna</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bands.map((b) => {
|
||||
const e = map[b.toUpperCase()] ?? { rx: '', tx: '' };
|
||||
return (
|
||||
<tr key={b} className="border-t border-border/50">
|
||||
<td className="px-3 py-1.5 font-mono font-semibold">{b}</td>
|
||||
<td className="px-3 py-1.5">
|
||||
<select value={e.rx ?? ''} onChange={(ev) => set(b, 'rx', ev.target.value)}
|
||||
className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
|
||||
<option value="">— none —</option>
|
||||
{rxList.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-3 py-1.5">
|
||||
<select value={e.tx ?? ''} onChange={(ev) => set(b, 'tx', ev.target.value)}
|
||||
className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
|
||||
<option value="">— none —</option>
|
||||
{txList.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{msg && <span className="text-[11px] text-muted-foreground">{msg}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable }: Props) {
|
||||
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -3848,6 +3942,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
antenna: UltrabeamPanel,
|
||||
antgenius: AntGeniusPanelSettings,
|
||||
pgxl: PGXLPanelSettings,
|
||||
flex: () => <FlexBandAntennasPanel bands={lists.bands ?? []} />,
|
||||
audio: AudioPanel,
|
||||
};
|
||||
|
||||
@@ -3865,7 +3960,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<div className="grid grid-cols-[320px_1fr] min-h-0 overflow-hidden">
|
||||
{/* Left sidebar tree */}
|
||||
<aside className="border-r border-border bg-muted/30 overflow-y-auto p-2">
|
||||
<Tree selected={selected} onSelect={setSelected} />
|
||||
<Tree selected={selected} onSelect={setSelected} flexAvailable={flexAvailable} />
|
||||
</aside>
|
||||
|
||||
{/* Right content pane */}
|
||||
|
||||
Reference in New Issue
Block a user