feat: Support for Power Genius XL
This commit is contained in:
@@ -3382,7 +3382,7 @@ export default function App() {
|
||||
|
||||
{/* ===== CW decoder strip (only when enabled AND mode is CW) ===== */}
|
||||
{cwOn && (
|
||||
<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">
|
||||
<div className="ml-2.5 mt-1.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.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. */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||
FlexMox, FlexAmpOperate,
|
||||
GetPGXLStatus, PGXLSetFanMode,
|
||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel,
|
||||
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
||||
FlexSetAPF, FlexSetAPFLevel, FlexSetCWSpeed, FlexSetCWPitch, FlexSetCWBreakInDelay,
|
||||
@@ -215,6 +216,16 @@ export function FlexPanel() {
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
|
||||
// PowerGenius XL direct connection (fan mode), independent of the Flex link.
|
||||
const [pg, setPg] = useState<{ connected: boolean; fan_mode?: string }>({ connected: false });
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = async () => { try { const s: any = await GetPGXLStatus(); if (alive) setPg(s || { connected: false }); } catch {} };
|
||||
tick();
|
||||
const id = window.setInterval(tick, 2000);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
|
||||
const change = (key: keyof FlexState, val: number | boolean | string, send: () => Promise<any>) => {
|
||||
hold.current[key] = Date.now() + 900;
|
||||
setSt((p) => ({ ...p, [key]: val }));
|
||||
@@ -430,6 +441,22 @@ export function FlexPanel() {
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{st.amp_operate ? 'Amplifier is in line (transmitting through PA).' : 'Amplifier bypassed (standby).'}
|
||||
</span>
|
||||
{/* Fan mode — only when the PowerGenius direct connection is up
|
||||
(configured in Settings → PowerGenius). */}
|
||||
{pg.connected && (
|
||||
<label className="flex items-center gap-1.5 text-xs">
|
||||
<span className="text-muted-foreground">Fan</span>
|
||||
<select
|
||||
value={(pg.fan_mode || 'CONTEST').toUpperCase()}
|
||||
onChange={(e) => { const v = e.target.value; setPg((s) => ({ ...s, fan_mode: v })); PGXLSetFanMode(v).catch(() => {}); }}
|
||||
className="h-8 rounded-md border border-orange-300 bg-card px-2 text-xs font-semibold text-orange-800 outline-none focus:border-orange-500"
|
||||
>
|
||||
<option value="STANDARD">Standard</option>
|
||||
<option value="CONTEST">Contest</option>
|
||||
<option value="BROADCAST">Broadcast</option>
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{st.amp_fault && st.amp_fault !== 'NONE' && (
|
||||
<span className="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs font-bold">FAULT: {st.amp_fault}</span>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GetRotatorSettings, SaveRotatorSettings, TestRotator, RotatorPark, RotatorStop,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetPGXLSettings, SavePGXLSettings,
|
||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
|
||||
GetAudioSettings, SaveAudioSettings, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
|
||||
GetClublogCtyInfo, SetClublogCtyEnabled, DownloadClublogCty,
|
||||
@@ -172,6 +173,7 @@ type SectionId =
|
||||
| 'winkeyer'
|
||||
| 'antenna'
|
||||
| 'antgenius'
|
||||
| 'pgxl'
|
||||
| 'audio';
|
||||
|
||||
type TreeNode =
|
||||
@@ -210,6 +212,7 @@ const TREE: TreeNode[] = [
|
||||
{ kind: 'item', label: 'CW Keyer', id: 'winkeyer' },
|
||||
{ kind: 'item', label: 'Antenna', id: 'antenna' },
|
||||
{ kind: 'item', label: 'Antenna Genius', id: 'antgenius' },
|
||||
{ kind: 'item', label: 'PowerGenius', id: 'pgxl' },
|
||||
{ kind: 'item', label: 'Audio devices', id: 'audio' },
|
||||
],
|
||||
},
|
||||
@@ -236,6 +239,7 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
winkeyer: 'CW Keyer',
|
||||
antenna: 'Antenna',
|
||||
antgenius: 'Antenna Genius',
|
||||
pgxl: 'PowerGenius',
|
||||
audio: 'Audio devices',
|
||||
};
|
||||
|
||||
@@ -634,6 +638,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
// Antenna Genius (4O3A) switch settings — TCP port is fixed at 9007.
|
||||
const [antgenius, setAntgenius] = useState<{ enabled: boolean; host: string }>({ enabled: false, host: '' });
|
||||
|
||||
// PowerGenius XL (4O3A) amp fan-control settings.
|
||||
const [pgxl, setPgxl] = useState<{ enabled: boolean; host: string; port: number }>({ enabled: false, host: '', port: 9006 });
|
||||
|
||||
// WinKeyer CW keyer settings + macro editor.
|
||||
type WKMac = { label: string; text: string };
|
||||
type WKSettings = {
|
||||
@@ -892,6 +899,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
setRotator(r);
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setPgxl(await GetPGXLSettings() as any); } catch {}
|
||||
setBackupCfg(b as any);
|
||||
setQslDefaults(qd as any);
|
||||
setExtSvc(es as any);
|
||||
@@ -932,6 +940,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
try { setRotator(await GetRotatorSettings() as any); } catch {}
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setPgxl(await GetPGXLSettings() as any); } catch {}
|
||||
try { setBackupCfg(await GetBackupSettings() as any); } catch {}
|
||||
try { setQslDefaults(await GetQSLDefaults() as any); } catch {}
|
||||
try { setExtSvc(await GetExternalServices() as any); } catch {}
|
||||
@@ -1100,6 +1109,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
await SaveRotatorSettings(rotator as any);
|
||||
await SaveUltrabeamSettings(ultrabeam as any);
|
||||
await SaveAntGeniusSettings(antgenius as any);
|
||||
await SavePGXLSettings(pgxl as any);
|
||||
await SaveWinkeyerSettings(wk as any);
|
||||
await SaveAudioSettings(audioCfg as any);
|
||||
await SaveEmailSettings(emailCfg as any);
|
||||
@@ -2052,6 +2062,46 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
);
|
||||
}
|
||||
|
||||
function PGXLPanelSettings() {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="PowerGenius XL"
|
||||
hint="OpsLog reads the amp's operate state through the FlexRadio, but the fan mode is only reachable on the PowerGenius XL's own TCP control port. Enter its IP to add a fan-mode selector next to Operate in the FlexRadio panel."
|
||||
/>
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={pgxl.enabled} onCheckedChange={(c) => setPgxl((s) => ({ ...s, enabled: !!c }))} />
|
||||
Enable PowerGenius fan control
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>Host / IP</Label>
|
||||
<Input
|
||||
value={pgxl.host ?? ''}
|
||||
onChange={(e) => setPgxl((s) => ({ ...s, host: e.target.value }))}
|
||||
placeholder="192.168.1.70"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>TCP port</Label>
|
||||
<Input
|
||||
type="number" min={1} max={65535}
|
||||
value={pgxl.port}
|
||||
onChange={(e) => setPgxl((s) => ({ ...s, port: parseInt(e.target.value) || 9006 }))}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default port is 9006. Once enabled, a fan-mode dropdown (Standard / Contest / Broadcast) appears next to the amplifier Operate button in the FlexRadio tab.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RotatorPanel() {
|
||||
return (
|
||||
<>
|
||||
@@ -3808,6 +3858,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
winkeyer: WinkeyerPanel,
|
||||
antenna: UltrabeamPanel,
|
||||
antgenius: AntGeniusPanelSettings,
|
||||
pgxl: PGXLPanelSettings,
|
||||
audio: AudioPanel,
|
||||
};
|
||||
|
||||
|
||||
Vendored
+9
@@ -10,6 +10,7 @@ import {award} from '../models';
|
||||
import {awardref} from '../models';
|
||||
import {cluster} from '../models';
|
||||
import {extsvc} from '../models';
|
||||
import {powergenius} from '../models';
|
||||
import {winkeyer} from '../models';
|
||||
import {audio} from '../models';
|
||||
import {operating} from '../models';
|
||||
@@ -272,6 +273,10 @@ export function GetMySQLSettings():Promise<main.MySQLSettings>;
|
||||
|
||||
export function GetOnlineOperators():Promise<Array<main.ChatPresence>>;
|
||||
|
||||
export function GetPGXLSettings():Promise<main.PGXLSettings>;
|
||||
|
||||
export function GetPGXLStatus():Promise<powergenius.Status>;
|
||||
|
||||
export function GetPOTAToken():Promise<string>;
|
||||
|
||||
export function GetQSLDefaults():Promise<main.QSLDefaults>;
|
||||
@@ -374,6 +379,8 @@ export function OpenExternalURL(arg1:string):Promise<void>;
|
||||
|
||||
export function OperatingDefaultForBand(arg1:string):Promise<operating.BandDefault>;
|
||||
|
||||
export function PGXLSetFanMode(arg1:string):Promise<void>;
|
||||
|
||||
export function PickAudioFolder():Promise<string>;
|
||||
|
||||
export function PickBackupFolder():Promise<string>;
|
||||
@@ -486,6 +493,8 @@ export function SaveOperatingAntenna(arg1:operating.Antenna):Promise<operating.A
|
||||
|
||||
export function SaveOperatingStation(arg1:operating.Station):Promise<operating.Station>;
|
||||
|
||||
export function SavePGXLSettings(arg1:main.PGXLSettings):Promise<void>;
|
||||
|
||||
export function SavePOTAToken(arg1:string):Promise<void>;
|
||||
|
||||
export function SaveProfile(arg1:profile.Profile):Promise<profile.Profile>;
|
||||
|
||||
@@ -514,6 +514,14 @@ export function GetOnlineOperators() {
|
||||
return window['go']['main']['App']['GetOnlineOperators']();
|
||||
}
|
||||
|
||||
export function GetPGXLSettings() {
|
||||
return window['go']['main']['App']['GetPGXLSettings']();
|
||||
}
|
||||
|
||||
export function GetPGXLStatus() {
|
||||
return window['go']['main']['App']['GetPGXLStatus']();
|
||||
}
|
||||
|
||||
export function GetPOTAToken() {
|
||||
return window['go']['main']['App']['GetPOTAToken']();
|
||||
}
|
||||
@@ -718,6 +726,10 @@ export function OperatingDefaultForBand(arg1) {
|
||||
return window['go']['main']['App']['OperatingDefaultForBand'](arg1);
|
||||
}
|
||||
|
||||
export function PGXLSetFanMode(arg1) {
|
||||
return window['go']['main']['App']['PGXLSetFanMode'](arg1);
|
||||
}
|
||||
|
||||
export function PickAudioFolder() {
|
||||
return window['go']['main']['App']['PickAudioFolder']();
|
||||
}
|
||||
@@ -942,6 +954,10 @@ export function SaveOperatingStation(arg1) {
|
||||
return window['go']['main']['App']['SaveOperatingStation'](arg1);
|
||||
}
|
||||
|
||||
export function SavePGXLSettings(arg1) {
|
||||
return window['go']['main']['App']['SavePGXLSettings'](arg1);
|
||||
}
|
||||
|
||||
export function SavePOTAToken(arg1) {
|
||||
return window['go']['main']['App']['SavePOTAToken'](arg1);
|
||||
}
|
||||
|
||||
@@ -1473,6 +1473,22 @@ export namespace main {
|
||||
this.database = source["database"];
|
||||
}
|
||||
}
|
||||
export class PGXLSettings {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new PGXLSettings(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.enabled = source["enabled"];
|
||||
this.host = source["host"];
|
||||
this.port = source["port"];
|
||||
}
|
||||
}
|
||||
export class POTAUnmatched {
|
||||
activator: string;
|
||||
date: string;
|
||||
@@ -2109,6 +2125,33 @@ export namespace operating {
|
||||
|
||||
}
|
||||
|
||||
export namespace powergenius {
|
||||
|
||||
export class Status {
|
||||
connected: boolean;
|
||||
host?: string;
|
||||
last_error?: string;
|
||||
state?: string;
|
||||
fan_mode?: string;
|
||||
temperature: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Status(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.connected = source["connected"];
|
||||
this.host = source["host"];
|
||||
this.last_error = source["last_error"];
|
||||
this.state = source["state"];
|
||||
this.fan_mode = source["fan_mode"];
|
||||
this.temperature = source["temperature"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace profile {
|
||||
|
||||
export class ProfileDB {
|
||||
|
||||
Reference in New Issue
Block a user