fix(sat,decodes): the left column holds position too, and drift stops lying
Two things. The sky plot and the position share the left column now, and the button that opens it says so the way the right one does — it was a radar icon, left over from when it toggled a plot inside the readout column, and the gesture is the same one on both sides. The plot and the numbers are the same answer at two precisions: azimuth and elevation drawn, then written out to the digit. Having them at opposite ends of the window meant reading a bearing off one side and finding it on the other. And the band-drift warning, reported by W4TE. It compared the decoder's announced band against RigState.Band, which is the TRANSMIT band — so with slice A on 20 m running its own WSJT-X, slice B on 40 m, and transmit focus on B, the 20 m decoder was told the rig was on 40 m while the slice it listens to had been on 20 m throughout. The panel's own comment had accepted this as a line that setup could read past; it is worse than that, because the warning names a band and asserts something false about the radio. RigState now carries RxBands: every band the rig has a receiver on. One entry on a single-VFO rig, one per slice on a Flex, the transmit band always included so it cannot come back empty while the rig is on a frequency. The warning fires only when the decoder announces a band NOTHING on the radio is on, which is what it was always for and what a lost CAT link actually looks like.
This commit is contained in:
@@ -6644,6 +6644,9 @@ export default function App() {
|
||||
// Only while CAT is actually connected: an empty band means "nothing to
|
||||
// compare with", never "the rig is on no band".
|
||||
rigBand={catState.connected ? (catState.band || '') : ''}
|
||||
// Every band the radio is listening on, so a second slice on a second
|
||||
// band does not read as a decoder that has lost CAT.
|
||||
rigBands={catState.connected ? (catState.rx_bands ?? []) : []}
|
||||
myCall={station.callsign}
|
||||
myGrid={station.my_grid}
|
||||
// A DOUBLE click answers the station: it hands the decode back to
|
||||
|
||||
@@ -102,6 +102,9 @@ interface Props {
|
||||
// The band the RIG is on, when CAT is connected. Only ever compared with what
|
||||
// the decoder announces — see the drift warning.
|
||||
rigBand?: string;
|
||||
// Every band the radio has a receiver on. A Flex running two slices has
|
||||
// two, and a decoder on either of them is not drifting.
|
||||
rigBands?: string[];
|
||||
onCall: (d: Decode) => void;
|
||||
// A single click: take the station without transmitting — fill the entry, and
|
||||
// point the panels at it. Absent, a click falls back to onCall.
|
||||
@@ -641,7 +644,7 @@ function buildPeriods(filtered: Decode[], txMsgs: TxMsg[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, rigBand, onCall, onSelect, myCall, myGrid, onClear, onHalt, autoCallOn, onToggleAutoCall, autoCall, autoCallOnly, onSetAutoCallOnly, watchlist }: Props) {
|
||||
export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, rigBand, rigBands, onCall, onSelect, myCall, myGrid, onClear, onHalt, autoCallOn, onToggleAutoCall, autoCall, autoCallOnly, onSetAutoCallOnly, watchlist }: Props) {
|
||||
const { t } = useI18n();
|
||||
// Column widths, dragged in the header and shared by every row. Persisted
|
||||
// through writeUiPref (not raw localStorage) so the layout travels with data/
|
||||
@@ -778,9 +781,19 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, r
|
||||
// Said, not decided. Using the rig's band instead would be wrong for anyone
|
||||
// decoding a second receiver on another band, and a warning costs that setup
|
||||
// nothing but a line it can read past.
|
||||
//
|
||||
// Compared against every band the radio is RECEIVING on, not the transmit
|
||||
// band. Two slices on two bands with a decoder on each is a normal setup,
|
||||
// and it made this warning lie: with slice A on 20 m, slice B on 40 m and
|
||||
// transmit focus on B, the 20 m decoder was told the rig was on 40 m while
|
||||
// the slice it listens to was on 20 m all along. The warning is for a
|
||||
// decoder announcing a band NOTHING on the radio is on, which is what a
|
||||
// lost CAT link actually looks like.
|
||||
const decoderBand = decodes.length ? (decodes[decodes.length - 1].band ?? '') : '';
|
||||
const bandDrift = !!rigBand && !!decoderBand
|
||||
&& rigBand.toLowerCase() !== decoderBand.toLowerCase();
|
||||
const onAir = (rigBands && rigBands.length ? rigBands : (rigBand ? [rigBand] : []))
|
||||
.map((b) => b.toLowerCase());
|
||||
const bandDrift = onAir.length > 0 && !!decoderBand
|
||||
&& !onAir.includes(decoderBand.toLowerCase());
|
||||
const driftInstance = decodes.length ? (decodes[decodes.length - 1].instance ?? '') : '';
|
||||
|
||||
const liveMode = decodes.length ? decodes[decodes.length - 1].mode : txState?.mode;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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, Compass,
|
||||
import { Satellite as SatIcon, Radio, ArrowUp, ArrowDown, PanelRightClose, PanelRightOpen,
|
||||
PanelLeftClose, PanelLeftOpen, Radar, Compass,
|
||||
ChevronDown, Clock, Crosshair, SlidersHorizontal, ListOrdered } from 'lucide-react';
|
||||
import {
|
||||
GetSatelliteBirds, GetSatellitePositions, GetSatellitePasses, GetSatelliteTuning,
|
||||
@@ -857,13 +858,18 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
>
|
||||
{Object.entries(BASEMAPS).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
|
||||
</select>
|
||||
{/* The left column, with the same kind of control as the right one.
|
||||
|
||||
It was a Radar icon, from when this toggled a plot inside the
|
||||
readout column. It now opens and shuts a column of the window, so
|
||||
it says so the way the other one does — the two are the same
|
||||
gesture and an operator should not have to learn them twice. */}
|
||||
<Button
|
||||
variant="ghost" size="sm"
|
||||
className={cn('h-7 px-1.5', skyShown && 'text-success')}
|
||||
variant="ghost" size="sm" className="h-7 px-1.5"
|
||||
onClick={() => setSkyShown((v) => !v)}
|
||||
title={skyShown ? t('sat.hideSky') : t('sat.showSky')}
|
||||
>
|
||||
<Radar className="size-3.5" />
|
||||
{skyShown ? <PanelLeftClose className="size-3.5" /> : <PanelLeftOpen className="size-3.5" />}
|
||||
</Button>
|
||||
{/* Put the whole window on the map. On a laptop the readout takes a
|
||||
third of the screen, and there are moments — watching a footprint
|
||||
@@ -880,17 +886,23 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
{err && <div className="px-2 text-[11px] text-danger shrink-0">{err}</div>}
|
||||
|
||||
<div className="flex gap-1 flex-1 min-h-0">
|
||||
{/* The sky, seen from underneath it: the centre is straight up, the
|
||||
rim is the horizon, north is at the top. One glance says whether
|
||||
the pass comes over the roof or along the treeline.
|
||||
{/* WHERE IT IS: the sky plot and the position, in a column of their
|
||||
own to the left of the map.
|
||||
|
||||
To the LEFT of the map, in a column of its own. It was stacked in
|
||||
the readout column, where a plot that wants to be square competed
|
||||
for width with the numbers and pushed the pass table off the
|
||||
bottom of the screen — while the margin on this side of the map
|
||||
sat empty the whole time. */}
|
||||
The plot is the sky seen from underneath — the centre is
|
||||
straight up, the rim is the horizon, north is at the top — and
|
||||
one glance says whether the pass comes over the roof or along
|
||||
the treeline. The numbers below it are the same answer to the
|
||||
digit: azimuth, elevation, distance, height. They belong
|
||||
together, and having them apart meant reading a bearing off one
|
||||
side of the window and finding it on the other.
|
||||
|
||||
Both were in the readout column, where a plot that wants to be
|
||||
square competed for width with everything else and pushed the
|
||||
pass table off the bottom of the screen — while the margin on
|
||||
this side of the map sat empty the whole time. */}
|
||||
{skyShown && (
|
||||
<div className="shrink-0 flex flex-col gap-1 min-h-0" style={{ width: skyW }}>
|
||||
<div className="shrink-0 flex flex-col gap-1 min-h-0 overflow-y-auto" style={{ width: skyW }}>
|
||||
<div className="rounded-lg border border-border bg-card p-2">
|
||||
<SkyPlot
|
||||
track={sky}
|
||||
@@ -900,6 +912,68 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
visible={!!tuning?.visible}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Where it is, right now. Its elevation goes in the heading: above
|
||||
or below the horizon is the one thing worth knowing with the
|
||||
block shut. */}
|
||||
<Section
|
||||
title={t('sat.secWhere')}
|
||||
icon={Crosshair}
|
||||
open={secOpen.where}
|
||||
onToggle={() => toggleSec('where')}
|
||||
right={tuning && !secOpen.where ? (
|
||||
<Pill tone={tuning.visible ? 'success' : 'muted'}>
|
||||
{fmtDeg(tuning.el)} {tuning.visible ? t('sat.up') : t('sat.below')}
|
||||
</Pill>
|
||||
) : undefined}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Readout label={t('sat.az')} value={tuning ? fmtDeg(tuning.az) : '—'}
|
||||
sub={tuning ? compass(tuning.az) : ''} />
|
||||
<Readout label={t('sat.el')} value={tuning ? fmtDeg(tuning.el) : '—'}
|
||||
colour={tuning?.visible ? 'var(--success)' : 'var(--muted-foreground)'}
|
||||
sub={tuning ? (tuning.visible ? t('sat.up') : t('sat.below')) : ''} />
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-3 gap-1 text-[11px] tabular-nums">
|
||||
<PassBit label={t('sat.range')} value={tuning?.range_km ? fmtKm(tuning.range_km) : '—'} />
|
||||
<PassBit label={t('sat.altitude')} value={tuning?.alt_km ? fmtKm(tuning.alt_km) : '—'} />
|
||||
<PassBit label={t('sat.footprint')} value={tuning?.footprint_km ? fmtKm(tuning.footprint_km) : '—'} />
|
||||
</div>
|
||||
|
||||
{/* Approaching or receding, which is the sign of the whole Doppler
|
||||
correction and the one number that explains why the frequencies
|
||||
are moving the way they are. */}
|
||||
{!!tuning && !bird?.geostationary && (
|
||||
<div className="mt-1.5 flex items-center gap-1.5 text-[11px] text-muted-foreground tabular-nums">
|
||||
{tuning.range_rate < 0
|
||||
? <ArrowUp className="size-3 text-success" />
|
||||
: <ArrowDown className="size-3 text-warning" />}
|
||||
<span>{tuning.range_rate < 0 ? t('sat.approaching') : t('sat.receding')}</span>
|
||||
<span>{Math.abs(tuning.range_rate).toFixed(2)} km/s</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Where the antenna is, beside where the satellite is. The two
|
||||
differing is a rotator still slewing; the two differing for a
|
||||
long time is a rotator that is stuck, and that is worth being
|
||||
able to see without walking outside. */}
|
||||
{tracking?.rot_on && (
|
||||
<div className="mt-1.5 pt-1.5 border-t border-border/60 flex items-baseline gap-2 text-[11px] tabular-nums">
|
||||
<span className="text-muted-foreground uppercase tracking-wide text-[10px]">{t('sat.antenna')}</span>
|
||||
{/* No elevation when none is being driven: an undriven zero
|
||||
draws an antenna lying on the horizon, which is a bearing
|
||||
and not the absence of one. */}
|
||||
<span className="font-medium">
|
||||
{tracking.rot_az_only
|
||||
? fmtDeg(tracking.rot_az)
|
||||
: `${fmtDeg(tracking.rot_az)} / ${fmtDeg(tracking.rot_el)}`}
|
||||
</span>
|
||||
{tracking.rot_az_only && <span className="text-muted-foreground">{t('sat.rotAzOnly')}</span>}
|
||||
{!tracking.rot_live && <span className="text-muted-foreground">{t('sat.rotCommanded')}</span>}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
{skyShown && (
|
||||
@@ -1004,68 +1078,6 @@ export function SatellitePanel({ myGrid }: { myGrid: string }) {
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* Where it is, right now. Its elevation goes in the heading: above
|
||||
or below the horizon is the one thing worth knowing with the
|
||||
block shut. */}
|
||||
<Section
|
||||
title={t('sat.secWhere')}
|
||||
icon={Crosshair}
|
||||
open={secOpen.where}
|
||||
onToggle={() => toggleSec('where')}
|
||||
right={tuning && !secOpen.where ? (
|
||||
<Pill tone={tuning.visible ? 'success' : 'muted'}>
|
||||
{fmtDeg(tuning.el)} {tuning.visible ? t('sat.up') : t('sat.below')}
|
||||
</Pill>
|
||||
) : undefined}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Readout label={t('sat.az')} value={tuning ? fmtDeg(tuning.az) : '—'}
|
||||
sub={tuning ? compass(tuning.az) : ''} />
|
||||
<Readout label={t('sat.el')} value={tuning ? fmtDeg(tuning.el) : '—'}
|
||||
colour={tuning?.visible ? 'var(--success)' : 'var(--muted-foreground)'}
|
||||
sub={tuning ? (tuning.visible ? t('sat.up') : t('sat.below')) : ''} />
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-3 gap-1 text-[11px] tabular-nums">
|
||||
<PassBit label={t('sat.range')} value={tuning?.range_km ? fmtKm(tuning.range_km) : '—'} />
|
||||
<PassBit label={t('sat.altitude')} value={tuning?.alt_km ? fmtKm(tuning.alt_km) : '—'} />
|
||||
<PassBit label={t('sat.footprint')} value={tuning?.footprint_km ? fmtKm(tuning.footprint_km) : '—'} />
|
||||
</div>
|
||||
|
||||
{/* Approaching or receding, which is the sign of the whole Doppler
|
||||
correction and the one number that explains why the frequencies
|
||||
are moving the way they are. */}
|
||||
{!!tuning && !bird?.geostationary && (
|
||||
<div className="mt-1.5 flex items-center gap-1.5 text-[11px] text-muted-foreground tabular-nums">
|
||||
{tuning.range_rate < 0
|
||||
? <ArrowUp className="size-3 text-success" />
|
||||
: <ArrowDown className="size-3 text-warning" />}
|
||||
<span>{tuning.range_rate < 0 ? t('sat.approaching') : t('sat.receding')}</span>
|
||||
<span>{Math.abs(tuning.range_rate).toFixed(2)} km/s</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Where the antenna is, beside where the satellite is. The two
|
||||
differing is a rotator still slewing; the two differing for a
|
||||
long time is a rotator that is stuck, and that is worth being
|
||||
able to see without walking outside. */}
|
||||
{tracking?.rot_on && (
|
||||
<div className="mt-1.5 pt-1.5 border-t border-border/60 flex items-baseline gap-2 text-[11px] tabular-nums">
|
||||
<span className="text-muted-foreground uppercase tracking-wide text-[10px]">{t('sat.antenna')}</span>
|
||||
{/* No elevation when none is being driven: an undriven zero
|
||||
draws an antenna lying on the horizon, which is a bearing
|
||||
and not the absence of one. */}
|
||||
<span className="font-medium">
|
||||
{tracking.rot_az_only
|
||||
? fmtDeg(tracking.rot_az)
|
||||
: `${fmtDeg(tracking.rot_az)} / ${fmtDeg(tracking.rot_el)}`}
|
||||
</span>
|
||||
{tracking.rot_az_only && <span className="text-muted-foreground">{t('sat.rotAzOnly')}</span>}
|
||||
{!tracking.rot_live && <span className="text-muted-foreground">{t('sat.rotCommanded')}</span>}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* What to tune. */}
|
||||
<Section
|
||||
title={t('sat.secTune')}
|
||||
|
||||
@@ -625,7 +625,7 @@ const en: Dict = {
|
||||
'sat.sideWidthTip': 'Drag to resize the readout. Double-click to reset it.',
|
||||
'sat.thSat': 'Satellite', 'sat.thAos': 'Rise', 'sat.thLos': 'Set', 'sat.thMaxEl': 'Max', 'sat.thIn': 'In',
|
||||
'sat.skyPlot': 'The pass across the sky — centre is straight up, the rim is the horizon, north is at the top',
|
||||
'sat.showSky': 'Show the sky plot', 'sat.hideSky': 'Hide the sky plot',
|
||||
'sat.showSky': 'Show the sky and position', 'sat.hideSky': 'Hide the sky and position',
|
||||
'sec.satellite': 'Satellites',
|
||||
'satset.hint': 'Where the antenna is, and the machine that points it. The satellites you follow and the frequency plan are in the Satellites tab.',
|
||||
'satset.gridPlaceholder': 'e.g. JN18cx',
|
||||
@@ -1251,7 +1251,7 @@ const fr: Dict = {
|
||||
'sat.sideWidthTip': 'Glisser pour redimensionner le panneau. Double-clic pour le remettre par défaut.',
|
||||
'sat.thSat': 'Satellite', 'sat.thAos': 'Lever', 'sat.thLos': 'Coucher', 'sat.thMaxEl': 'Max', 'sat.thIn': 'Dans',
|
||||
'sat.skyPlot': 'Le passage dans le ciel — le centre est à la verticale, le bord est l’horizon, le nord en haut',
|
||||
'sat.showSky': 'Afficher la vue du ciel', 'sat.hideSky': 'Masquer la vue du ciel',
|
||||
'sat.showSky': 'Afficher le ciel et la position', 'sat.hideSky': 'Masquer le ciel et la position',
|
||||
'sec.satellite': 'Satellites',
|
||||
'satset.hint': 'Où se trouve l’antenne, et la machine qui la pointe. Les satellites suivis et le plan de fréquences sont dans l’onglet Satellites.',
|
||||
'satset.gridPlaceholder': 'ex. JN18cx',
|
||||
|
||||
@@ -1154,6 +1154,7 @@ export namespace cat {
|
||||
split?: boolean;
|
||||
mode?: string;
|
||||
band?: string;
|
||||
rx_bands?: string[];
|
||||
vfo?: string;
|
||||
error?: string;
|
||||
// Go type: time
|
||||
@@ -1175,6 +1176,7 @@ export namespace cat {
|
||||
this.split = source["split"];
|
||||
this.mode = source["mode"];
|
||||
this.band = source["band"];
|
||||
this.rx_bands = source["rx_bands"];
|
||||
this.vfo = source["vfo"];
|
||||
this.error = source["error"];
|
||||
this.updated_at = this.convertValues(source["updated_at"], null);
|
||||
|
||||
Reference in New Issue
Block a user