feat(rotator): one list of rotator interfaces, and ERC-M
The satellite page configured its own EasyComm or PstRotator link while five other backends were configured in the rotator list. An operator with one az/el mast therefore described it twice, and could describe it differently the second time — a station that works on HF and not on a pass, for no reason visible anywhere on screen. Now every interface lives in Settings ▸ Rotator, once, and the satellite page stores only a KEY into that list plus the tracking policy that is genuinely its own (minimum elevation, step, park). The key and not the index: deleting the first rotor must not silently point the tracker at a different mast. migrateSatRotator() turns an existing satellite link into a real entry in the list, selects it, and clears the old keys so it cannot run twice. Which rotors have an elevation axis is now a question with one answer, in Go: rotatorTypes plus rotorHasElevation, exposed to the panel by GetRotatorTypes. The dropdown, the labels, each backend's default port and default baud all come from there, so TypeScript no longer keeps a second copy of the same knowledge to drift out of step. Three cases do not follow from the type alone and are treated as such: PstRotator forwards elevation to a mast that may not have any, so the operator says; a SPID's dialect decides (Rot1Prog has no elevation in its reply format); and an ARCO and an ERC-M speak the same GS-232 while only one of them lifts. Each interface carries an Az / Az+El badge beside it. The satellite rotor dropdown LISTS the azimuth-only ones, disabled, rather than hiding them: an operator who owns one rotator and does not see it concludes OpsLog cannot find it, where a greyed row saying "azimuth only" teaches the actual thing. ERC-M by DF9GR is new — the az/el interface for a Yaesu G-5500. It emulates GS-232, so internal/rotator/gs232 grew the elevation half: W for a two-axis move, C2 to read both, falling back to C+B for the firmware that answers C2 with the azimuth alone. That fallback is the point of the parser tests: reading such a reply as "elevation zero" would put the antenna on the horizon, which is the one wrong answer that looks plausible. EasyComm II is promoted to an ordinary rotator interface, so it can also turn the antenna from the compass and from a spot click. The ERC-M is UNTESTED on hardware. Its Test button reads BOTH axes rather than just the azimuth, so a controller wired for azimuth alone says so there instead of during a pass. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -13,13 +13,13 @@ import {
|
||||
GetChaseSettings, SaveChaseSettings,
|
||||
GetAudioMonitorPref,
|
||||
ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop,
|
||||
GetRotators, SaveRotators, TestRotatorDevice, RotatorPark, RotatorStop, GetRotatorTypes,
|
||||
GetRotorPresets, SaveRotorPresets, ResetRotorPresets,
|
||||
GetUltrabeamSettings, SaveUltrabeamSettings, TestUltrabeam, CompactDatabase, CheckHamlogKey, CompareRDASources, ApplyRDAChoices,
|
||||
GetAntGeniusSettings, SaveAntGeniusSettings,
|
||||
GetTunerGeniusSettings, SaveTunerGeniusSettings,
|
||||
GetPSUSettings, SavePSUSettings,
|
||||
GetSatSettings, SaveSatSettings, TestSatelliteRotator,
|
||||
GetSatSettings, SaveSatSettings, TestSatelliteRotator, ListSatelliteRotors,
|
||||
GetSatelliteTLEInfo, RefreshSatelliteTLE, AddSatelliteElements, GetSatelliteBirds,
|
||||
GetAmplifiers, SaveAmplifiers, GetAmpStatuses, AmpOperate,
|
||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts,
|
||||
@@ -1896,6 +1896,15 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
// While true, the next key press is captured as the PTT hotkey.
|
||||
const [capturingPtt, setCapturingPtt] = useState(false);
|
||||
const [rotors, setRotors] = useState<RotatorDevice[]>([]);
|
||||
// What each rotator backend is and what it can do, straight from Go. The
|
||||
// panel used to hold its own copy of the list — labels, default ports, which
|
||||
// ones offer a COM port — and a second copy of that knowledge is a copy that
|
||||
// drifts. It also carries the answer the satellite page needs: which
|
||||
// interfaces drive an elevation axis.
|
||||
const [rotTypes, setRotTypes] = useState<any[]>([]);
|
||||
// The rotors the satellite page may choose between. Read from the same list,
|
||||
// so a mast is described once.
|
||||
const [satRotors, setSatRotors] = useState<any[]>([]);
|
||||
const [rotorPresets, setRotorPresets] = useState<{ label: string; azimuth: number }[]>([]);
|
||||
// Whether the presets have actually been READ back yet.
|
||||
//
|
||||
@@ -1922,7 +1931,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
// 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_type: 'easycomm', rot_pst_port: 12000, 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 [satCfg, setSatCfg] = useState<any>({ min_el: 10, window_h: 24, auto_tle: true, grid: '', alt_m: 0, rot_on: false, rot_id: '', 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),
|
||||
@@ -2517,6 +2526,8 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
await reloadClusterServers();
|
||||
setCatCfg(c);
|
||||
setRotors((r ?? []) as any);
|
||||
try { setRotTypes(((await GetRotatorTypes()) ?? []) as any[]); } catch {}
|
||||
try { setSatRotors(((await ListSatelliteRotors()) ?? []) as any[]); } catch {}
|
||||
// Loaded HERE, in the loader that runs on mount — not only in the
|
||||
// event-driven one below. Missing from this one, the state stayed empty
|
||||
// on a normal open and Save then wrote an empty list over the operator's
|
||||
@@ -2579,6 +2590,8 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
try { setLookup(await GetLookupSettings() as any); } catch {}
|
||||
try { setCatCfg(await GetCATSettings() as any); } catch {}
|
||||
try { setRotors(((await GetRotators()) ?? []) as any); } catch {}
|
||||
try { setRotTypes(((await GetRotatorTypes()) ?? []) as any[]); } catch {}
|
||||
try { setSatRotors(((await ListSatelliteRotors()) ?? []) as any[]); } catch {}
|
||||
try { setRotorPresets((((await GetRotorPresets()) ?? []) as any)); setRotorPresetsLoaded(true); } catch {}
|
||||
try { setUltrabeam(await GetUltrabeamSettings() as any); } catch {}
|
||||
try { setAntgenius(await GetAntGeniusSettings() as any); } catch {}
|
||||
@@ -2780,6 +2793,10 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
await SaveLookupSettings(lookup as any);
|
||||
await SaveCATSettings(catCfg as any);
|
||||
await SaveRotators(rotors as any);
|
||||
// The satellite page picks from this list. Re-read it after a save so a
|
||||
// rotor added on the Rotator panel can be chosen straight away, without
|
||||
// closing the window first.
|
||||
try { setSatRotors(((await ListSatelliteRotors()) ?? []) as any[]); } catch {}
|
||||
// Only once they have been read back — see rotorPresetsLoaded.
|
||||
if (rotorPresetsLoaded) await SaveRotorPresets(rotorPresets as any);
|
||||
await SaveUltrabeamSettings(ultrabeam as any);
|
||||
@@ -4673,113 +4690,40 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
|
||||
{!!satCfg.rot_on && (
|
||||
<>
|
||||
{/* Who drives the mast. Not a detail: a station already running
|
||||
PstRotator must NOT have OpsLog on the same cable as well. */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{/* Two columns wide: "OpsLog (EasyComm II)" does not fit in a
|
||||
third of the row, and a truncated choice is a choice an
|
||||
operator cannot read. */}
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>{t('satset.rotType')}</Label>
|
||||
<Select value={satCfg.rot_type || 'easycomm'} onValueChange={(v) => set('rot_type', v)}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
{/* WHICH rotor, not how to reach it. Every interface is
|
||||
described once, in Settings ▸ Rotator; this page only picks
|
||||
one of them. Describing one mast in two places is how a
|
||||
station ends up working on HF and not on a pass. */}
|
||||
<div className="space-y-1 max-w-md">
|
||||
<Label>{t('satset.rotPick')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={satCfg.rot_id || '_'} onValueChange={(v) => set('rot_id', v === '_' ? '' : v)}>
|
||||
<SelectTrigger className="h-9 flex-1"><SelectValue placeholder={t('satset.rotPickNone')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="easycomm">{t('satset.rotEasycomm')}</SelectItem>
|
||||
<SelectItem value="pstrotator">{t('satset.rotPst')}</SelectItem>
|
||||
{satRotors.length === 0 && <SelectItem value="_" disabled>{t('satset.rotNoneConfigured')}</SelectItem>}
|
||||
{/* The azimuth-only rotors are LISTED, and disabled. An
|
||||
operator who owns one rotator and does not see it
|
||||
concludes OpsLog cannot find it; shown greyed with
|
||||
"azimuth only" beside it, they learn the real thing. */}
|
||||
{satRotors.map((r: any) => (
|
||||
<SelectItem key={r.key} value={r.key} disabled={!r.has_el}>
|
||||
{(r.name || r.type) + (r.has_el ? '' : ` — ${t('satset.rotAzOnly')}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button size="sm" variant="outline" className="h-9"
|
||||
onClick={() => ListSatelliteRotors().then((r) => setSatRotors((r ?? []) as any[])).catch(() => {})}>
|
||||
↻
|
||||
</Button>
|
||||
</div>
|
||||
{satCfg.rot_type === 'pstrotator' && (
|
||||
<>
|
||||
<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.rotPstPort')}</Label>
|
||||
<Input className="font-mono" value={String(satCfg.rot_pst_port ?? 12000)}
|
||||
onChange={(e) => set('rot_pst_port', num(e.target.value))} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{satCfg.rot_type === 'pstrotator' && (
|
||||
<p className="text-xs text-muted-foreground">{t('satset.rotPstHint')}</p>
|
||||
)}
|
||||
<div className={cn('grid grid-cols-3 gap-3', satCfg.rot_type === 'pstrotator' && 'hidden')}>
|
||||
<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>
|
||||
<Select value={String(satCfg.rot_baud || 9600)} onValueChange={(v) => set('rot_baud', parseInt(v, 10) || 9600)}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{[1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200].map((b) => (
|
||||
<SelectItem key={b} value={String(b)}>{b}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
<p className="text-xs text-muted-foreground">{t('satset.rotPickHint')}</p>
|
||||
{satRotors.length > 0 && !satRotors.some((r: any) => r.has_el) && (
|
||||
<p className="text-xs text-[var(--warning)]">{t('satset.rotNoElAtAll')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* The rotator's range is ours to know only when we drive the
|
||||
controller. PstRotator knows which machine is on the other
|
||||
end and does its own overlap; two programs each deciding to
|
||||
go the long way round is how an antenna unwinds mid-pass. */}
|
||||
{satCfg.rot_type !== 'pstrotator' && (
|
||||
<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)}
|
||||
@@ -5138,7 +5082,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
const addRotor = () => setRotors((l) => [...l, {
|
||||
id: '', name: '', type: 'pst', host: '127.0.0.1', port: 12000, has_elevation: false,
|
||||
rotator_num: 1, dual: false, motorized: true, name2: '', motorized2: false,
|
||||
transport: 'tcp', com_port: '', baud: 9600,
|
||||
transport: 'tcp', com_port: '', baud: 9600, max_az: 360,
|
||||
} as any]);
|
||||
const removeRotor = (i: number) => setRotors((l) => l.filter((_, j) => j !== i));
|
||||
const anyPst = rotors.some((d) => ((d as any).type ?? 'pst') === 'pst');
|
||||
@@ -5153,18 +5097,45 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
const dev = d as any;
|
||||
const isRG = dev.type === 'rotgenius';
|
||||
const isARCO = dev.type === 'arco';
|
||||
const isERC = dev.type === 'erc';
|
||||
const isDCU1 = dev.type === 'dcu1';
|
||||
// A SPID has a COM port and nothing else — no network transport to
|
||||
// offer, which is the whole point of driving it without PstRotator.
|
||||
const isSPID = dev.type === 'spid';
|
||||
const isSerialCap = isARCO || isDCU1 || isSPID; // COM-port or serial-over-IP controllers
|
||||
const isEasycomm = dev.type === 'easycomm';
|
||||
const isSerialCap = isARCO || isERC || isDCU1 || isSPID || isEasycomm; // COM-port or serial-over-IP controllers
|
||||
const transport = dev.transport ?? 'tcp';
|
||||
// What this backend can do, from Go. The panel asks rather than
|
||||
// deciding: the same table answers the satellite page's question
|
||||
// about which rotors have an elevation axis, and two tables would
|
||||
// eventually disagree about one rotor.
|
||||
const info = rotTypes.find((k) => k.id === (dev.type ?? 'pst'));
|
||||
// PstRotator is the only backend where the elevation belongs to the
|
||||
// station rather than to the protocol — it forwards EL happily to a
|
||||
// mast that has no elevation motor, so only the operator knows.
|
||||
const elOptional = !!info?.elevation_optional;
|
||||
const hasEl = elOptional ? !!dev.has_elevation
|
||||
: (isSPID ? (dev.spid_model || 'rot2prog') !== 'rot1prog' : !!info?.elevation);
|
||||
// The mast's range is ours to know only when OpsLog drives the
|
||||
// controller. PstRotator knows which machine is on the other end and
|
||||
// does its own overlap; two programs each deciding to go the long
|
||||
// way round is how an antenna unwinds mid-pass.
|
||||
const ownsOverlap = isERC || isEasycomm;
|
||||
return (
|
||||
<div key={dev.id || i} className="rounded-xl border border-border bg-card/40 p-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Compass className="size-4 text-primary shrink-0" />
|
||||
<Input className="h-8 flex-1" value={dev.name ?? ''} placeholder={`Rotor ${i + 1}`}
|
||||
onChange={(e) => patch(i, { name: e.target.value })} />
|
||||
{/* Which axes this interface drives, beside the interface
|
||||
itself. Without it the only way to find out is to pick a
|
||||
rotor on the satellite page and be told no. */}
|
||||
<span className={cn('shrink-0 rounded-md px-2 py-0.5 text-[11px] font-medium border',
|
||||
hasEl ? 'border-[var(--success)]/40 text-[var(--success)] bg-[var(--success)]/10'
|
||||
: 'border-border text-muted-foreground bg-muted/40')}
|
||||
title={hasEl ? t('rot.capAzElHint') : t('rot.capAzHint')}>
|
||||
{hasEl ? t('rot.capAzEl') : t('rot.capAz')}
|
||||
</span>
|
||||
<Button size="icon" variant="ghost" className="size-8 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeRotor(i)} title={t('rot.remove')}>
|
||||
<Trash2 className="size-4" />
|
||||
@@ -5173,17 +5144,28 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.type')}</Label>
|
||||
{/* Each backend gets its default port: Rotator Genius 9006, ARCO 4001
|
||||
(placeholder — must match the ARCO's LAN menu), PstRotator 12000. */}
|
||||
{/* The list, the labels, each backend's default port and its
|
||||
default baud all come from Go — see rotatorTypes. A
|
||||
SPID's 600 baud and an ERC-M's 19200 are not typos, and a
|
||||
wrong rate reads exactly like a dead controller. */}
|
||||
<Select value={dev.type ?? 'pst'}
|
||||
onValueChange={(v) => patch(i, { type: v as any, port: v === 'rotgenius' ? 9006 : (v === 'arco' || v === 'dcu1') ? 4001 : 12000, ...(v === 'dcu1' || v === 'spid' ? { transport: 'serial' } : {}), ...(v === 'spid' ? { baud: 600, spid_model: 'rot2prog' } : {}) })}>
|
||||
onValueChange={(v) => {
|
||||
const k = rotTypes.find((x) => x.id === v);
|
||||
patch(i, {
|
||||
type: v as any,
|
||||
port: k?.default_port || 12000,
|
||||
baud: k?.default_baud || 9600,
|
||||
// A backend with no network transport must not be left
|
||||
// pointing at a TCP host it cannot use.
|
||||
...(k && !k.network ? { transport: 'serial' } : {}),
|
||||
...(v === 'spid' ? { spid_model: 'rot2prog' } : {}),
|
||||
} as any);
|
||||
}}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pst">PstRotator (UDP)</SelectItem>
|
||||
<SelectItem value="rotgenius">Rotator Genius (4O3A, native)</SelectItem>
|
||||
<SelectItem value="arco">GS-232A controller (microHAM ARCO, ERC…)</SelectItem>
|
||||
<SelectItem value="dcu1">Hy-Gain DCU-1 (RotorCard DXA, Rotor-EZ, Green Heron)</SelectItem>
|
||||
<SelectItem value="spid">SPID / AlfaSpid (RAS, BIG-RAS, MD-01, MD-02)</SelectItem>
|
||||
{rotTypes.map((k) => (
|
||||
<SelectItem key={k.id} value={k.id}>{k.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -5216,8 +5198,10 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{/* ARCO and DCU-1 controllers reach over the LAN (TCP) or a serial COM. */}
|
||||
{isSerialCap && !isSPID && (
|
||||
{/* Offered only when the backend really has both. A SPID has a
|
||||
COM port and nothing else, which is the whole point of
|
||||
driving it without PstRotator in front. */}
|
||||
{!!info?.serial && !!info?.network && (
|
||||
<div className="space-y-1">
|
||||
<Label>Connection</Label>
|
||||
<Select value={transport} onValueChange={(v) => patch(i, { transport: v as any })}>
|
||||
@@ -5254,10 +5238,10 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
{/* A SPID runs at 600 or 1200 baud — not a typo, a pulse
|
||||
controller has nothing to say quickly. Offering only the
|
||||
usual rates would have left it permanently mute. */}
|
||||
<Select value={String(dev.baud || (isSPID ? 600 : 9600))} onValueChange={(v) => patch(i, { baud: Number(v) })}>
|
||||
<Select value={String(dev.baud || info?.default_baud || 9600)} onValueChange={(v) => patch(i, { baud: Number(v) })}>
|
||||
<SelectTrigger className="h-9 w-28"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(isSPID ? [600, 1200, 2400, 4800, 9600] : [4800, 9600, 19200, 38400, 57600]).map((b) => (
|
||||
{(isSPID ? [600, 1200, 2400, 4800, 9600] : [4800, 9600, 19200, 38400, 57600, 115200]).map((b) => (
|
||||
<SelectItem key={b} value={String(b)}>{b} baud</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -5274,20 +5258,35 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
<div className="space-y-1">
|
||||
<Label>{isRG || isSerialCap ? 'TCP port' : 'UDP port'}</Label>
|
||||
<PortInput value={dev.port} onChange={(n) => patch(i, { port: n })}
|
||||
fallback={isRG ? 9006 : isSerialCap ? 4001 : 12000} className="font-mono" />
|
||||
fallback={info?.default_port || 12000} className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isRG && !isSerialCap && (
|
||||
{elOptional && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={!!dev.has_elevation} onCheckedChange={(c) => patch(i, { has_elevation: !!c })} />
|
||||
This rotator supports elevation (VHF / satellite)
|
||||
{t('rot.hasElevation')}
|
||||
</label>
|
||||
)}
|
||||
{/* 360 or 450, and only for the backends OpsLog drives itself. */}
|
||||
{ownsOverlap && (
|
||||
<div className="space-y-1 max-w-[10rem]">
|
||||
<Label>{t('rot.range')}</Label>
|
||||
<Select value={String(dev.max_az === 450 ? 450 : 360)} onValueChange={(v) => patch(i, { max_az: parseInt(v, 10) } as any)}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="360">360°</SelectItem>
|
||||
<SelectItem value="450">450°</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{isRG && <p className="text-xs text-muted-foreground">{t('rot.rgHint')}</p>}
|
||||
{isARCO && <p className="text-xs text-muted-foreground">{t('rot.arcoHint')}</p>}
|
||||
{isDCU1 && <p className="text-xs text-muted-foreground">{t('rot.dcu1Hint')}</p>}
|
||||
{isSPID && <p className="text-xs text-muted-foreground">{t('rot.spidHint')}</p>}
|
||||
{isERC && <p className="text-xs text-muted-foreground">{t('rot.ercHint')}</p>}
|
||||
{isEasycomm && <p className="text-xs text-muted-foreground">{t('rot.easycommHint')}</p>}
|
||||
{/* Which antenna this rotor carries — only relevant with >1 rotor. */}
|
||||
{multi && (
|
||||
<div className="space-y-1 max-w-xs">
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+4
@@ -583,6 +583,8 @@ export function GetRelayAuto():Promise<main.RelayAutoConfig>;
|
||||
|
||||
export function GetRotatorHeading():Promise<main.RotatorHeading>;
|
||||
|
||||
export function GetRotatorTypes():Promise<Array<main.RotatorTypeInfo>>;
|
||||
|
||||
export function GetRotators():Promise<Array<main.RotatorDevice>>;
|
||||
|
||||
export function GetRotorPresets():Promise<Array<main.RotorPreset>>;
|
||||
@@ -831,6 +833,8 @@ export function ListQSOFiltered(arg1:qso.QueryFilter):Promise<Array<qso.QSO>>;
|
||||
|
||||
export function ListRadios():Promise<Array<main.RadioListEntry>>;
|
||||
|
||||
export function ListSatelliteRotors():Promise<Array<main.SatelliteRotorChoice>>;
|
||||
|
||||
export function ListSerialPorts():Promise<Array<string>>;
|
||||
|
||||
export function ListTQSLStationLocations():Promise<Array<extsvc.StationLocation>>;
|
||||
|
||||
@@ -1098,6 +1098,10 @@ export function GetRotatorHeading() {
|
||||
return window['go']['main']['App']['GetRotatorHeading']();
|
||||
}
|
||||
|
||||
export function GetRotatorTypes() {
|
||||
return window['go']['main']['App']['GetRotatorTypes']();
|
||||
}
|
||||
|
||||
export function GetRotators() {
|
||||
return window['go']['main']['App']['GetRotators']();
|
||||
}
|
||||
@@ -1594,6 +1598,10 @@ export function ListRadios() {
|
||||
return window['go']['main']['App']['ListRadios']();
|
||||
}
|
||||
|
||||
export function ListSatelliteRotors() {
|
||||
return window['go']['main']['App']['ListSatelliteRotors']();
|
||||
}
|
||||
|
||||
export function ListSerialPorts() {
|
||||
return window['go']['main']['App']['ListSerialPorts']();
|
||||
}
|
||||
|
||||
@@ -3881,6 +3881,7 @@ export namespace main {
|
||||
com_port: string;
|
||||
baud: number;
|
||||
spid_model?: string;
|
||||
max_az?: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RotatorDevice(source);
|
||||
@@ -3903,6 +3904,7 @@ export namespace main {
|
||||
this.com_port = source["com_port"];
|
||||
this.baud = source["baud"];
|
||||
this.spid_model = source["spid_model"];
|
||||
this.max_az = source["max_az"];
|
||||
}
|
||||
}
|
||||
export class RotatorHeading {
|
||||
@@ -3910,6 +3912,8 @@ export namespace main {
|
||||
ok: boolean;
|
||||
azimuth: number;
|
||||
raw: string;
|
||||
elevation: number;
|
||||
has_elevation: boolean;
|
||||
rotors: string[];
|
||||
active: number;
|
||||
motorized: boolean;
|
||||
@@ -3924,11 +3928,39 @@ export namespace main {
|
||||
this.ok = source["ok"];
|
||||
this.azimuth = source["azimuth"];
|
||||
this.raw = source["raw"];
|
||||
this.elevation = source["elevation"];
|
||||
this.has_elevation = source["has_elevation"];
|
||||
this.rotors = source["rotors"];
|
||||
this.active = source["active"];
|
||||
this.motorized = source["motorized"];
|
||||
}
|
||||
}
|
||||
export class RotatorTypeInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
elevation: boolean;
|
||||
elevation_optional: boolean;
|
||||
serial: boolean;
|
||||
network: boolean;
|
||||
default_port: number;
|
||||
default_baud: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RotatorTypeInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.label = source["label"];
|
||||
this.elevation = source["elevation"];
|
||||
this.elevation_optional = source["elevation_optional"];
|
||||
this.serial = source["serial"];
|
||||
this.network = source["network"];
|
||||
this.default_port = source["default_port"];
|
||||
this.default_baud = source["default_baud"];
|
||||
}
|
||||
}
|
||||
export class RotorPreset {
|
||||
label: string;
|
||||
azimuth: number;
|
||||
@@ -4138,14 +4170,7 @@ export namespace main {
|
||||
grid: string;
|
||||
alt_m: number;
|
||||
rot_on: boolean;
|
||||
rot_type: string;
|
||||
rot_pst_port: number;
|
||||
rot_transport: string;
|
||||
rot_host: string;
|
||||
rot_port: number;
|
||||
rot_com: string;
|
||||
rot_baud: number;
|
||||
rot_max_az: number;
|
||||
rot_id: string;
|
||||
rot_min_el: number;
|
||||
rot_step: number;
|
||||
rot_park: boolean;
|
||||
@@ -4163,14 +4188,7 @@ export namespace main {
|
||||
this.grid = source["grid"];
|
||||
this.alt_m = source["alt_m"];
|
||||
this.rot_on = source["rot_on"];
|
||||
this.rot_type = source["rot_type"];
|
||||
this.rot_pst_port = source["rot_pst_port"];
|
||||
this.rot_transport = source["rot_transport"];
|
||||
this.rot_host = source["rot_host"];
|
||||
this.rot_port = source["rot_port"];
|
||||
this.rot_com = source["rot_com"];
|
||||
this.rot_baud = source["rot_baud"];
|
||||
this.rot_max_az = source["rot_max_az"];
|
||||
this.rot_id = source["rot_id"];
|
||||
this.rot_min_el = source["rot_min_el"];
|
||||
this.rot_step = source["rot_step"];
|
||||
this.rot_park = source["rot_park"];
|
||||
@@ -4362,6 +4380,24 @@ export namespace main {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class SatelliteRotorChoice {
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
has_el: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SatelliteRotorChoice(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.key = source["key"];
|
||||
this.name = source["name"];
|
||||
this.type = source["type"];
|
||||
this.has_el = source["has_el"];
|
||||
}
|
||||
}
|
||||
export class ScpStatus {
|
||||
enabled: boolean;
|
||||
count: number;
|
||||
|
||||
Reference in New Issue
Block a user