fix(sat): the antenna stops flickering, and the pass is readable from the header
THE FLICKER was a real bug and not a rotator problem. The rotator is asked where it is at most every three seconds — a controller query binds a socket and waits — but satTrackStep built a fresh status every second and carried over only Radio and Error. So the antenna readout was filled in on one tick in three and blank on the other two, which on screen is a rotator that keeps disconnecting. The status is now rebuilt FROM the previous one, so a field nobody wrote this tick keeps the value somebody wrote last tick. THE HEADER now carries the two frequencies and the antenna bearing, beside the button that started tracking. During a pass an operator watches the radio and the antenna, not a column on the far side of the window — and that column is the first thing they hide to get the map full width, which until now took the numbers with it. The compass spins while the antenna is still on its way: a mast takes tens of seconds to cross a pass, and "moving" against "stuck" is the whole reason to look at it, which a number alone cannot show. THE PRECISION drops from one hertz to a hundred. The Doppler moves about sixty hertz a second on 70 cm, so the last two digits changed on every tick and the display was a blur that could not be read and did not need to be. The radio still gets the whole figure — the correction is computed and sent to the hertz — this is only how much of it is worth putting in front of somebody. The shift beside it says "+9.7 kHz" rather than "+9741 Hz", which is how it is read aloud. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { Satellite as SatIcon, Radio, ArrowUp, ArrowDown, PanelRightClose, PanelRightOpen, Radar } from 'lucide-react';
|
||||
import { Satellite as SatIcon, Radio, ArrowUp, ArrowDown, PanelRightClose, PanelRightOpen, Radar, Compass } from 'lucide-react';
|
||||
import {
|
||||
GetSatelliteBirds, GetSatellitePositions, GetSatellitePasses, GetSatelliteTuning,
|
||||
GetSatelliteGroundTrack, GetSatelliteTLEInfo, GetSatelliteNextPass, GetSatelliteSkyTrack,
|
||||
@@ -74,11 +74,28 @@ const SIDE_SHOWN_KEY = 'opslog.satSideShown';
|
||||
const SIDE_W_DEFAULT = 336, SIDE_W_MIN = 240, SIDE_W_MAX = 720;
|
||||
const SKY_SHOWN_KEY = 'opslog.satSkyShown';
|
||||
|
||||
// Four decimals — a hundred hertz, which is what a linear transponder is
|
||||
// actually tuned to.
|
||||
//
|
||||
// It used to be six, and the last two digits changed every tick: the Doppler
|
||||
// moves about sixty hertz a second on 70 cm, so the display was a blur of
|
||||
// numbers nobody could read and nobody needed. The RADIO still gets the whole
|
||||
// figure — the correction is computed and sent to the hertz — this is only how
|
||||
// much of it is worth putting in front of an operator. The shift beside it, in
|
||||
// kilohertz, is where the fine movement shows.
|
||||
const fmtHz = (hz: number) => {
|
||||
if (!hz) return '—';
|
||||
// Six decimals: a linear transponder is tuned to the hundred hertz, and the
|
||||
// Doppler correction moves the last three digits every second.
|
||||
return (hz / 1e6).toFixed(6).replace(/(\d)(?=(\d{3})+\.)/g, '$1 ');
|
||||
return (hz / 1e6).toFixed(4).replace(/(\d)(?=(\d{3})+\.)/g, '$1 ');
|
||||
};
|
||||
|
||||
// The Doppler shift, as an operator would say it: hertz while it is small
|
||||
// enough to say in hertz, kilohertz once it is not. "+9741 Hz" is four digits
|
||||
// of precision on a number that is only ever read as "about ten kilohertz".
|
||||
const fmtShift = (hz: number) => {
|
||||
const sign = hz > 0 ? '+' : '−';
|
||||
const a = Math.abs(hz);
|
||||
if (a < 1000) return `${sign}${Math.round(a)} Hz`;
|
||||
return `${sign}${(a / 1000).toFixed(1)} kHz`;
|
||||
};
|
||||
const fmtDeg = (d: number) => `${d.toFixed(1)}°`;
|
||||
const fmtKm = (km: number) => `${Math.round(km).toLocaleString()} km`;
|
||||
@@ -619,6 +636,14 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
|
||||
// The pass, as a countdown and a bar. Both derived here from two timestamps,
|
||||
// so they move every second without asking Go anything.
|
||||
// Is the antenna still on its way? The rotator is asked where it is every
|
||||
// three seconds and a mast takes tens of seconds to cross a pass, so a
|
||||
// difference between where it is and where the satellite is means it is
|
||||
// moving — which is exactly what a number alone cannot show, and the
|
||||
// difference between "on its way" and "stuck" is the whole reason to look.
|
||||
const antennaMoving = !!tracking?.rot_on && !!tracking.rot_live &&
|
||||
Math.abs(((tracking.az - tracking.rot_az + 540) % 360) - 180) > 3;
|
||||
|
||||
const aosMs = pass?.has_pass ? Date.parse(pass.aos) : 0;
|
||||
const losMs = pass?.has_pass ? Date.parse(pass.los) : 0;
|
||||
const inPass = !!pass?.has_pass && now >= aosMs && now < losMs;
|
||||
@@ -687,6 +712,40 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
{tracking?.on && tracking.radio === 'downlink-only' && (
|
||||
<span className="text-[11px] text-warning">{t('sat.downlinkOnly')}</span>
|
||||
)}
|
||||
|
||||
{/* What the station is actually doing, beside the button that started
|
||||
it. During a pass an operator watches the radio and the antenna, not
|
||||
a column on the far side of the window — and that column is the
|
||||
first thing they hide to get the map full width. */}
|
||||
{tracking?.on && (
|
||||
<div className="flex items-center gap-2.5 rounded-md border border-border bg-card/60 px-2 py-0.5 text-xs tabular-nums">
|
||||
<span className="flex items-center gap-1" title={t('sat.down')}>
|
||||
<ArrowDown className="size-3 text-muted-foreground" />
|
||||
<span className="font-medium">{fmtHz(tracking.down_hz)}</span>
|
||||
</span>
|
||||
{!!tracking.up_hz && (
|
||||
<span className="flex items-center gap-1" title={t('sat.up')}>
|
||||
<ArrowUp className="size-3 text-muted-foreground" />
|
||||
<span className="font-medium">{fmtHz(tracking.up_hz)}</span>
|
||||
</span>
|
||||
)}
|
||||
{tracking.rot_on && (
|
||||
<span className={cn('flex items-center gap-1 border-l border-border pl-2.5',
|
||||
antennaMoving && 'text-caution')} title={t('sat.antenna')}>
|
||||
{/* The needle spins while the antenna is slewing. A rotator
|
||||
takes tens of seconds to cross a pass, and the difference
|
||||
between "on its way" and "stuck" is the whole reason to look
|
||||
at it — a number alone cannot show movement. */}
|
||||
<Compass className={cn('size-3', antennaMoving ? 'animate-spin' : 'text-muted-foreground')}
|
||||
style={antennaMoving ? { animationDuration: '3s' } : undefined} />
|
||||
<span className="font-medium">
|
||||
{Math.round(tracking.rot_az)}°
|
||||
{!tracking.rot_az_only && ` / ${Math.round(tracking.rot_el)}°`}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{/* Elements are maintenance, so only their AGE is here — and only when
|
||||
it has become a reason the panel might be wrong. */}
|
||||
@@ -1031,9 +1090,7 @@ function FreqRow({ label, hz, nominal }: { label: string; hz: number; nominal: n
|
||||
<span className="w-10 text-[10px] uppercase tracking-wide text-muted-foreground">{label}</span>
|
||||
<span className="text-base font-semibold tabular-nums">{fmtHz(hz)}</span>
|
||||
{!!shift && (
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground">
|
||||
{shift > 0 ? '+' : '−'}{Math.abs(Math.round(shift))} Hz
|
||||
</span>
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground">{fmtShift(shift)}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user