feat: follow the SUB VFO over OmniRig; distance column in the QSO grids
OmniRig reports the VFO pair (AA/AB/BA/BB) on the whole Yaesu range and never the single-letter form. Only the latter was honoured, so every rig in that family stayed pinned to VFO A: pressing SUB moved the radio but not OpsLog, and a QSO worked on SUB was logged on the main VFO's frequency. The first letter of the pair is the VFO being listened on — Log4OM reads it and gets the right frequency on the same rigs, which is what showed the data was there. The enum now wins over the Yaesu Freq==FreqB inference, which is only a fallback for a rig file that names no VFO at all (the stock FTDX10 one answers neither VS; nor FR; usably — verified on the air). Split: the ON flag is still latched to survive a rig file that flips it on its own, but the latch is now ARMED only after 8 flips in 30 s. A first cut at 3-in-15s was armed by the operator toggling split while testing, imposing the 6 s clearing delay on a radio that did not need it; a misreading file flips a dozen times in that window untouched, so the two cases separate cleanly. Distance (km) column added to Recent QSOs and Worked before (shared catalog). Computed from the QSO's OWN my_grid/my_lat/lon first, falling back to the current profile's locator: a log spans years and portable outings, so the station a QSO was made from is not necessarily today's. Locator: a precise QRZ/HamQTH grid is no longer overwritten by the cty.dat entity centroid. The lookup runs several times per QSO and the provider gets 2 s; a slow second answer fell back to cty.dat and downgraded JN05JG to JN16 while name and QTH survived (they are only written when non-empty). The OmniRig diagnostic line now logs what OpsLog concluded, not just what OmniRig reported. icomnet.go: gofmt alignment only.
This commit is contained in:
@@ -1,4 +1,24 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.21.4",
|
||||||
|
"date": "2026-07-26",
|
||||||
|
"en": [
|
||||||
|
"OmniRig on an FTDX101D: the frequency follows the SUB VFO again, and split is detected. The stock rig file never names a single VFO — it reports only the A/B pair and alternates between the two values, split included, from one poll to the next. OpsLog now takes the displayed frequency as the reference on Yaesu and keeps a split reported once for a few seconds, so the contradicting reading no longer cancels it.",
|
||||||
|
"Recent QSOs and Worked before: new Distance (km) column, like the cluster's. It is computed from the QSO's own locator pair, so a contact made portable keeps the distance from where you actually were; QSOs with no locator on either side stay blank.",
|
||||||
|
"OmniRig: the frequency follows the SUB VFO on radios that report the two VFOs as a pair (AA / AB / BA / BB) — the whole Yaesu range. Only the single-VFO form was recognised, so pressing SUB left OpsLog on the main VFO, and a QSO worked on SUB was logged on the wrong frequency.",
|
||||||
|
"OmniRig split is held briefly ONLY for a radio whose rig file reports it erratically. On a radio that reports it correctly, split now clears the instant you cancel it on the front panel instead of a few seconds later.",
|
||||||
|
"The diagnostic log now records what OpsLog concluded from OmniRig (VFO, TX/RX frequencies, split), not just what OmniRig reported.",
|
||||||
|
"Locator: a precise grid from QRZ/HamQTH is no longer replaced by the country centroid (JN05JG becoming JN16 for a French station). The lookup runs more than once per QSO and the provider only gets two seconds to answer; on a slower connection the second answer fell back to cty.dat and downgraded the locator, while the name and QTH stayed — which made it look like QRZ had returned a wrong grid."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"OmniRig sur FTDX101D : la fréquence suit de nouveau le VFO SUB, et le split est détecté. Le fichier radio d'origine ne nomme jamais un VFO seul — il ne rapporte que la paire A/B, et alterne entre les deux valeurs, split compris, d'une interrogation à l'autre. OpsLog prend désormais la fréquence affichée comme référence sur Yaesu et conserve quelques secondes un split annoncé une fois, pour que la lecture contradictoire ne l'annule plus.",
|
||||||
|
"QSO récents et Déjà contacté : nouvelle colonne Distance (km), comme celle du cluster. Elle est calculée à partir des locators propres au QSO, si bien qu'un contact fait en portable garde la distance depuis l'endroit où vous étiez réellement ; les QSO sans locator d'un côté ou de l'autre restent vides.",
|
||||||
|
"OmniRig : la fréquence suit le VFO SUB sur les radios qui rapportent les deux VFO sous forme de paire (AA / AB / BA / BB) — toute la gamme Yaesu. Seule la forme à un seul VFO était reconnue, si bien qu'un passage sur SUB laissait OpsLog sur le VFO principal, et qu'un QSO fait sur SUB était enregistré sur la mauvaise fréquence.",
|
||||||
|
"Le split OmniRig n'est maintenu brièvement que pour une radio dont le fichier de définition le rapporte de façon erratique. Sur une radio qui le rapporte correctement, le split disparaît désormais dès que vous le coupez en façade, au lieu de quelques secondes plus tard.",
|
||||||
|
"Le journal de diagnostic enregistre maintenant ce qu'OpsLog a déduit d'OmniRig (VFO, fréquences TX/RX, split), et pas seulement ce qu'OmniRig a rapporté.",
|
||||||
|
"Locator : un locator précis venu de QRZ/HamQTH n'est plus remplacé par le centre du pays (JN05JG devenant JN16 pour une station française). La recherche est lancée plusieurs fois par QSO et le fournisseur ne dispose que de deux secondes ; sur une connexion plus lente, la seconde réponse retombait sur cty.dat et dégradait le locator, alors que le nom et le QTH restaient — d'où l'impression que QRZ renvoyait un mauvais locator."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"date": "2026-07-25",
|
"date": "2026-07-25",
|
||||||
|
|||||||
+23
-4
@@ -612,6 +612,10 @@ export default function App() {
|
|||||||
|
|
||||||
const userEditedRef = useRef<Set<string>>(new Set());
|
const userEditedRef = useRef<Set<string>>(new Set());
|
||||||
const lastLookedUpRef = useRef<string>('');
|
const lastLookedUpRef = useRef<string>('');
|
||||||
|
// Callsign for which a PROVIDER (QRZ/HamQTH) grid is currently in the field.
|
||||||
|
// A later cty.dat-only answer for the same call must not replace that precise
|
||||||
|
// locator with the entity centroid — see the apply block in doLookup.
|
||||||
|
const providerGridCallRef = useRef<string>('');
|
||||||
// Tracks the call we last auto-switched to the Worked-before tab for, so we
|
// Tracks the call we last auto-switched to the Worked-before tab for, so we
|
||||||
// don't keep yanking the tab on every wb refresh of the same callsign.
|
// don't keep yanking the tab on every wb refresh of the same callsign.
|
||||||
const lastWbFocusRef = useRef<string>('');
|
const lastWbFocusRef = useRef<string>('');
|
||||||
@@ -2723,6 +2727,7 @@ export default function App() {
|
|||||||
|
|
||||||
function resetAutoFill() {
|
function resetAutoFill() {
|
||||||
setName(''); setQth(''); setCountry(''); setGrid('');
|
setName(''); setQth(''); setCountry(''); setGrid('');
|
||||||
|
providerGridCallRef.current = ''; // the grid field is empty again
|
||||||
// NOTE: don't clear `wb` here. It's owned by runWorkedBefore (fast 150 ms
|
// NOTE: don't clear `wb` here. It's owned by runWorkedBefore (fast 150 ms
|
||||||
// pass) and the short-callsign guard in scheduleLookup. Clearing it inside
|
// pass) and the short-callsign guard in scheduleLookup. Clearing it inside
|
||||||
// runLookup blanked the Worked-before table for the whole (possibly slow,
|
// runLookup blanked the Worked-before table for the whole (possibly slow,
|
||||||
@@ -3015,12 +3020,24 @@ export default function App() {
|
|||||||
if (!ue.has('name') && (r.name ?? '') !== '') setName(r.name ?? '');
|
if (!ue.has('name') && (r.name ?? '') !== '') setName(r.name ?? '');
|
||||||
if (!ue.has('qth') && (r.qth ?? '') !== '') setQth(r.qth ?? '');
|
if (!ue.has('qth') && (r.qth ?? '') !== '') setQth(r.qth ?? '');
|
||||||
if (!ue.has('grid')) {
|
if (!ue.has('grid')) {
|
||||||
if ((r.grid ?? '') !== '') setGrid(r.grid ?? '');
|
if ((r.grid ?? '') !== '') {
|
||||||
|
setGrid(r.grid ?? '');
|
||||||
|
providerGridCallRef.current = call;
|
||||||
|
}
|
||||||
// No provider grid (cty.dat-only / portable): derive a 4-char grid from
|
// No provider grid (cty.dat-only / portable): derive a 4-char grid from
|
||||||
// the entity centroid (e.g. Svalbard → JQ88) so the field — and the
|
// the entity centroid (e.g. Svalbard → JQ88) so the field — and the
|
||||||
// bearing/map — aren't empty. 4 chars signals it's entity-level, not a
|
// bearing/map — aren't empty. 4 chars signals it's entity-level, not a
|
||||||
// precise QTH (matches how Log4OM shows it).
|
// precise QTH (matches how Log4OM shows it).
|
||||||
else if (r.lat || r.lon) setGrid(latLonToGrid(r.lat || 0, r.lon || 0, 4));
|
//
|
||||||
|
// Skipped when QRZ already gave a precise grid for THIS call: lookups run
|
||||||
|
// more than once per QSO (debounced typing, then blur/Enter), and the
|
||||||
|
// provider only has a 2-second budget — one slow answer fell back to
|
||||||
|
// cty.dat and downgraded JN05JG to the France centroid JN16, while Name
|
||||||
|
// and QTH stayed (they are only written when non-empty). Same rule as
|
||||||
|
// those fields: a cty.dat answer never overwrites a richer one.
|
||||||
|
else if ((r.lat || r.lon) && providerGridCallRef.current !== call) {
|
||||||
|
setGrid(latLonToGrid(r.lat || 0, r.lon || 0, 4));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Country/zones are exactly what cty.dat IS authoritative for — set them
|
// Country/zones are exactly what cty.dat IS authoritative for — set them
|
||||||
// (only skipped if empty, so we never blank a known country).
|
// (only skipped if empty, so we never blank a known country).
|
||||||
@@ -4139,7 +4156,7 @@ export default function App() {
|
|||||||
case 'worked':
|
case 'worked':
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
|
<div className="h-full w-full min-h-0 flex flex-col bg-card border border-border rounded-lg overflow-hidden">
|
||||||
<WorkedBeforeGrid wb={wbWithAwards as any} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
|
<WorkedBeforeGrid wb={wbWithAwards as any} myGrid={station.my_grid} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
|
||||||
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog}
|
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog}
|
||||||
onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
|
onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
|
||||||
onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields}
|
onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields}
|
||||||
@@ -4176,6 +4193,7 @@ export default function App() {
|
|||||||
<RecentQSOsGrid
|
<RecentQSOsGrid
|
||||||
key={`rqg-${activeProfileId ?? 'x'}`}
|
key={`rqg-${activeProfileId ?? 'x'}`}
|
||||||
rows={qsosWithAwards as any}
|
rows={qsosWithAwards as any}
|
||||||
|
myGrid={station.my_grid}
|
||||||
total={total}
|
total={total}
|
||||||
awardCols={awardCols}
|
awardCols={awardCols}
|
||||||
onRowDoubleClicked={(q) => openEdit(q.id as number)}
|
onRowDoubleClicked={(q) => openEdit(q.id as number)}
|
||||||
@@ -5309,6 +5327,7 @@ export default function App() {
|
|||||||
<RecentQSOsGrid
|
<RecentQSOsGrid
|
||||||
key={`rqg2-${activeProfileId ?? 'x'}`}
|
key={`rqg2-${activeProfileId ?? 'x'}`}
|
||||||
rows={qsosWithAwards as any}
|
rows={qsosWithAwards as any}
|
||||||
|
myGrid={station.my_grid}
|
||||||
total={total}
|
total={total}
|
||||||
awardCols={awardCols}
|
awardCols={awardCols}
|
||||||
onFilteredCountChange={setGridFilteredCount}
|
onFilteredCountChange={setGridFilteredCount}
|
||||||
@@ -5550,7 +5569,7 @@ export default function App() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="worked" className="mt-0 flex flex-col min-h-0 flex-1">
|
<TabsContent value="worked" className="mt-0 flex flex-col min-h-0 flex-1">
|
||||||
<WorkedBeforeGrid wb={wbWithAwards as any} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
|
<WorkedBeforeGrid wb={wbWithAwards as any} myGrid={station.my_grid} awardCols={awardCols} busy={wbBusy} currentCall={callsign} onRowDoubleClicked={(q) => openEdit(q.id as number)}
|
||||||
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording}
|
onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording}
|
||||||
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
|
onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)}
|
||||||
onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields}
|
onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs';
|
import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { gridToLatLon, pathBetweenLatLon } from '@/lib/maidenhead';
|
||||||
|
|
||||||
// Register every Community feature once. v32+ requires explicit registration;
|
// Register every Community feature once. v32+ requires explicit registration;
|
||||||
// AllCommunityModule keeps it simple and pulls in sort/filter/resize/reorder/
|
// AllCommunityModule keeps it simple and pulls in sort/filter/resize/reorder/
|
||||||
@@ -27,6 +28,9 @@ const hamlogTheme = hamlogGridTheme.withParams({ rowHeight: 32, headerHeight: 34
|
|||||||
type Props = {
|
type Props = {
|
||||||
rows: QSOForm[];
|
rows: QSOForm[];
|
||||||
total: number;
|
total: number;
|
||||||
|
// Operator's CURRENT locator — fallback for the distance column on older QSOs
|
||||||
|
// that carry no my_grid of their own.
|
||||||
|
myGrid?: string;
|
||||||
// Bump this number to programmatically select every row (e.g. after a search
|
// Bump this number to programmatically select every row (e.g. after a search
|
||||||
// in the QSL Manager, where the default is "all selected").
|
// in the QSL Manager, where the default is "all selected").
|
||||||
selectAllSignal?: number;
|
selectAllSignal?: number;
|
||||||
@@ -111,7 +115,26 @@ export type ColEntry = ColDef<QSOForm> & { group: string; label: string; default
|
|||||||
// hooks can't run at module level, so the component calls this with its own t.
|
// hooks can't run at module level, so the component calls this with its own t.
|
||||||
type TFn = (key: string, vars?: Record<string, string | number>) => string;
|
type TFn = (key: string, vars?: Record<string, string | number>) => string;
|
||||||
|
|
||||||
export const makeColCatalog = (t: TFn): ColEntry[] => [
|
// qsoDistanceKm returns the great-circle distance for one row, in km.
|
||||||
|
//
|
||||||
|
// The QSO's OWN my_grid / my_lat / my_lon come first: a log spans years and
|
||||||
|
// portable outings, so the station the QSO was made from is not necessarily the
|
||||||
|
// one configured today. `myGrid` (the current profile) is only the fallback for
|
||||||
|
// the many older records that carry no my_* fields at all.
|
||||||
|
function qsoDistanceKm(d: any, myGrid?: string): number | undefined {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const here =
|
||||||
|
(d.my_grid && gridToLatLon(d.my_grid)) ||
|
||||||
|
(d.my_lat || d.my_lon ? { lat: d.my_lat || 0, lon: d.my_lon || 0 } : null) ||
|
||||||
|
(myGrid ? gridToLatLon(myGrid) : null);
|
||||||
|
const there =
|
||||||
|
(d.grid && gridToLatLon(d.grid)) ||
|
||||||
|
(d.lat || d.lon ? { lat: d.lat || 0, lon: d.lon || 0 } : null);
|
||||||
|
if (!here || !there) return undefined;
|
||||||
|
return Math.round(pathBetweenLatLon(here, there).distanceShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeColCatalog = (t: TFn, myGrid?: string): ColEntry[] => [
|
||||||
// ── QSO basics ──
|
// ── QSO basics ──
|
||||||
{ group: 'QSO', label: t('rqg.c.qso_date'), colId: 'qso_date', headerName: t('rqg.c.qso_date'), field: 'qso_date' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value), sort: 'desc', defaultVisible: true },
|
{ group: 'QSO', label: t('rqg.c.qso_date'), colId: 'qso_date', headerName: t('rqg.c.qso_date'), field: 'qso_date' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value), sort: 'desc', defaultVisible: true },
|
||||||
{ group: 'QSO', label: t('rqg.c.qso_date_off'), colId: 'qso_date_off', headerName: t('rqg.c.qso_date_off'), field: 'qso_date_off' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value) },
|
{ group: 'QSO', label: t('rqg.c.qso_date_off'), colId: 'qso_date_off', headerName: t('rqg.c.qso_date_off'), field: 'qso_date_off' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value) },
|
||||||
@@ -146,6 +169,11 @@ export const makeColCatalog = (t: TFn): ColEntry[] => [
|
|||||||
{ group: 'Contacted', label: t('rqg.c.age'), colId: 'age', headerName: t('rqg.c.age'), field: 'age' as any, width: 60, type: 'rightAligned' },
|
{ group: 'Contacted', label: t('rqg.c.age'), colId: 'age', headerName: t('rqg.c.age'), field: 'age' as any, width: 60, type: 'rightAligned' },
|
||||||
{ group: 'Contacted', label: t('rqg.c.lat'), colId: 'lat', headerName: t('rqg.c.lat'), field: 'lat' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' },
|
{ group: 'Contacted', label: t('rqg.c.lat'), colId: 'lat', headerName: t('rqg.c.lat'), field: 'lat' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' },
|
||||||
{ group: 'Contacted', label: t('rqg.c.lon'), colId: 'lon', headerName: t('rqg.c.lon'), field: 'lon' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' },
|
{ group: 'Contacted', label: t('rqg.c.lon'), colId: 'lon', headerName: t('rqg.c.lon'), field: 'lon' as any, width: 90, type: 'rightAligned', cellClass: 'font-mono' },
|
||||||
|
// Derived, not stored: computed from the two locations at display time, like
|
||||||
|
// the cluster grid's own distance column.
|
||||||
|
{ group: 'Contacted', label: t('rqg.c.distance_km'), colId: 'distance_km', headerName: t('rqg.h.distance_km'), width: 90, type: 'rightAligned', cellClass: 'font-mono',
|
||||||
|
valueGetter: (p) => qsoDistanceKm(p.data, myGrid),
|
||||||
|
comparator: (a, b) => (a ?? 0) - (b ?? 0), defaultVisible: true },
|
||||||
{ group: 'Contacted', label: t('rqg.c.email'), colId: 'email', headerName: t('rqg.c.email'), field: 'email' as any, width: 180 },
|
{ group: 'Contacted', label: t('rqg.c.email'), colId: 'email', headerName: t('rqg.c.email'), field: 'email' as any, width: 180 },
|
||||||
{ group: 'Contacted', label: t('rqg.c.web'), colId: 'web', headerName: t('rqg.c.web'), field: 'web' as any, width: 180 },
|
{ group: 'Contacted', label: t('rqg.c.web'), colId: 'web', headerName: t('rqg.c.web'), field: 'web' as any, width: 180 },
|
||||||
|
|
||||||
@@ -263,7 +291,7 @@ export const groupLabel = (t: TFn, g: string): string => t(GRP_KEYS[g] ?? g);
|
|||||||
const stripAwardCols = (st: any[] | null | undefined): any[] =>
|
const stripAwardCols = (st: any[] | null | undefined): any[] =>
|
||||||
(st ?? []).filter((s) => !String(s?.colId ?? '').startsWith('award_'));
|
(st ?? []).filter((s) => !String(s?.colId ?? '').startsWith('award_'));
|
||||||
|
|
||||||
export function RecentQSOsGrid({ rows, selectAllSignal, selectRowSignal, rowDragCall, passOrder, onGridApi, storageKey, onRowDoubleClicked, onRowClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportSelectedFields, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, onFilteredCountChange, awardCols }: Props) {
|
export function RecentQSOsGrid({ rows, myGrid, selectAllSignal, selectRowSignal, rowDragCall, passOrder, onGridApi, storageKey, onRowDoubleClicked, onRowClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportSelectedFields, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, onFilteredCountChange, awardCols }: Props) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const gridRef = useRef<any>(null);
|
const gridRef = useRef<any>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
@@ -275,7 +303,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, selectRowSignal, rowDrag
|
|||||||
const [dispCount, setDispCount] = useState(0); // rows currently displayed (post column filters) — drives Select all ↔ Unselect all
|
const [dispCount, setDispCount] = useState(0); // rows currently displayed (post column filters) — drives Select all ↔ Unselect all
|
||||||
|
|
||||||
// Localized column catalog — rebuilt when the language changes.
|
// Localized column catalog — rebuilt when the language changes.
|
||||||
const COL_CATALOG = useMemo(() => makeColCatalog(t), [t]);
|
const COL_CATALOG = useMemo(() => makeColCatalog(t, myGrid), [t, myGrid]);
|
||||||
|
|
||||||
// Right-click: if the clicked row isn't already part of the selection,
|
// Right-click: if the clicked row isn't already part of the selection,
|
||||||
// select just it; then open the bulk-action menu on the whole selection.
|
// select just it; then open the bulk-action menu on the whole selection.
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ const hamlogTheme = hamlogGridTheme;
|
|||||||
type WorkedEntry = QSOForm; // entries are now full QSO records
|
type WorkedEntry = QSOForm; // entries are now full QSO records
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
// Operator's CURRENT locator — fallback for the distance column (see catalog).
|
||||||
|
myGrid?: string;
|
||||||
wb: WorkedBeforeView | null;
|
wb: WorkedBeforeView | null;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
currentCall: string;
|
currentCall: string;
|
||||||
@@ -54,14 +56,14 @@ function fmtDate(s: any): string {
|
|||||||
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
|
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkedBeforeGrid({ wb, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportSelectedFields, onExportCabrilloSelected, onDelete, awardCols }: Props) {
|
export function WorkedBeforeGrid({ wb, myGrid, busy, currentCall, onRowDoubleClicked, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportSelectedFields, onExportCabrilloSelected, onDelete, awardCols }: Props) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const gridRef = useRef<any>(null);
|
const gridRef = useRef<any>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
const [menu, setMenu] = useState<QSOMenuState>(null);
|
const [menu, setMenu] = useState<QSOMenuState>(null);
|
||||||
|
|
||||||
// Localized column catalog (shared with the Recent QSOs grid).
|
// Localized column catalog (shared with the Recent QSOs grid).
|
||||||
const COL_CATALOG = useMemo(() => makeColCatalog(t), [t]);
|
const COL_CATALOG = useMemo(() => makeColCatalog(t, myGrid), [t, myGrid]);
|
||||||
|
|
||||||
function handleRowDoubleClicked(e: RowDoubleClickedEvent<WorkedEntry>) {
|
function handleRowDoubleClicked(e: RowDoubleClickedEvent<WorkedEntry>) {
|
||||||
if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data);
|
if (e.data && onRowDoubleClicked) onRowDoubleClicked(e.data);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -98,8 +98,8 @@ type icomNet struct {
|
|||||||
vTracked uint16
|
vTracked uint16
|
||||||
vCivSeq uint16
|
vCivSeq uint16
|
||||||
|
|
||||||
rx chan []byte // CI-V byte chunks from civPump → Read (control replies)
|
rx chan []byte // CI-V byte chunks from civPump → Read (control replies)
|
||||||
scopeRx chan []byte // scope (0x27) frames, kept off rx so the panadapter
|
scopeRx chan []byte // scope (0x27) frames, kept off rx so the panadapter
|
||||||
// stream can't crowd control replies out (→ ScopeChan)
|
// stream can't crowd control replies out (→ ScopeChan)
|
||||||
leftover []byte // partial chunk not yet returned by Read (Read-only)
|
leftover []byte // partial chunk not yet returned by Read (Read-only)
|
||||||
readTO time.Duration // Read timeout (SetReadTimeout)
|
readTO time.Duration // Read timeout (SetReadTimeout)
|
||||||
@@ -114,11 +114,11 @@ type icomNet struct {
|
|||||||
// login token every ~45 s. The rig invalidates the session ~2 min after login
|
// login token every ~45 s. The rig invalidates the session ~2 min after login
|
||||||
// without renewal (this was the "loses control after 2 min" drop — RS-BA1/the
|
// without renewal (this was the "loses control after 2 min" drop — RS-BA1/the
|
||||||
// Remote Utility renew too). Owned solely by ctrlPump after dial → no lock.
|
// Remote Utility renew too). Owned solely by ctrlPump after dial → no lock.
|
||||||
cTracked uint16 // control-stream tracked seq (continues after dial)
|
cTracked uint16 // control-stream tracked seq (continues after dial)
|
||||||
cAuthSeq uint16 // token-packet innerseq
|
cAuthSeq uint16 // token-packet innerseq
|
||||||
cToken uint32 // login token (opaque, echoed back verbatim)
|
cToken uint32 // login token (opaque, echoed back verbatim)
|
||||||
cTokReq uint16 // token-request id (echoed)
|
cTokReq uint16 // token-request id (echoed)
|
||||||
cSentBuf map[uint16][]byte // control-stream retransmit buffer (token renewals)
|
cSentBuf map[uint16][]byte // control-stream retransmit buffer (token renewals)
|
||||||
|
|
||||||
// Receive-side retransmit (CI-V stream): track the rig's data-packet send seq
|
// Receive-side retransmit (CI-V stream): track the rig's data-packet send seq
|
||||||
// and ask it to resend any gap. Under the scope stream, UDP drops are common;
|
// and ask it to resend any gap. Under the scope stream, UDP drops are common;
|
||||||
@@ -844,8 +844,8 @@ func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, use
|
|||||||
copy(b[0x60:0x70], icnPasscode(user))
|
copy(b[0x60:0x70], icnPasscode(user))
|
||||||
b[0x70] = rxEnable // rxenable: 1 opens the 50003 RX audio stream, 0 = CI-V only
|
b[0x70] = rxEnable // rxenable: 1 opens the 50003 RX audio stream, 0 = CI-V only
|
||||||
b[0x71] = 0x00 // txenable (Phase 5)
|
b[0x71] = 0x00 // txenable (Phase 5)
|
||||||
b[0x72] = 0x10 // rxcodec
|
b[0x72] = 0x10 // rxcodec
|
||||||
b[0x73] = 0x04 // txcodec
|
b[0x73] = 0x04 // txcodec
|
||||||
icnBE.PutUint32(b[0x74:], 16000)
|
icnBE.PutUint32(b[0x74:], 16000)
|
||||||
icnBE.PutUint32(b[0x78:], 8000)
|
icnBE.PutUint32(b[0x78:], 8000)
|
||||||
icnBE.PutUint32(b[0x7c:], uint32(civPort))
|
icnBE.PutUint32(b[0x7c:], uint32(civPort))
|
||||||
|
|||||||
+100
-14
@@ -46,6 +46,19 @@ type OmniRig struct {
|
|||||||
// the sideband (freq moved, but mode read the old band → wrong sideband).
|
// the sideband (freq moved, but mode read the old band → wrong sideband).
|
||||||
lastSetFreq int64
|
lastSetFreq int64
|
||||||
lastSetFreqAt time.Time
|
lastSetFreqAt time.Time
|
||||||
|
|
||||||
|
// lastSplitOnAt is when OmniRig last reported PM_SPLITON cleanly. See the
|
||||||
|
// FTDX101D note in ReadState — some .ini files alternate between ON and OFF
|
||||||
|
// on consecutive polls, so the flag has to be latched to be usable.
|
||||||
|
lastSplitOnAt time.Time
|
||||||
|
|
||||||
|
// splitFlaky records that THIS rig's .ini flips the split flag on its own,
|
||||||
|
// which is what arms the latch. lastSplitFlag / splitFlips / splitFlipWindow
|
||||||
|
// count the flips inside a rolling window to detect it.
|
||||||
|
lastSplitFlag bool
|
||||||
|
splitFlaky bool
|
||||||
|
splitFlips int
|
||||||
|
splitFlipWindow time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOmniRig creates a non-connected backend. Call Connect before use.
|
// NewOmniRig creates a non-connected backend. Call Connect before use.
|
||||||
@@ -251,21 +264,61 @@ func (o *OmniRig) ReadState() (RigState, error) {
|
|||||||
splitRaw = v.Val
|
splitRaw = v.Val
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic logged ONLY when Split or VFO changes (not on a timer), so
|
// FTDX101D field capture: OmniRig alternates between two contradictory
|
||||||
// normal operation stays quiet but toggling split on the radio is captured —
|
// readings on consecutive polls — "Vfo=AB Split=0x10000(OFF)" then
|
||||||
// needed to pin down this rig's PM_SPLITON value.
|
// "Vfo=BA Split=0x8000(ON)", ~1.5 s apart, with the rig untouched. The stock
|
||||||
|
// .ini evidently has two status commands that each write these params. A
|
||||||
|
// sample-by-sample test therefore reports split for half the polls and no
|
||||||
|
// split for the other half, which the UI shows as no split at all. Latch the
|
||||||
|
// ON flag briefly so one truthful sample survives the contradicting one; the
|
||||||
|
// latch expires on its own once the rig stops reporting ON, so cancelling
|
||||||
|
// split on the radio still clears within a few seconds.
|
||||||
|
//
|
||||||
|
// The latch is ARMED ONLY for a rig that actually oscillates, because it costs
|
||||||
|
// ~6 s before split clears on screen. A correct .ini (FTDX10 with VS; and FT;
|
||||||
|
// read as separate frames, confirmed on the air 2026-07-26) never flips
|
||||||
|
// unprompted, and there the latch would be a pure delay on a reading that was
|
||||||
|
// already right.
|
||||||
|
//
|
||||||
|
// 8 flips in 30 s: a first cut at 3-in-15s was armed by the OPERATOR toggling
|
||||||
|
// split three times while testing, which then imposed the 6 s delay on a rig
|
||||||
|
// that did not need it. A misreading .ini flips every 1.5–3 s without being
|
||||||
|
// touched — a dozen in the same window — so the gap is wide. The arming also
|
||||||
|
// expires after 30 s without a flip, so a rig that behaves is never stuck with
|
||||||
|
// the delay because of one burst.
|
||||||
|
const flipWindow, flipsToArm = 30 * time.Second, 8
|
||||||
|
now := time.Now()
|
||||||
|
flagOn := splitRaw&pmSplitOn != 0 && splitRaw&pmSplitOff == 0
|
||||||
|
if flagOn {
|
||||||
|
o.lastSplitOnAt = now
|
||||||
|
}
|
||||||
|
if flagOn != o.lastSplitFlag {
|
||||||
|
o.lastSplitFlag = flagOn
|
||||||
|
if now.Sub(o.splitFlipWindow) > flipWindow {
|
||||||
|
o.splitFlipWindow, o.splitFlips, o.splitFlaky = now, 0, false
|
||||||
|
}
|
||||||
|
o.splitFlips++
|
||||||
|
if o.splitFlips >= flipsToArm {
|
||||||
|
o.splitFlaky = true
|
||||||
|
}
|
||||||
|
} else if o.splitFlaky && now.Sub(o.splitFlipWindow) > flipWindow {
|
||||||
|
o.splitFlaky, o.splitFlips = false, 0 // stopped oscillating — drop the delay
|
||||||
|
}
|
||||||
|
splitRecentOn := o.splitFlaky && !o.lastSplitOnAt.IsZero() && now.Sub(o.lastSplitOnAt) < 6*time.Second
|
||||||
|
|
||||||
|
s.FreqHz, s.RxFreqHz, s.Split = resolveOmniRigVFOs(o.rigType, freqMain, freqA, freqB, s.Vfo, splitRaw, splitRecentOn)
|
||||||
|
|
||||||
|
// Diagnostic logged ONLY when Split or VFO changes (not on a timer), so normal
|
||||||
|
// operation stays quiet but toggling split or SUB VFO on the radio is
|
||||||
|
// captured. It logs the RESOLVED tx/rx/split too: with the raw values alone a
|
||||||
|
// user's log showed what OmniRig said but not what OpsLog concluded, which is
|
||||||
|
// the half that was wrong.
|
||||||
if sig := fmt.Sprintf("%x:%x", splitRaw, rawVfo); sig != o.lastSig {
|
if sig := fmt.Sprintf("%x:%x", splitRaw, rawVfo); sig != o.lastSig {
|
||||||
o.lastSig = sig
|
o.lastSig = sig
|
||||||
debugLog.Printf("OmniRig Rig%d raw: Freq=%d FreqA=%d FreqB=%d Vfo=%q(raw=0x%X) Split=0x%X status=%d",
|
debugLog.Printf("OmniRig Rig%d raw: rig=%q Freq=%d FreqA=%d FreqB=%d Vfo=%q(raw=0x%X) Split=0x%X sticky=%v → tx=%d rx=%d split=%v",
|
||||||
o.RigNum, freqMain, freqA, freqB, s.Vfo, rawVfo, splitRaw, func() int64 {
|
o.RigNum, o.rigType, freqMain, freqA, freqB, s.Vfo, rawVfo, splitRaw, splitRecentOn,
|
||||||
if v, e := oleutil.GetProperty(o.rig, "Status"); e == nil {
|
s.FreqHz, s.RxFreqHz, s.Split)
|
||||||
return v.Val
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.FreqHz, s.RxFreqHz, s.Split = resolveOmniRigVFOs(freqMain, freqA, freqB, s.Vfo, splitRaw)
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,14 +329,14 @@ func (o *OmniRig) ReadState() (RigState, error) {
|
|||||||
// contradict each other — what fixes a Yaesu can break an Icom — and the only way
|
// contradict each other — what fixes a Yaesu can break an Icom — and the only way
|
||||||
// to change it safely is with every known rig's behaviour pinned in a test. COM
|
// to change it safely is with every known rig's behaviour pinned in a test. COM
|
||||||
// cannot be exercised from a test; this can.
|
// cannot be exercised from a test; this can.
|
||||||
func resolveOmniRigVFOs(freqMain, freqA, freqB int64, vfo string, splitRaw int64) (txHz, rxHz int64, split bool) {
|
func resolveOmniRigVFOs(rigType string, freqMain, freqA, freqB int64, vfo string, splitRaw int64, splitRecentOn bool) (txHz, rxHz int64, split bool) {
|
||||||
// PM_SPLITON is tested as a BIT, not by equality. OmniRig's Split is a flag
|
// PM_SPLITON is tested as a BIT, not by equality. OmniRig's Split is a flag
|
||||||
// word: an exact `== 0x8000` holds only for a rig whose ini sets that bit and
|
// word: an exact `== 0x8000` holds only for a rig whose ini sets that bit and
|
||||||
// nothing else, and silently reports "no split" for any rig reporting the bit
|
// nothing else, and silently reports "no split" for any rig reporting the bit
|
||||||
// alongside another. Requiring ON set and OFF clear keeps the two states apart
|
// alongside another. Requiring ON set and OFF clear keeps the two states apart
|
||||||
// (both flags are non-zero, so a bare `!= 0` would read OFF as split) while
|
// (both flags are non-zero, so a bare `!= 0` would read OFF as split) while
|
||||||
// tolerating extra bits.
|
// tolerating extra bits.
|
||||||
splitFlagged := splitRaw&pmSplitOn != 0 && splitRaw&pmSplitOff == 0
|
splitFlagged := (splitRaw&pmSplitOn != 0 && splitRaw&pmSplitOff == 0) || splitRecentOn
|
||||||
|
|
||||||
// A genuine split also needs two distinct, non-zero VFOs in the SAME band. The
|
// A genuine split also needs two distinct, non-zero VFOs in the SAME band. The
|
||||||
// band test kills the common false positive where VFO B merely holds a
|
// band test kills the common false positive where VFO B merely holds a
|
||||||
@@ -316,6 +369,32 @@ func resolveOmniRigVFOs(freqMain, freqA, freqB int64, vfo string, splitRaw int64
|
|||||||
// the IC-7610, whose stock ini reports the generic Freq as VFO B; and for the
|
// the IC-7610, whose stock ini reports the generic Freq as VFO B; and for the
|
||||||
// PM_FREQA rigs (Yaesu, Kenwood) versus the Icoms (IC-9100) that populate only
|
// PM_FREQA rigs (Yaesu, Kenwood) versus the Icoms (IC-9100) that populate only
|
||||||
// the generic Freq.
|
// the generic Freq.
|
||||||
|
// The PAIR enums name BOTH VFOs at once — first letter = the one being
|
||||||
|
// listened on, second = the one that transmits. Only the single-letter forms
|
||||||
|
// were honoured here, so a rig that reports nothing but pairs (the whole Yaesu
|
||||||
|
// family) always fell through to freqA and never followed the operator to SUB.
|
||||||
|
// Log4OM reads that first letter and logs the right frequency on the same
|
||||||
|
// rigs, which is what showed this was readable data and not a dead end.
|
||||||
|
//
|
||||||
|
// Checked BEFORE the Yaesu fallback below: an enum that names a VFO is the
|
||||||
|
// rig speaking, the fallback is only an inference.
|
||||||
|
switch {
|
||||||
|
case (vfo == "BA" || vfo == "BB") && freqB != 0:
|
||||||
|
return freqB, 0, false
|
||||||
|
case (vfo == "AA" || vfo == "AB") && freqA != 0:
|
||||||
|
return freqA, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yaesu fallback, for a rig file that names no VFO at all (the stock FTDX10
|
||||||
|
// one: it answers neither VS; nor FR; usably, verified on the air 2026-07-26).
|
||||||
|
// There the generic Freq is the last clue — matching FreqB and not FreqA means
|
||||||
|
// the operator is on SUB. Limited to Yaesu: the IC-7610's stock ini reports
|
||||||
|
// the generic Freq as VFO B permanently, where this would name the wrong VFO —
|
||||||
|
// that case is pinned in the test table.
|
||||||
|
if isYaesuRig(rigType) && freqMain != 0 && freqMain == freqB && freqB != freqA {
|
||||||
|
return freqB, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case (vfo == "B" || vfo == "BB") && freqB != 0:
|
case (vfo == "B" || vfo == "BB") && freqB != 0:
|
||||||
return freqB, 0, false
|
return freqB, 0, false
|
||||||
@@ -601,6 +680,13 @@ func omniRigMode(m int64) string {
|
|||||||
|
|
||||||
// omniRigVfo maps the OmniRig Vfo RigParamX enum to a short label, using the
|
// omniRigVfo maps the OmniRig Vfo RigParamX enum to a short label, using the
|
||||||
// documented PM_VFO* constants.
|
// documented PM_VFO* constants.
|
||||||
|
// isYaesuRig recognises a Yaesu from OmniRig's RigType (the .ini title, e.g.
|
||||||
|
// "FTDX101D", "FT-891"). Only used to gate rules that are true for Yaesu and
|
||||||
|
// false for Icom, so a mis-titled ini simply keeps the generic behaviour.
|
||||||
|
func isYaesuRig(rigType string) bool {
|
||||||
|
return strings.HasPrefix(strings.ToUpper(strings.TrimSpace(rigType)), "FT")
|
||||||
|
}
|
||||||
|
|
||||||
func omniRigVfo(v int64) string {
|
func omniRigVfo(v int64) string {
|
||||||
switch {
|
switch {
|
||||||
case v&0x40 != 0: // PM_VFOAA
|
case v&0x40 != 0: // PM_VFOAA
|
||||||
|
|||||||
@@ -14,40 +14,61 @@ func TestResolveOmniRigVFOs(t *testing.T) {
|
|||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
rig string
|
||||||
main, fa, fb int64
|
main, fa, fb int64
|
||||||
vfo string
|
vfo string
|
||||||
split int64
|
split int64
|
||||||
|
sticky bool
|
||||||
wantTX, wantRX int64
|
wantTX, wantRX int64
|
||||||
wantSplit bool
|
wantSplit bool
|
||||||
}{
|
}{
|
||||||
// The reported failure: FTDX101D, SUB VFO pressed. OmniRig names VFO B and
|
// The reported failure: FTDX101D, SUB VFO pressed. OmniRig names VFO B and
|
||||||
// reports it as the generic Freq; freqA still holds the main VFO. Preferring
|
// reports it as the generic Freq; freqA still holds the main VFO. Preferring
|
||||||
// freqA meant the display never followed the operator to B.
|
// freqA meant the display never followed the operator to B.
|
||||||
{"FTDX101D on SUB VFO", b14205, a14200, b14205, "B", pmSplitOff, b14205, 0, false},
|
{"FTDX101D on SUB VFO", "FTDX101D", b14205, a14200, b14205, "B", pmSplitOff, false, b14205, 0, false},
|
||||||
{"FTDX101D on MAIN VFO", a14200, a14200, b14205, "A", pmSplitOff, a14200, 0, false},
|
{"FTDX101D on MAIN VFO", "FTDX101D", a14200, a14200, b14205, "A", pmSplitOff, false, a14200, 0, false},
|
||||||
|
|
||||||
|
// Yaesu fallback for a rig file that names NO VFO at all (the stock FTDX10
|
||||||
|
// one, confirmed 2026-07-26): the generic Freq matching FreqB is then the
|
||||||
|
// only sign that the operator is on SUB. It applies ONLY when the enum is
|
||||||
|
// silent — when the enum names a VFO, the enum wins (pair cases below).
|
||||||
|
{"FTDX10, no enum, generic Freq is B", "FTDX10", b14205, a14200, b14205, "", pmSplitOff, false, b14205, 0, false},
|
||||||
|
// Same alternating ini: one poll says OFF while the rig IS in split. The
|
||||||
|
// latched ON flag has to survive the contradicting sample.
|
||||||
|
{"FTDX101D split, contradicting OFF sample", "FTDX101D", a14200, a14200, b14205, "AB", pmSplitOff, true, b14205, a14200, true},
|
||||||
|
// The latch must not manufacture a split out of a stale cross-band VFO B.
|
||||||
|
{"FTDX101D latch, VFOs on different bands", "FTDX101D", a14200, a14200, b21000, "AB", pmSplitOff, true, a14200, 0, false},
|
||||||
|
|
||||||
|
// Pair enums: first letter is the VFO being listened on. Reported by the
|
||||||
|
// whole Yaesu family; ignoring them pinned the display to VFO A (F4NBZ,
|
||||||
|
// 2026-07-26 — Log4OM follows SUB on the same rig through OmniRig).
|
||||||
|
{"pair enum BA, simplex → listening on B", "", b14205, a14200, b14205, "BA", pmSplitOff, false, b14205, 0, false},
|
||||||
|
{"pair enum BB, simplex → listening on B", "", b14205, a14200, b14205, "BB", pmSplitOff, false, b14205, 0, false},
|
||||||
|
{"pair enum AB, simplex → listening on A", "", a14200, a14200, b14205, "AB", pmSplitOff, false, a14200, 0, false},
|
||||||
|
{"pair enum AA, simplex → listening on A", "", a14200, a14200, b14205, "AA", pmSplitOff, false, a14200, 0, false},
|
||||||
|
|
||||||
// Non-regression: a rig that does not report the VFO enum keeps the old
|
// Non-regression: a rig that does not report the VFO enum keeps the old
|
||||||
// order — freqA, then the generic Freq, then freqB.
|
// order — freqA, then the generic Freq, then freqB.
|
||||||
{"no VFO enum, freqA populated (Yaesu/Kenwood)", a14200, a14200, 0, "", pmSplitOff, a14200, 0, false},
|
{"no VFO enum, freqA populated (Yaesu/Kenwood)", "FT-891", a14200, a14200, 0, "", pmSplitOff, false, a14200, 0, false},
|
||||||
{"no VFO enum, only generic Freq (IC-9100)", a14200, 0, 0, "", pmSplitOff, a14200, 0, false},
|
{"no VFO enum, only generic Freq (IC-9100)", "IC-9100", a14200, 0, 0, "", pmSplitOff, false, a14200, 0, false},
|
||||||
{"IC-7610: generic Freq reports B, enum says A", b14205, a14200, b14205, "A", pmSplitOff, a14200, 0, false},
|
{"IC-7610: generic Freq reports B, enum says A", "IC-7610", b14205, a14200, b14205, "A", pmSplitOff, false, a14200, 0, false},
|
||||||
|
|
||||||
// Split: PM_SPLITON must be read as a BIT. An exact == 0x8000 reported "no
|
// Split: PM_SPLITON must be read as a BIT. An exact == 0x8000 reported "no
|
||||||
// split" for any rig that sets the flag alongside another bit.
|
// split" for any rig that sets the flag alongside another bit.
|
||||||
{"split, ON flag alone", a14200, a14200, b14205, "AB", pmSplitOn, b14205, a14200, true},
|
{"split, ON flag alone", "", a14200, a14200, b14205, "AB", pmSplitOn, false, b14205, a14200, true},
|
||||||
{"split, ON flag with extra bits set", a14200, a14200, b14205, "AB", pmSplitOn | 0x40, b14205, a14200, true},
|
{"split, ON flag with extra bits set", "", a14200, a14200, b14205, "AB", pmSplitOn | 0x40, false, b14205, a14200, true},
|
||||||
{"listening on B → TX on A", b14205, a14200, b14205, "BA", pmSplitOn, a14200, b14205, true},
|
{"listening on B → TX on A", "", b14205, a14200, b14205, "BA", pmSplitOn, false, a14200, b14205, true},
|
||||||
|
|
||||||
// Split must NOT be inferred when the rig says OFF, nor from a stale VFO B
|
// Split must NOT be inferred when the rig says OFF, nor from a stale VFO B
|
||||||
// left on another band (the FT-710 / TS-570 false positive).
|
// left on another band (the FT-710 / TS-570 false positive).
|
||||||
{"OFF flag, two distinct VFOs", a14200, a14200, b14205, "A", pmSplitOff, a14200, 0, false},
|
{"OFF flag, two distinct VFOs", "", a14200, a14200, b14205, "A", pmSplitOff, false, a14200, 0, false},
|
||||||
{"ON flag but VFOs on different bands", a14200, a14200, b21000, "AB", pmSplitOn, a14200, 0, false},
|
{"ON flag but VFOs on different bands", "", a14200, a14200, b21000, "AB", pmSplitOn, false, a14200, 0, false},
|
||||||
{"ON flag but both VFOs identical", a14200, a14200, a14200, "AB", pmSplitOn, a14200, 0, false},
|
{"ON flag but both VFOs identical", "", a14200, a14200, a14200, "AB", pmSplitOn, false, a14200, 0, false},
|
||||||
{"ON and OFF both set — ambiguous, treat as no split", a14200, a14200, b14205, "A", pmSplitOn | pmSplitOff, a14200, 0, false},
|
{"ON and OFF both set — ambiguous, treat as no split", "", a14200, a14200, b14205, "A", pmSplitOn | pmSplitOff, false, a14200, 0, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
tx, rx, split := resolveOmniRigVFOs(c.main, c.fa, c.fb, c.vfo, c.split)
|
tx, rx, split := resolveOmniRigVFOs(c.rig, c.main, c.fa, c.fb, c.vfo, c.split, c.sticky)
|
||||||
if tx != c.wantTX || rx != c.wantRX || split != c.wantSplit {
|
if tx != c.wantTX || rx != c.wantRX || split != c.wantSplit {
|
||||||
t.Errorf("%s:\n got TX=%d RX=%d split=%v\n want TX=%d RX=%d split=%v",
|
t.Errorf("%s:\n got TX=%d RX=%d split=%v\n want TX=%d RX=%d split=%v",
|
||||||
c.name, tx, rx, split, c.wantTX, c.wantRX, c.wantSplit)
|
c.name, tx, rx, split, c.wantTX, c.wantRX, c.wantSplit)
|
||||||
|
|||||||
Reference in New Issue
Block a user