@ham2k/extension-sdk 1.12.2 → 1.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activityExports.js +74 -8
- package/dist/activityScoring.js +10 -3
- package/dist/i18n/activityExports.en.json +1 -0
- package/dist/i18n/activityExports.es.json +1 -0
- package/dist/index.d.ts +37 -6
- package/docs/hooks.md +20 -4
- package/package.json +1 -1
package/dist/activityExports.js
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
import { exportTypeDefinition } from "./exportSettings.js";
|
|
2
2
|
import { exportFilename, startMillisOf } from "./exportNames.js";
|
|
3
|
-
import { segmentsWith } from "./segments.js";
|
|
3
|
+
import { operationForQso, segmentsWith } from "./segments.js";
|
|
4
|
+
import { utcDate } from "./templateContext.js";
|
|
4
5
|
import { createCachedTranslator } from "./i18n.js";
|
|
5
6
|
import en from "./i18n/activityExports.en.json";
|
|
6
7
|
import es from "./i18n/activityExports.es.json";
|
|
7
8
|
const tFor = createCachedTranslator({ en: { translation: en }, es: { translation: es } });
|
|
8
9
|
const SEPARATOR = ":";
|
|
10
|
+
const DAY_MARK = "@";
|
|
11
|
+
const DAY_LENGTH = "YYYY-MM-DD".length;
|
|
12
|
+
function byUtcDay(qsos) {
|
|
13
|
+
const timed = /* @__PURE__ */ new Map();
|
|
14
|
+
const timeless = [];
|
|
15
|
+
for (const qso of qsos) {
|
|
16
|
+
const millis = Number(qso.startAtMillis ?? 0);
|
|
17
|
+
if (!(millis > 0)) {
|
|
18
|
+
timeless.push(qso);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const day = utcDate(millis);
|
|
22
|
+
const list = timed.get(day);
|
|
23
|
+
if (list) list.push(qso);
|
|
24
|
+
else timed.set(day, [qso]);
|
|
25
|
+
}
|
|
26
|
+
const days = [...timed.keys()].filter((day) => timed.get(day).some((qso) => !qso.deleted)).sort();
|
|
27
|
+
return new Map(days.map((day, i) => [day, i === 0 ? [...timeless, ...timed.get(day)] : timed.get(day)]));
|
|
28
|
+
}
|
|
29
|
+
function activeQsos(operation, qsos, segments, activationType, ref) {
|
|
30
|
+
if (!segments || segments.length === 0) return qsos;
|
|
31
|
+
const refsAt = (qso) => refsOfType(operationForQso(operation, qso, segments), activationType);
|
|
32
|
+
const segmentRefs = segments.map((segment) => refsOfType(segment.operation, activationType).map((r) => r.ref));
|
|
33
|
+
const known = new Set(segmentRefs.flat());
|
|
34
|
+
if (known.has(ref)) {
|
|
35
|
+
const own = qsos.filter((qso) => refsAt(qso).some((r) => r.ref === ref));
|
|
36
|
+
const movedHere = known.size > 1 && segmentRefs.slice(1).some((refs) => refs.includes(ref));
|
|
37
|
+
if (own.length > 0 || movedHere) return own;
|
|
38
|
+
}
|
|
39
|
+
const active = qsos.filter((qso) => refsAt(qso).length > 0);
|
|
40
|
+
return active.length > 0 ? active : qsos;
|
|
41
|
+
}
|
|
9
42
|
function refsOfType(container, type) {
|
|
10
43
|
const refs = container.refs ?? [];
|
|
11
44
|
return refs.filter((r) => r && r.type === type && typeof r.ref === "string" && r.ref.length > 0).map((r) => ({ ref: r.ref, name: typeof r.name === "string" ? r.name : void 0 }));
|
|
@@ -34,10 +67,10 @@ function activityExportHook(rules) {
|
|
|
34
67
|
}
|
|
35
68
|
];
|
|
36
69
|
},
|
|
37
|
-
async suggestExportOptions({ operation, qsos, compactFilenames }, ctx) {
|
|
70
|
+
async suggestExportOptions({ operation, qsos, segments, compactFilenames }, ctx) {
|
|
38
71
|
const refs = refsOfType(operation, rules.activationType);
|
|
39
72
|
const t = tFor(ctx);
|
|
40
|
-
|
|
73
|
+
const whole = (ref) => ({
|
|
41
74
|
exportType: `${rules.activationType}-adif`,
|
|
42
75
|
templateData: { ref: ref.ref, refName: ref.name ?? "", activity: rules.label },
|
|
43
76
|
exportKey: `${prefix}${SEPARATOR}${ref.ref}`,
|
|
@@ -57,15 +90,44 @@ function activityExportHook(rules) {
|
|
|
57
90
|
// being activated.
|
|
58
91
|
refType: rules.activationType,
|
|
59
92
|
ref: ref.ref
|
|
60
|
-
})
|
|
93
|
+
});
|
|
94
|
+
const daysOf = (ref) => byUtcDay(activeQsos(operation, qsos ?? [], segments, rules.activationType, ref));
|
|
95
|
+
const split = rules.splitByUtcDay ? refs.map((ref) => ({ ref, days: daysOf(ref.ref) })) : [];
|
|
96
|
+
if (split.some(({ days }) => days.size > 1)) {
|
|
97
|
+
return split.flatMap(
|
|
98
|
+
({ ref, days }) => days.size === 0 ? [whole(ref)] : [...days].map(([day, dayQsos]) => ({
|
|
99
|
+
exportType: `${rules.activationType}-adif`,
|
|
100
|
+
// The day rides as the filename template's modifier: a template
|
|
101
|
+
// is dated by the whole log's first contact, and two files that
|
|
102
|
+
// differed only in their contents would overwrite each other.
|
|
103
|
+
templateData: { ref: ref.ref, refName: ref.name ?? "", activity: rules.label, modifier: day },
|
|
104
|
+
exportKey: `${prefix}${DAY_MARK}${day}${SEPARATOR}${ref.ref}`,
|
|
105
|
+
format: "adif",
|
|
106
|
+
label: t("adifForRefOnDay", { program: rules.label, ref: ref.ref, day }),
|
|
107
|
+
// Dated by the day's own first contact, which the file is.
|
|
108
|
+
filename: filenameFor(operation, dayQsos, ref.ref, compactFilenames),
|
|
109
|
+
// The day's own count: the operator checking that a day reached
|
|
110
|
+
// its threshold needs that day's number, not the whole log's.
|
|
111
|
+
qsoCount: dayQsos.filter((qso) => !qso.deleted).length,
|
|
112
|
+
icon: rules.icon,
|
|
113
|
+
selectedByDefault: true,
|
|
114
|
+
refType: rules.activationType,
|
|
115
|
+
ref: ref.ref
|
|
116
|
+
}))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return refs.map(whole);
|
|
61
120
|
},
|
|
62
121
|
async generateExport(args, ctx) {
|
|
63
122
|
const exportType = String(args.exportType ?? "");
|
|
64
123
|
const exportKey = String(args.exportKey ?? "");
|
|
65
|
-
|
|
124
|
+
const dayed = exportKey.startsWith(`${prefix}${DAY_MARK}`) && exportKey.charAt(prefix.length + DAY_MARK.length + DAY_LENGTH) === SEPARATOR;
|
|
125
|
+
const day = dayed ? exportKey.slice(prefix.length + DAY_MARK.length, prefix.length + DAY_MARK.length + DAY_LENGTH) : void 0;
|
|
126
|
+
const head = dayed ? prefix.length + DAY_MARK.length + DAY_LENGTH : prefix.length;
|
|
127
|
+
if (exportType !== `${rules.activationType}-adif` || !(dayed || exportKey.startsWith(`${prefix}${SEPARATOR}`))) {
|
|
66
128
|
throw new Error(`${rules.key}: unknown export type/key '${exportType}' / '${exportKey}'`);
|
|
67
129
|
}
|
|
68
|
-
const wanted = exportKey.slice(
|
|
130
|
+
const wanted = exportKey.slice(head + SEPARATOR.length);
|
|
69
131
|
if (!refsOfType(args.operation, rules.activationType).some((r) => r.ref === wanted)) {
|
|
70
132
|
throw new Error(`${rules.key}: operation has no ${rules.activationType} reference '${wanted}'`);
|
|
71
133
|
}
|
|
@@ -75,9 +137,13 @@ function activityExportHook(rules) {
|
|
|
75
137
|
(r) => r?.type !== rules.activationType || r?.ref === wanted
|
|
76
138
|
)
|
|
77
139
|
});
|
|
140
|
+
const qsos = day === void 0 ? args.qsos : byUtcDay(args.qsos ?? []).get(day) ?? [];
|
|
141
|
+
if (day !== void 0 && !qsos.some((qso) => !qso.deleted)) {
|
|
142
|
+
throw new Error(`${rules.key}: no contacts on ${day} for ${wanted}`);
|
|
143
|
+
}
|
|
78
144
|
const content = await adifForExport({
|
|
79
145
|
operation: claimOnly(args.operation),
|
|
80
|
-
qsos
|
|
146
|
+
qsos,
|
|
81
147
|
segments: segmentsWith(args.segments, claimOnly),
|
|
82
148
|
includePrivateData: args.includePrivateData,
|
|
83
149
|
includeLookupData: args.includeLookupData,
|
|
@@ -89,7 +155,7 @@ function activityExportHook(rules) {
|
|
|
89
155
|
operatorFallback: rules.operatorFallback
|
|
90
156
|
});
|
|
91
157
|
return {
|
|
92
|
-
filename: filenameFor(args.operation,
|
|
158
|
+
filename: filenameFor(args.operation, qsos, wanted, args.compactFilenames),
|
|
93
159
|
mimeType: "text/plain",
|
|
94
160
|
content
|
|
95
161
|
};
|
package/dist/activityScoring.js
CHANGED
|
@@ -34,6 +34,7 @@ function activityScorer(rules) {
|
|
|
34
34
|
return typeof rule === "function" ? rule(activatedRefs) : rule;
|
|
35
35
|
}
|
|
36
36
|
const freshRefRescuesActivation = rules.freshRefRescuesActivation ?? true;
|
|
37
|
+
const distinctCallsPerDay = rules.distinctCallsPerDay ?? false;
|
|
37
38
|
const tracksActivation = rules.tracksActivation ?? true;
|
|
38
39
|
const tracksOperators = rules.tracksOperators ?? true;
|
|
39
40
|
const allowsMultiple = rules.allowsMultipleReferences ?? false;
|
|
@@ -125,16 +126,22 @@ function activityScorer(rules) {
|
|
|
125
126
|
const now = { day, band, mode, refs: huntedRefs };
|
|
126
127
|
const priors = sheet.workedByCall[call] ?? [];
|
|
127
128
|
const verdict = judge(priors, now);
|
|
128
|
-
|
|
129
|
+
let isDupe = verdict === "duplicate";
|
|
129
130
|
const freshRefOnly = verdict === "ref" && !freshRefRescuesActivation;
|
|
130
|
-
|
|
131
|
+
let activationCredit = freshRefOnly ? 0 : activationValue;
|
|
132
|
+
if (distinctCallsPerDay && activationCredit > 0 && !isDupe) {
|
|
133
|
+
const days = sheet.activationCallDays ?? (sheet.activationCallDays = {});
|
|
134
|
+
activationCredit = days[call] === day ? 0 : 1;
|
|
135
|
+
days[call] = day;
|
|
136
|
+
if (activationCredit + huntingValue === 0 && !verdict) isDupe = true;
|
|
137
|
+
}
|
|
131
138
|
const hasStake = activationValue + huntingValue > 0;
|
|
132
139
|
const score = { value: isDupe ? 0 : activationCredit + huntingValue };
|
|
133
140
|
if (!hasStake) {
|
|
134
141
|
} else if (isDupe) {
|
|
135
142
|
score.dupe = true;
|
|
136
143
|
score.alerts = ["duplicate"];
|
|
137
|
-
} else if (verdict) {
|
|
144
|
+
} else if (verdict && verdict !== "duplicate") {
|
|
138
145
|
score.notices = [NOTICE_KEYS[verdict]];
|
|
139
146
|
} else if (priors.length > 0 && score.value > 0 && freshRefRescuesActivation) {
|
|
140
147
|
score.dupe = false;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 1.12.
|
|
1
|
+
// @ham2k/extension-sdk 1.12.4
|
|
2
2
|
/** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
|
|
3
3
|
export interface PanelScene {
|
|
4
4
|
version: 1;
|
|
@@ -175,10 +175,18 @@ export interface PanelSceneSpectrum {
|
|
|
175
175
|
display?: "both" | "pan" | "waterfall";
|
|
176
176
|
/**
|
|
177
177
|
* Which way the waterfall moves; the panadapter sits on the side it comes from. Frequency rises
|
|
178
|
-
* left to right for `down` (the default) and `up`, bottom to top for
|
|
179
|
-
*
|
|
178
|
+
* as `rising` says: by default left to right for `down` (the default) and `up`, bottom to top for
|
|
179
|
+
* `right` and top to bottom for `left` — `down` turned a quarter either way. Text stays upright;
|
|
180
|
+
* offsets and `corner`s are measured as if flowing down.
|
|
180
181
|
*/
|
|
181
182
|
flow?: "down" | "up" | "right" | "left";
|
|
183
|
+
/**
|
|
184
|
+
* Which way frequency rises, across the `flow`: `right` or `left` for `down` and `up`, `down` or
|
|
185
|
+
* `up` for `right` and `left`. Omitted, `right` for `down` and `up`, `up` for `right`, `down` for
|
|
186
|
+
* `left`. Offsets, `align` and `corner`s are still measured as if frequency rose to the right.
|
|
187
|
+
* Extension API 12; before it, `left` rose bottom to top as `right` does.
|
|
188
|
+
*/
|
|
189
|
+
rising?: "right" | "left" | "down" | "up";
|
|
182
190
|
/** The panadapter's share of the height with `both`, 0.1–0.9. Default 0.4. */
|
|
183
191
|
split?: number;
|
|
184
192
|
/** dB added to the radio's own floor. */
|
|
@@ -222,7 +230,9 @@ export interface PanelSceneSpectrum {
|
|
|
222
230
|
/**
|
|
223
231
|
* Hz shown across the control, from the middle of each sweep. A sweep wider than this is cropped
|
|
224
232
|
* to it, so a radio whose scope comes in a few widths can be shown at any narrower one — set the
|
|
225
|
-
* scope (`setRadioSpectrum`) to the narrowest that covers it.
|
|
233
|
+
* scope (`setRadioSpectrum`) to the narrowest that covers it. Where the radio's `spectrum` says
|
|
234
|
+
* `autoSpan`, the app sets the scope itself, wide enough for the widest `span` on screen.
|
|
235
|
+
* Extension API 7; `autoSpan` 12.
|
|
226
236
|
*/
|
|
227
237
|
span?: PanelSceneSpectrumNumber;
|
|
228
238
|
/** A click tunes to the nearest multiple of this many Hz. Omitted, wherever it lands. */
|
|
@@ -408,6 +418,12 @@ export interface PanelSceneControl extends PanelSceneSpectrum {
|
|
|
408
418
|
value: string;
|
|
409
419
|
to: number;
|
|
410
420
|
};
|
|
421
|
+
/**
|
|
422
|
+
* A `button` only: a click sends, as the event's `value`, where along the button's longer side it
|
|
423
|
+
* landed — 0 at its top or left end, 1 at its bottom or right — so a scale can be clicked at a
|
|
424
|
+
* place. Keys and screen readers activate it with no value, as does an app before this. Extension API 12.
|
|
425
|
+
*/
|
|
426
|
+
tapAt?: boolean;
|
|
411
427
|
/**
|
|
412
428
|
* 0 to 1, fixed or bound like a layer's. At 0 the control is gone: it takes no clicks,
|
|
413
429
|
* keyboard focus or screen-reader node. Anything between is only drawn fainter.
|
|
@@ -1099,6 +1115,7 @@ export interface ExportResult {
|
|
|
1099
1115
|
export interface ExportOptionsRequest {
|
|
1100
1116
|
operation: Record<string, JSONValue>;
|
|
1101
1117
|
qsos: Record<string, JSONValue>[];
|
|
1118
|
+
segments?: OperationSegmentPayload[];
|
|
1102
1119
|
compactFilenames?: boolean;
|
|
1103
1120
|
}
|
|
1104
1121
|
export interface ExportOption {
|
|
@@ -2098,13 +2115,24 @@ export interface PanelRadioSpectrum {
|
|
|
2098
2115
|
mode: "center" | "fixed" | null;
|
|
2099
2116
|
/** Half the width shown while centered — what the radio's menu shows as ±. */
|
|
2100
2117
|
spanHz: number | null;
|
|
2101
|
-
/**
|
|
2118
|
+
/**
|
|
2119
|
+
* The edges shown: while fixed, and while centred where the radio places its scope apart from the
|
|
2120
|
+
* dial (a FlexRadio's panadapter, which a retune inside it does not move). Null where the scope
|
|
2121
|
+
* sits on the dial, `spanHz` either side of it.
|
|
2122
|
+
*/
|
|
2102
2123
|
lowHz: number | null;
|
|
2103
2124
|
highHz: number | null;
|
|
2104
2125
|
refLevelDb: number | null;
|
|
2105
2126
|
speed: PanelRadioSpectrumSpeed | null;
|
|
2106
2127
|
/** What `setRadioSpectrum` accepts. A span between two is snapped to the nearer. */
|
|
2107
2128
|
spansHz: number[];
|
|
2129
|
+
/**
|
|
2130
|
+
* The app sets the span itself — a sweep it computes, whose width changes nothing on the radio
|
|
2131
|
+
* (a FlexRadio's) — to the narrowest covering the widest `span` of the spectrum controls shown,
|
|
2132
|
+
* each cropping its own width. A panel then names its width with `span` and leaves
|
|
2133
|
+
* `setRadioSpectrum`'s `spanHz` alone, or the panels take turns. Absent before extension API 12.
|
|
2134
|
+
*/
|
|
2135
|
+
autoSpan?: boolean;
|
|
2108
2136
|
refMinDb: number;
|
|
2109
2137
|
refMaxDb: number;
|
|
2110
2138
|
refStepDb: number;
|
|
@@ -2423,6 +2451,7 @@ export interface ActivityScoringRules {
|
|
|
2423
2451
|
tracksActivation?: boolean;
|
|
2424
2452
|
tracksOperators?: boolean;
|
|
2425
2453
|
freshRefRescuesActivation?: boolean;
|
|
2454
|
+
distinctCallsPerDay?: boolean;
|
|
2426
2455
|
allowsMultipleReferences?: boolean;
|
|
2427
2456
|
uniquePer?: readonly UniqueAxis[];
|
|
2428
2457
|
activates?: "daily" | "once";
|
|
@@ -2446,6 +2475,7 @@ export type ActivityScoresheet = {
|
|
|
2446
2475
|
dayActivatedQsos: number;
|
|
2447
2476
|
dayHuntedQsos: number;
|
|
2448
2477
|
dayDuplicates: number;
|
|
2478
|
+
activationCallDays?: Record<string, number>;
|
|
2449
2479
|
};
|
|
2450
2480
|
export declare function activityScorer(rules: ActivityScoringRules): ContestScorer<ActivityScoresheet>;
|
|
2451
2481
|
export interface ActivityExportRules {
|
|
@@ -2456,10 +2486,11 @@ export interface ActivityExportRules {
|
|
|
2456
2486
|
icon?: string;
|
|
2457
2487
|
includeFieldsFrom?: string[];
|
|
2458
2488
|
operatorFallback?: OperatorFallback;
|
|
2489
|
+
splitByUtcDay?: boolean;
|
|
2459
2490
|
}
|
|
2460
2491
|
export declare function activityExportHook(rules: ActivityExportRules): {
|
|
2461
2492
|
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
2462
|
-
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
2493
|
+
suggestExportOptions({ operation, qsos, segments, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
2463
2494
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
2464
2495
|
};
|
|
2465
2496
|
export interface HuntingExportRules {
|
package/docs/hooks.md
CHANGED
|
@@ -413,7 +413,7 @@ interface ExportHook {
|
|
|
413
413
|
suggestExportOptions?(args: ExportOptionsRequest, ctx): Promise<ExportOption[]>
|
|
414
414
|
generateExport(args: ExportRequest, ctx): Promise<ExportResult>
|
|
415
415
|
}
|
|
416
|
-
// ExportOptionsRequest: {operation, qsos, compactFilenames?}
|
|
416
|
+
// ExportOptionsRequest: {operation, qsos, segments?, compactFilenames?}
|
|
417
417
|
// ExportOption: {exportType, exportKey?, format, label, filename?, icon?, color?, refType?, ref?, priority?, selectedByDefault?}
|
|
418
418
|
// ExportRequest: {operation, qsos, segments?, events?, exportType?, exportKey?, compactFilenames?} — full QSON, one coarse call
|
|
419
419
|
// ExportResult: {filename, mimeType, content}
|
|
@@ -1926,7 +1926,7 @@ the extension. `value` names what it shows and sets.
|
|
|
1926
1926
|
|
|
1927
1927
|
| Kind | Binds to | Sends | Also takes |
|
|
1928
1928
|
|---|---|---|---|
|
|
1929
|
-
| `button` | — | `activate` | `menu` |
|
|
1929
|
+
| `button` | — | `activate` | `menu`, `tapAt` (extension API 12: a click's `value` is where along the button's longer side it landed, 0 at its top or left end to 1 at its bottom or right; a key or screen reader activates it with none) |
|
|
1930
1930
|
| `slider` | number | `commit` | `min`, `max`, `step`, `discrete`, `hover`, `valueLabels`, `continuous`, `scrollThrough` |
|
|
1931
1931
|
| `knob` | number | `commit` | `min`, `max`, `step`, `sensitivity`, `invertDrag`, `scrollThrough`, `valueLabels`, `continuous`, `menu`, a second axis (`valueY`, `eventY`, `minY`, `maxY`, `stepY`, `sensitivityY`) |
|
|
1932
1932
|
| `nativeButton` | — | `activate` | `variant` (`filled`, `tonal`, `outlined`, `text`), `icon`, `menu`; needs `event` or `menu` |
|
|
@@ -2104,6 +2104,13 @@ control and takes the fields of `PanelSceneSpectrum` in the SDK:
|
|
|
2104
2104
|
(`both`, `pan`, `waterfall`), `split`, and `flow` — which way the waterfall
|
|
2105
2105
|
moves (`down`, `up`, `right`, `left`), the panadapter on the side it comes
|
|
2106
2106
|
from.
|
|
2107
|
+
- `rising` (extension API 12): which way frequency increases, across the
|
|
2108
|
+
flow — `right` or `left` for `down` and `up`, `down` or `up` for `right` and
|
|
2109
|
+
`left`; one along the flow is refused. Omitted, each flow is `down` turned a
|
|
2110
|
+
quarter, never mirrored: `right` for `down` and `up`, `up` for `right`,
|
|
2111
|
+
`down` for `left` (before API 12, `left` rose up the screen too). Text stays
|
|
2112
|
+
upright, and offsets, `align` and pointer corners are measured as if
|
|
2113
|
+
frequency rose to the right.
|
|
2107
2114
|
- `floor`, `range`, `average` (0–99, default 40), `fps` (default 15),
|
|
2108
2115
|
`waterfallSpeed`, `waterfallFloor`, `waterfallRange`: numbers, or the names
|
|
2109
2116
|
of scene values the control reads on every sweep.
|
|
@@ -2121,7 +2128,10 @@ control and takes the fields of `PanelSceneSpectrum` in the SDK:
|
|
|
2121
2128
|
number or a scene value. A wider sweep is cropped to it, so a radio whose
|
|
2122
2129
|
scope comes in a few fixed widths can be shown at any narrower one; set the
|
|
2123
2130
|
scope to the narrowest that covers it. Everything is drawn, labelled and
|
|
2124
|
-
clicked on the window shown.
|
|
2131
|
+
clicked on the window shown. Where the radio's `spectrum` says `autoSpan`
|
|
2132
|
+
(extension API 12), the app sets the scope itself, to the narrowest span
|
|
2133
|
+
covering the widest `span` among the spectrum controls on screen, so each
|
|
2134
|
+
panel names its width here and leaves `setRadioSpectrum`'s `spanHz` alone.
|
|
2125
2135
|
- `follow` (0–0.45): keeps the tuned frequency at least this share of the
|
|
2126
2136
|
width from either edge, by moving the radio's scope just far enough to put
|
|
2127
2137
|
it on that margin — the picture scrolls with the dial rather than jumping
|
|
@@ -2357,7 +2367,13 @@ for a radio without a scope, otherwise its settings as the radio reports them
|
|
|
2357
2367
|
(`mode`, `spanHz`, `lowHz`/`highHz`, `refLevelDb`, `speed`), what it accepts
|
|
2358
2368
|
(`spansHz`, `refMinDb`…`refStepDb`, `modes`, `speeds`) and `problem`, why it
|
|
2359
2369
|
cannot run, in the operator's language. The settings are known only while a
|
|
2360
|
-
`spectrum` control has the scope running, and null otherwise.
|
|
2370
|
+
`spectrum` control has the scope running, and null otherwise. `autoSpan`
|
|
2371
|
+
(extension API 12) is true where the app sizes the scope for its displays — a
|
|
2372
|
+
sweep it computes, whose width changes nothing on the radio (a FlexRadio's DAX
|
|
2373
|
+
IQ): it runs at the narrowest span covering the widest spectrum control's
|
|
2374
|
+
`span` on screen, each control cropping its own width from it, and narrows
|
|
2375
|
+
again as wider ones go. A panel there names its width with `span`; setting
|
|
2376
|
+
`spanHz` too has the panels take turns.
|
|
2361
2377
|
`host.setRadioSpectrum({id, selection?, mode?, spanHz?, refLevelDb?, speed?})`
|
|
2362
2378
|
changes them; a span between two the radio offers is snapped to the nearer. It
|
|
2363
2379
|
answers `stopped` when no control is running the scope: nothing is queued for
|