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:
+6
-2
@@ -6,14 +6,18 @@
|
||||
"Every block of the readout column now has a heading and shuts on its own: the pass, the position, what to tune, the pass list. Each one remembers whether it is open, because an operator working FM birds never looks at the linear passband and one watching a schedule never looks at the range rate. A shut block keeps the one thing worth knowing in its heading — the countdown, the elevation, the mode.",
|
||||
"The frequency to tune to is the CENTRE of the passband, not the Doppler-corrected figure. The corrected one moves every second, differs for every operator, and is not what the frequency plan or anybody on the air calls the satellite's frequency; it belongs beside Tracking, where the radio is, and that is where it now lives. The correction stays here as the offset that explains the difference between the two.",
|
||||
"More colour where it decides something: the band of each frequency, the Doppler offset, an inverting transponder, the passband width and the peak elevation of a pass are badges in the app's own status colours rather than rows of grey words.",
|
||||
"The ground track is visible on every basemap. It was drawn in a CSS variable, which is not a colour a canvas can use, so the browser kept whatever it had and the track came out a pale near-white that disappeared over the imagery and the deserts alike. It is now a proper colour, drawn over a dark casing the way a road is on a map — one line cannot hold up over both a pale sea and a dark continent, but a line with an outline can."
|
||||
"The ground track is visible on every basemap. It was drawn in a CSS variable, which is not a colour a canvas can use, so the browser kept whatever it had and the track came out a pale near-white that disappeared over the imagery and the deserts alike. It is now a proper colour, drawn over a dark casing the way a road is on a map — one line cannot hold up over both a pale sea and a dark continent, but a line with an outline can.",
|
||||
"The sky plot and the position now share the left column, and it opens and shuts with the same kind of control as the readout column on the right — it used to be a radar icon, from when it toggled a plot inside that column. The plot and the numbers are the same answer at different precisions, azimuth and elevation drawn and then written out, and having them on opposite sides of the window meant reading a bearing off one and finding it on the other.",
|
||||
"The decode panel's band-drift warning stops firing on a second slice. It compared the decoder's band against the radio's TRANSMIT band, so a station with slice A on 20 m, slice B on 40 m and transmit focus on B was told its 20 m decoder disagreed with a radio that had been on 20 m all along — on the slice that decoder listens to. The comparison is now against every band the radio has a receiver on, which is what the warning was always about: a decoder announcing a band nothing on the radio is on, the way a lost CAT link looks."
|
||||
],
|
||||
"fr": [
|
||||
"L’onglet Satellites est réagencé. Le tracé du ciel passe dans sa propre colonne à GAUCHE de la carte, où un graphique qui veut être carré ne dispute plus la largeur aux chiffres — il occupait la colonne de droite et repoussait le tableau des passes hors de l’écran, alors que la marge de l’autre côté de la carte restait vide. Sa largeur se règle à la souris, et un double-clic sur la poignée la réinitialise.",
|
||||
"Chaque bloc de la colonne de droite a désormais un titre et se replie indépendamment : la passe, la position, ce qu’il faut accorder, la liste des passes. Chacun retient s’il est ouvert, car qui travaille les satellites FM ne regarde jamais la bande passante linéaire, et qui surveille un calendrier ne regarde jamais la vitesse radiale. Un bloc replié conserve dans son titre la seule chose utile : le décompte, l’élévation, le mode.",
|
||||
"La fréquence à accorder est le CENTRE de la bande passante, pas la valeur corrigée du Doppler. Celle-ci bouge chaque seconde, diffère pour chaque opérateur, et n’est pas ce que le plan de fréquences ou n’importe qui sur l’air appelle la fréquence du satellite ; elle a sa place à côté de Tracking, là où se trouve la radio, et c’est là qu’elle est désormais. La correction reste ici, comme l’écart qui explique la différence entre les deux.",
|
||||
"Plus de couleur là où elle décide de quelque chose : la bande de chaque fréquence, l’écart Doppler, un transpondeur inverseur, la largeur de bande passante et le pic d’élévation d’une passe sont des pastilles aux couleurs d’état de l’application, plutôt que des rangées de mots gris.",
|
||||
"Le tracé au sol est visible sur tous les fonds de carte. Il était dessiné avec une variable CSS, qui n’est pas une couleur exploitable par un canvas : le navigateur conservait ce qu’il avait et le tracé sortait dans un blanc pâle qui disparaissait aussi bien sur l’imagerie que sur les déserts. C’est maintenant une vraie couleur, tracée sur un liseré sombre comme on dessine une route sur une carte — une seule ligne ne peut pas tenir à la fois sur une mer claire et un continent sombre, une ligne avec un contour si."
|
||||
"Le tracé au sol est visible sur tous les fonds de carte. Il était dessiné avec une variable CSS, qui n’est pas une couleur exploitable par un canvas : le navigateur conservait ce qu’il avait et le tracé sortait dans un blanc pâle qui disparaissait aussi bien sur l’imagerie que sur les déserts. C’est maintenant une vraie couleur, tracée sur un liseré sombre comme on dessine une route sur une carte — une seule ligne ne peut pas tenir à la fois sur une mer claire et un continent sombre, une ligne avec un contour si.",
|
||||
"Le tracé du ciel et la position partagent désormais la colonne de gauche, qui s’ouvre et se ferme avec le même type de bouton que le panneau de droite — c’était une icône radar, héritée de l’époque où elle basculait un graphique à l’intérieur de ce panneau. Le tracé et les chiffres sont la même réponse à deux précisions près, l’azimut et l’élévation dessinés puis écrits, et les avoir aux deux extrémités de la fenêtre obligeait à lire un relèvement d’un côté pour le retrouver de l’autre.",
|
||||
"L’avertissement de dérive de bande du panneau de décodages ne se déclenche plus sur une seconde tranche. Il comparait la bande du décodeur à la bande d’ÉMISSION de la radio : une station avec la tranche A sur 20 m, la B sur 40 m et le focus d’émission sur B se voyait annoncer que son décodeur 20 m contredisait une radio qui était sur 20 m depuis le début — sur la tranche que ce décodeur écoute. La comparaison porte désormais sur toutes les bandes où la radio a un récepteur, ce qui a toujours été le sujet de cet avertissement : un décodeur qui annonce une bande sur laquelle rien de la radio ne se trouve, comme le fait une liaison CAT perdue."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -60,6 +60,16 @@ type RigState struct {
|
||||
Split bool `json:"split,omitempty"` // rig is in split mode
|
||||
Mode string `json:"mode,omitempty"` // ADIF mode (SSB/CW/DATA/AM/FM/RTTY)
|
||||
Band string `json:"band,omitempty"` // computed from FreqHz
|
||||
// RxBands is every band the radio currently has a receiver on, Band
|
||||
// included. One entry on a rig with one VFO; one per slice on a Flex.
|
||||
//
|
||||
// It exists because Band alone is the TRANSMIT band, and a station running
|
||||
// two slices on two bands with a decoder on each is not drifting when one
|
||||
// of them announces the band it is legitimately on. Reported for W4TE:
|
||||
// slice A on 20 m with its own WSJT-X, slice B on 40 m holding transmit
|
||||
// focus, and OpsLog telling him his 20 m decoder disagreed with a radio
|
||||
// that was on 20 m — on that slice.
|
||||
RxBands []string `json:"rx_bands,omitempty"`
|
||||
Vfo string `json:"vfo,omitempty"` // "A" | "B" | "AA" | "AB" | "BA" | "BB"
|
||||
Error string `json:"error,omitempty"` // last connect/poll error if any
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
@@ -1002,11 +1012,47 @@ func (m *Manager) run(b Backend, stop, done chan struct{}, cmds chan func(), pol
|
||||
if ns.FreqHz != 0 && ns.Band == "" {
|
||||
ns.Band = BandFromHz(ns.FreqHz)
|
||||
}
|
||||
ns.RxBands = m.rxBands(ns)
|
||||
m.update(ns)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rxBands lists the bands the radio has a receiver on.
|
||||
//
|
||||
// Asked of the backend rather than derived from the state, because only the
|
||||
// backend knows: a Flex reports its slices, and everything else has exactly
|
||||
// one receiver whose band is already in the state. The transmit band is
|
||||
// always included, even on a Flex, so this can never come back empty while
|
||||
// the rig is on a frequency.
|
||||
func (m *Manager) rxBands(st RigState) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
add := func(b string) {
|
||||
if b = strings.ToLower(strings.TrimSpace(b)); b != "" && !seen[b] {
|
||||
seen[b] = true
|
||||
out = append(out, b)
|
||||
}
|
||||
}
|
||||
add(st.Band)
|
||||
if st.RxFreqHz != 0 {
|
||||
add(BandFromHz(st.RxFreqHz))
|
||||
}
|
||||
if fx, ok := m.FlexState(); ok {
|
||||
for _, sl := range fx.Slices {
|
||||
if sl.Band != "" {
|
||||
add(sl.Band)
|
||||
continue
|
||||
}
|
||||
// A slice the radio has reported a frequency but no band for.
|
||||
if sl.FreqHz != 0 {
|
||||
add(BandFromHz(sl.FreqHz))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) applyCommandDelay() {
|
||||
m.mu.RLock()
|
||||
d := m.cmdDelay
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package cat
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// RxBands is what stops the decode panel's band-drift warning from lying to a
|
||||
// station running two slices.
|
||||
//
|
||||
// Reported for W4TE: slice A on 20 m with its own WSJT-X, slice B on 40 m
|
||||
// holding transmit focus. RigState.Band is the TRANSMIT band, so the 20 m
|
||||
// decoder was told "the rig is on 40M" while the slice it listens to had been
|
||||
// on 20 m the whole time.
|
||||
func TestRxBandsAlwaysCarriesTheTransmitBand(t *testing.T) {
|
||||
m := &Manager{}
|
||||
got := m.rxBands(RigState{FreqHz: 14074000, Band: "20m"})
|
||||
if len(got) != 1 || got[0] != "20m" {
|
||||
t.Fatalf("got %v, want [20m]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A split rig receives on one band and transmits on another — cross-band split
|
||||
// is unusual but legal, and the receive side is where a decoder listens.
|
||||
func TestRxBandsIncludesTheSplitReceiveBand(t *testing.T) {
|
||||
m := &Manager{}
|
||||
got := m.rxBands(RigState{FreqHz: 14074000, Band: "20m", RxFreqHz: 7074000, Split: true})
|
||||
if !has(got, "20m") || !has(got, "40m") {
|
||||
t.Fatalf("got %v, want both 20m and 40m", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to report when the rig is on no frequency: an empty list means "there
|
||||
// is nothing to compare with", and the panel treats it as such rather than as
|
||||
// "the radio is on no band", which would warn about every decode.
|
||||
func TestRxBandsIsEmptyWithNoFrequency(t *testing.T) {
|
||||
m := &Manager{}
|
||||
if got := m.rxBands(RigState{}); len(got) != 0 {
|
||||
t.Fatalf("got %v, want nothing", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicated and lower-cased, because the panel compares strings: two slices
|
||||
// on the same band are one band, and "20M" from a backend must match "20m" from
|
||||
// BandFromHz.
|
||||
func TestRxBandsIsNormalised(t *testing.T) {
|
||||
m := &Manager{}
|
||||
got := m.rxBands(RigState{FreqHz: 14074000, Band: "20M", RxFreqHz: 14080000})
|
||||
if len(got) != 1 || got[0] != "20m" {
|
||||
t.Fatalf("got %v, want [20m]", got)
|
||||
}
|
||||
for _, b := range got {
|
||||
if b != strings.ToLower(b) {
|
||||
t.Errorf("%q is not lower-cased", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func has(list []string, want string) bool {
|
||||
for _, s := range list {
|
||||
if s == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user