feat(sat): Doppler tracking on the radio
The hard part of satellite tuning is not the arithmetic, it is deciding who owns the dial. A tracker that forces both frequencies fights the operator every time they turn the knob to follow a station across a linear transponder; one that never touches the receiver leaves them chasing a signal that slides nine kilohertz across a 70 cm pass. So the operator owns the receiver and the tracker follows them. Every second it asks the radio where the receiver actually is. Where it put it, nothing has changed. Further than a dial-turn's tolerance, and the operator has chosen a station: what they landed on is converted back into a nominal frequency, and the transmitter is derived from that. Which is the division of labour on a linear bird — the operator listens, the radio does the sums. Three ways to reach the radio, because a satellite pair is a shape of operating rather than a manufacturer's feature. An IC-9700 or IC-9100 is asked for its OWN satellite mode: it pairs main and sub, gives full duplex, and keeps the dials linked the way its designers meant, which is always better than an imitation built out of split. A Flex gets two slices, A the downlink and B the uplink, created when missing, because "slice B does not exist" is not something to make an operator fix at the start of a ten-minute pass. Everything else gets the downlink, and is told so — half the job announced beats half the job hidden. What goes in the log is the NOMINAL pair. Two stations working each other through a transponder read different numbers off their dials at the same instant; the only figure they can both agree on is the transponder's own. FREQ is the uplink and FREQ_RX the downlink — the one place a satellite QSO differs from every other kind, and the reason FREQ alone cannot describe one.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { RefreshCw, Star, Satellite as SatIcon, ClipboardPaste } from 'lucide-react';
|
||||
import { RefreshCw, Star, Satellite as SatIcon, ClipboardPaste, Radio } from 'lucide-react';
|
||||
import {
|
||||
GetSatelliteBirds, GetSatellitePositions, GetSatellitePasses, GetSatelliteTuning,
|
||||
GetSatelliteGroundTrack, GetSatelliteTLEInfo, RefreshSatelliteTLE, AddSatelliteElements,
|
||||
GetSatSettings, SaveSatSettings,
|
||||
StartSatelliteTracking, StopSatelliteTracking, GetSatelliteTracking,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -45,6 +46,13 @@ type Tuning = {
|
||||
ctcss: number; inverting: boolean;
|
||||
az: number; el: number; range_km: number; range_rate: number; visible: boolean;
|
||||
};
|
||||
type Track = {
|
||||
on: boolean; name: string; transponder: string; mode: string;
|
||||
nominal_down: number; nominal_up: number; down_hz: number; up_hz: number;
|
||||
az: number; el: number; visible: boolean;
|
||||
radio: string; // "sat" | "downlink-only" | ""
|
||||
error: string;
|
||||
};
|
||||
|
||||
const MAP_VIEW_SAT = 'opslog.satMapView';
|
||||
|
||||
@@ -76,6 +84,7 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
const [passes, setPasses] = useState<Pass[]>([]);
|
||||
const [tuning, setTuning] = useState<Tuning | null>(null);
|
||||
const [tle, setTle] = useState<{ count: number; age_h: number; stale: boolean; custom: number } | null>(null);
|
||||
const [tracking, setTracking] = useState<Track | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [err, setErr] = useState('');
|
||||
const [favs, setFavs] = useState<string[]>([]);
|
||||
@@ -159,6 +168,32 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
return () => window.clearInterval(id);
|
||||
}, [loadPasses]);
|
||||
|
||||
// The tracker's own state, pushed as it moves. Polled as well, at a lazy
|
||||
// rate, so a panel opened while tracking is already running is not blank
|
||||
// until the next tick.
|
||||
useEffect(() => {
|
||||
const read = async () => {
|
||||
try { setTracking((await GetSatelliteTracking()) as any); } catch { /* not tracking */ }
|
||||
};
|
||||
read();
|
||||
const off = EventsOn('sat:track', (s: any) => setTracking(s ?? null));
|
||||
const id = window.setInterval(read, 10_000);
|
||||
return () => { off(); window.clearInterval(id); };
|
||||
}, []);
|
||||
|
||||
const toggleTracking = async () => {
|
||||
setErr('');
|
||||
try {
|
||||
if (tracking?.on) {
|
||||
await StopSatelliteTracking();
|
||||
setTracking(null);
|
||||
} else {
|
||||
await StartSatelliteTracking(sel, tpIdx);
|
||||
setTracking((await GetSatelliteTracking()) as any);
|
||||
}
|
||||
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
||||
};
|
||||
|
||||
const refreshTle = async () => {
|
||||
setBusy(true); setErr('');
|
||||
try {
|
||||
@@ -336,6 +371,25 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
<Star className={cn('size-3.5', favs.includes(bird.name) && 'fill-warning text-warning')} />
|
||||
</Button>
|
||||
)}
|
||||
{/* Tracking is the one button on this panel that touches the radio, so
|
||||
it says which of the two things it is doing: holding both ends of
|
||||
the pass, or only the receiver on a rig with one. */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={tracking?.on ? 'default' : 'outline'}
|
||||
className={cn('h-7 px-2 gap-1.5', tracking?.on && 'bg-success text-background hover:bg-success/90')}
|
||||
onClick={toggleTracking}
|
||||
disabled={!bird?.transponders?.length}
|
||||
title={tracking?.on
|
||||
? (tracking.radio === 'sat' ? t('sat.trackingFull') : t('sat.trackingDown'))
|
||||
: t('sat.trackTip')}
|
||||
>
|
||||
<Radio className="size-3.5" />
|
||||
{tracking?.on ? t('sat.tracking') : t('sat.track')}
|
||||
</Button>
|
||||
{tracking?.on && tracking.radio === 'downlink-only' && (
|
||||
<span className="text-[11px] text-warning">{t('sat.downlinkOnly')}</span>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
<span className={cn('text-[11px] tabular-nums', tle?.stale ? 'text-warning' : 'text-muted-foreground')}>
|
||||
{tleLabel}{tle?.custom ? ` · ${t('sat.tleCustom', { n: tle.custom })}` : ''}
|
||||
|
||||
Reference in New Issue
Block a user