feat(sat): follow the azimuth only

A satellite tracker that insists on an elevation motor is a tracker switched off
for nearly everybody. A pass at the edge of the footprint — which is most of
them — never climbs above ten or fifteen degrees for its whole length, and a
yagi's beamwidth swallows that: the bearing alone is enough, and it is how most
stations that work satellites are actually built. The same switch rescues an
az/el station whose elevation motor has failed.

So it is an option, not a silent fallback, because it does cost something: a
bird straight overhead is a moving azimuth and a bearing that means nothing, and
whether to accept that is the operator's call. With it on, any rotor in the list
can be chosen — the PstRotator, the Rotator Genius, the ARCO, the tower already
turned for HF.

That works because the per-backend command dispatch moved out of the three
RotatorGoTo/Stop/Heading methods into linkGoTo/linkStop/linkHeading, so the
satellite tracker drives any of the seven backends through the same code the
compass uses instead of a second implementation of each. GetRotatorHeading loses
sixty lines of near-duplicate switch in the process, and a rotor with no
elevation axis now says so (HasElevation) rather than reporting a zero that
looks like a real bearing.

One trap, with a test on it: the step check compared both axes, so with the
elevation never commanded its difference stayed above the step for the whole
pass and every tick ordered the antenna to the bearing it was already on. A mast
has a finite number of turns in it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-09 11:35:23 +02:00
co-authored by Claude Opus 5
parent 8f17416eca
commit b15055ba4a
10 changed files with 337 additions and 160 deletions
+22 -7
View File
@@ -1931,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_id: '', 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_az_only: false, 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),
@@ -4701,13 +4701,15 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
<SelectTrigger className="h-9 flex-1"><SelectValue placeholder={t('satset.rotPickNone')} /></SelectTrigger>
<SelectContent>
{satRotors.length === 0 && <SelectItem value="_" disabled>{t('satset.rotNoneConfigured')}</SelectItem>}
{/* The azimuth-only rotors are LISTED, and disabled. An
{/* The azimuth-only rotors are always LISTED. Without
the switch below they are greyed and say why 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. */}
concludes OpsLog cannot find it, where "azimuth
only" beside it teaches the real thing. With the
switch on, every rotor is fair game. */}
{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 key={r.key} value={r.key} disabled={!r.has_el && !satCfg.rot_az_only}>
{(r.name || r.type) + (r.has_el ? '' : `${t('satset.rotAzOnlyTag')}`)}
</SelectItem>
))}
</SelectContent>
@@ -4718,11 +4720,24 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
</Button>
</div>
<p className="text-xs text-muted-foreground">{t('satset.rotPickHint')}</p>
{satRotors.length > 0 && !satRotors.some((r: any) => r.has_el) && (
{!satCfg.rot_az_only && satRotors.length > 0 && !satRotors.some((r: any) => r.has_el) && (
<p className="text-xs text-[var(--warning)]">{t('satset.rotNoElAtAll')}</p>
)}
</div>
{/* Azimuth only. Not a fallback it is how most stations that
work satellites are actually built, and refusing to track
without an elevation motor turned the feature off for every
operator with a tower and no az/el mast. */}
<label className="flex items-start gap-2 text-sm cursor-pointer">
<Checkbox className="mt-0.5" checked={!!satCfg.rot_az_only}
onCheckedChange={(c) => set('rot_az_only', !!c)} />
<span>
{t('satset.rotAzOnly')}
<span className="block text-xs text-muted-foreground">{t('satset.rotAzOnlyHint')}</span>
</span>
</label>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1">
<Label>{t('satset.rotMinEl')}</Label>