feat(sat): point the antenna — EasyComm II az/el rotator
EasyComm is what satellite rotator controllers agreed on, so a box that works with SatPC32, Gpredict or Hamlib works here. Serial or TCP, and its own settings rather than the HF rotator's: an az/el pair is a different machine on a different port, and an operator who has both must not have to choose. A great many EasyComm controllers — the Arduino trackers above all — accept commands and never say a word back. That is legal and common, so a silent controller is not treated as a broken one: it is still driven, and the last commanded position is reported in its place, marked as commanded rather than read. A stuck rotator must not be able to hide behind an order it never carried out, which is why the panel shows the antenna's position beside the satellite's. The 450° overlap is the reason a satellite rotator is worth having, so it is used: a pass crossing north continues past 360 instead of unwinding three quarters of a turn with the antenna sweeping the ground. Below the configured elevation the mast is left alone — the numbers are right all the way round the orbit, but a rotator that chases a satellite through the far side of the earth spends the night turning, and a mast has a finite number of turns in it.
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
GetPSUSettings, SavePSUSettings,
|
||||
GetSatSettings, SaveSatSettings, TestSatelliteRotator,
|
||||
GetAmplifiers, SaveAmplifiers, GetAmpStatuses, AmpOperate,
|
||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
|
||||
GetAudioSettings, SaveAudioSettings, AudioApplyLevels, ListAudioInputDevices, ListAudioOutputDevices, PickAudioFolder, TestPTT,
|
||||
@@ -228,6 +229,7 @@ type SectionId =
|
||||
| 'antgenius'
|
||||
| 'tunergenius'
|
||||
| 'psu'
|
||||
| 'satellite'
|
||||
| 'pgxl'
|
||||
| 'flex'
|
||||
| 'relayauto'
|
||||
@@ -294,6 +296,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
|
||||
const hardware: TreeNode[] = [
|
||||
{ kind: 'item', label: t('sec.cat'), id: 'cat' },
|
||||
{ kind: 'item', label: t('sec.rotator'), id: 'rotator' },
|
||||
{ kind: 'item', label: t('sec.satellite'), id: 'satellite' },
|
||||
{ kind: 'item', label: t('sec.winkeyer'), id: 'winkeyer' },
|
||||
{ kind: 'item', label: t('sec.antenna'), id: 'antenna' },
|
||||
{ kind: 'item', label: t('sec.antgenius'), id: 'antgenius', vendor: 'o3a' },
|
||||
@@ -1679,6 +1682,11 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
const [antgenius, setAntgenius] = useState<{ enabled: boolean; host: string; password: string; use_for_my_antenna?: boolean; ant1_port?: number }>({ enabled: false, host: '', password: '', use_for_my_antenna: false, ant1_port: 1 });
|
||||
const [tunergenius, setTunergenius] = useState<{ enabled: boolean; host: string; password: string }>({ enabled: false, host: '', password: '' });
|
||||
const [psuCfg, setPsuCfg] = useState<{ enabled: boolean; com_port: string; baud: number; address: number }>({ enabled: false, com_port: '', baud: 9600, address: 1 });
|
||||
// Satellites: the observer and the az/el rotator. The rest of the satellite
|
||||
// settings (favourites, the pass window) are set in the tab itself, where
|
||||
// they are used.
|
||||
const [satCfg, setSatCfg] = useState<any>({ min_el: 10, window_h: 24, auto_tle: true, grid: '', alt_m: 0, rot_on: false, rot_transport: 'serial', rot_host: '', rot_port: 4533, rot_com: '', rot_baud: 9600, rot_max_az: 360, rot_min_el: 0, rot_step: 5, rot_park: false });
|
||||
const [satTest, setSatTest] = useState('');
|
||||
|
||||
// Amplifier list — operators can run SEVERAL amps (even two SPEs combined),
|
||||
// each with its own connection. Saved as a whole via SaveAmplifiers.
|
||||
@@ -2286,6 +2294,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
|
||||
try { setPsuCfg(await GetPSUSettings() as any); } catch {}
|
||||
try { setSatCfg(await GetSatSettings() as any); } catch {}
|
||||
try { setAmps(((await GetAmplifiers()) ?? []) as AmpUI[]); } catch {}
|
||||
setBackupCfg(b as any);
|
||||
setQslDefaults(qd as any);
|
||||
@@ -2330,6 +2339,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
try { setTunergenius(await GetTunerGeniusSettings() as any); } catch {}
|
||||
try { setPsuCfg(await GetPSUSettings() as any); } catch {}
|
||||
try { setSatCfg(await GetSatSettings() as any); } catch {}
|
||||
try { setAmps(((await GetAmplifiers()) ?? []) as AmpUI[]); } catch {}
|
||||
try { setBackupCfg(await GetBackupSettings() as any); } catch {}
|
||||
try { setQslDefaults(await GetQSLDefaults() as any); } catch {}
|
||||
@@ -2531,6 +2541,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
await SaveAntGeniusSettings(antgenius as any);
|
||||
await SaveTunerGeniusSettings(tunergenius as any);
|
||||
await SavePSUSettings(psuCfg as any);
|
||||
await SaveSatSettings(satCfg as any);
|
||||
await SaveAmplifiers(amps as any);
|
||||
await SaveWinkeyerSettings(wk as any);
|
||||
await SaveAudioSettings(audioCfg as any);
|
||||
@@ -4365,6 +4376,165 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
);
|
||||
}
|
||||
|
||||
// Satellites: where the antenna is, and the machine that points it.
|
||||
//
|
||||
// The observer belongs here rather than in the tab because it is a property
|
||||
// of the station, and the rotator because it is a second machine on a second
|
||||
// port — a station with an HF rotator and an az/el pair must be able to have
|
||||
// both, and choosing between them in one panel would be the wrong question.
|
||||
function SatellitePanelSettings() {
|
||||
const ports = wkPorts;
|
||||
const setPorts = setWkPorts;
|
||||
const set = (k: string, v: any) => setSatCfg((s: any) => ({ ...s, [k]: v }));
|
||||
const num = (v: string) => parseInt(v.replace(/[^0-9-]/g, ''), 10) || 0;
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={t('sec.satellite')} hint={t('satset.hint')} />
|
||||
<div className="space-y-5 max-w-xl">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.grid')}</Label>
|
||||
<Input className="font-mono" placeholder={t('satset.gridPlaceholder')}
|
||||
value={satCfg.grid ?? ''} onChange={(e) => set('grid', e.target.value.toUpperCase())} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.altM')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.alt_m ?? 0)}
|
||||
onChange={(e) => set('alt_m', num(e.target.value))} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.minEl')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.min_el ?? 10)}
|
||||
onChange={(e) => set('min_el', num(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('satset.gridHint')}</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.windowH')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.window_h ?? 24)}
|
||||
onChange={(e) => set('window_h', num(e.target.value))} />
|
||||
</div>
|
||||
<label className="flex items-end gap-2 text-sm cursor-pointer pb-2">
|
||||
<Checkbox checked={!!satCfg.auto_tle} onCheckedChange={(c) => set('auto_tle', !!c)} />
|
||||
{t('satset.autoTle')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-4 space-y-3">
|
||||
<h4 className="text-sm font-semibold text-foreground">{t('satset.rotor')}</h4>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!satCfg.rot_on} onCheckedChange={(c) => set('rot_on', !!c)} />
|
||||
{t('satset.rotEnable')}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">{t('satset.rotHint')}</p>
|
||||
|
||||
{!!satCfg.rot_on && (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotLink')}</Label>
|
||||
<Select value={satCfg.rot_transport || 'serial'} onValueChange={(v) => set('rot_transport', v)}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="serial">{t('satset.rotSerial')}</SelectItem>
|
||||
<SelectItem value="tcp">{t('satset.rotTcp')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{satCfg.rot_transport === 'tcp' ? (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotHost')}</Label>
|
||||
<Input className="font-mono" placeholder="127.0.0.1"
|
||||
value={satCfg.rot_host ?? ''} onChange={(e) => set('rot_host', e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotPort')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.rot_port ?? 4533)}
|
||||
onChange={(e) => set('rot_port', num(e.target.value))} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotCom')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={satCfg.rot_com || '_'} onValueChange={(v) => set('rot_com', v === '_' ? '' : v)}>
|
||||
<SelectTrigger className="h-9 flex-1"><SelectValue placeholder="— COM —" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{ports.length === 0 && <SelectItem value="_" disabled>{t('station.noPorts')}</SelectItem>}
|
||||
{ports.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" onClick={() => ListSerialPorts().then((p) => setPorts((p ?? []) as string[])).catch(() => {})}>
|
||||
↻
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotBaud')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.rot_baud ?? 9600)}
|
||||
onChange={(e) => set('rot_baud', num(e.target.value))} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotRange')}</Label>
|
||||
<Select value={String(satCfg.rot_max_az ?? 360)} onValueChange={(v) => set('rot_max_az', parseInt(v, 10))}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="360">360°</SelectItem>
|
||||
<SelectItem value="450">450°</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotMinEl')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.rot_min_el ?? 0)}
|
||||
onChange={(e) => set('rot_min_el', num(e.target.value))} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('satset.rotStep')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.rot_step ?? 5)}
|
||||
onChange={(e) => set('rot_step', num(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t('satset.rotRangeHint')}</p>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!satCfg.rot_park} onCheckedChange={(c) => set('rot_park', !!c)} />
|
||||
{t('satset.rotPark')}
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" onClick={async () => {
|
||||
setSatTest(t('satset.rotTesting'));
|
||||
try {
|
||||
// Saved first: the test opens the port from the STORED
|
||||
// settings, and testing what is on screen rather than what
|
||||
// is stored is the classic way to prove a COM port that is
|
||||
// not the one about to be used.
|
||||
await SaveSatSettings(satCfg as any);
|
||||
setSatTest(String(await TestSatelliteRotator()));
|
||||
} catch (e: any) { setSatTest(String(e?.message ?? e)); }
|
||||
}}>
|
||||
{t('satset.rotTest')}
|
||||
</Button>
|
||||
{satTest && <span className="text-xs text-muted-foreground">{satTest}</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PGXLPanelSettings() {
|
||||
// The stored `type` stays a flat value ("spe13", "acom700", "pgxl"); the UI
|
||||
// presents it as brand + model.
|
||||
@@ -8370,6 +8540,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
antgenius: AntGeniusPanelSettings,
|
||||
tunergenius: TunerGeniusPanelSettings,
|
||||
psu: PSUPanelSettings,
|
||||
satellite: SatellitePanelSettings,
|
||||
pgxl: PGXLPanelSettings,
|
||||
flex: () => (
|
||||
<div className="space-y-6">
|
||||
|
||||
Reference in New Issue
Block a user