@ham2k/extension-sdk 0.12.12 → 0.12.14
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 +4 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +7 -0
- package/dist/settingsPanels.js +6 -3
- package/docs/distribution.md +30 -7
- package/docs/forms.md +11 -6
- package/docs/hooks.md +139 -13
- package/docs/settings.md +21 -7
- package/package.json +1 -1
package/dist/activityExports.js
CHANGED
|
@@ -53,8 +53,10 @@ function activityExportHook(rules) {
|
|
|
53
53
|
filename: filenameFor(operation, qsos ?? [], ref.ref, compactFilenames),
|
|
54
54
|
icon: rules.icon,
|
|
55
55
|
selectedByDefault: true,
|
|
56
|
-
// Segment slicing: only the contacts made while
|
|
57
|
-
|
|
56
|
+
// Segment slicing: only the contacts made while THIS reference was
|
|
57
|
+
// being activated.
|
|
58
|
+
refType: rules.activationType,
|
|
59
|
+
ref: ref.ref
|
|
58
60
|
}));
|
|
59
61
|
},
|
|
60
62
|
async generateExport(args, ctx) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.12.
|
|
1
|
+
// @ham2k/extension-sdk 0.12.14
|
|
2
2
|
/** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
|
|
3
3
|
export interface PanelScene {
|
|
4
4
|
version: 1;
|
|
@@ -597,6 +597,7 @@ export interface HookContext {
|
|
|
597
597
|
distanceUnits?: string;
|
|
598
598
|
decimalSeparator?: string;
|
|
599
599
|
groupingSeparator?: string;
|
|
600
|
+
dateOrder?: "mdy" | "dmy" | "ymd";
|
|
600
601
|
gridPrecision?: number;
|
|
601
602
|
edition?: string;
|
|
602
603
|
enabledExperiments?: string[];
|
|
@@ -703,6 +704,7 @@ export interface PostResult {
|
|
|
703
704
|
ok: boolean;
|
|
704
705
|
message?: string;
|
|
705
706
|
userMessage?: ResultMessage;
|
|
707
|
+
skipped?: boolean;
|
|
706
708
|
}
|
|
707
709
|
export interface ResultMessage {
|
|
708
710
|
presentation: "status" | "dialog";
|
|
@@ -869,6 +871,7 @@ export interface TextInputDescriptor {
|
|
|
869
871
|
}[];
|
|
870
872
|
numeric?: boolean;
|
|
871
873
|
uppercase?: boolean;
|
|
874
|
+
numbersRowKey?: string;
|
|
872
875
|
maxLength?: number;
|
|
873
876
|
suggestedValue?: string;
|
|
874
877
|
}
|
|
@@ -890,6 +893,7 @@ export interface OptionsInputDescriptor {
|
|
|
890
893
|
minCharsForSuggestions?: number;
|
|
891
894
|
placeholder?: string;
|
|
892
895
|
uppercase?: boolean;
|
|
896
|
+
numbersRowKey?: string;
|
|
893
897
|
maxLength?: number;
|
|
894
898
|
suggestedValue?: string;
|
|
895
899
|
pattern?: string;
|
|
@@ -910,6 +914,7 @@ export interface RefListInputDescriptor {
|
|
|
910
914
|
replacement: string;
|
|
911
915
|
flags?: string;
|
|
912
916
|
}[];
|
|
917
|
+
numbersRowKey?: string;
|
|
913
918
|
}
|
|
914
919
|
export interface ActivitySuggestion extends Ref {
|
|
915
920
|
distance?: number;
|
|
@@ -1004,6 +1009,7 @@ export interface ExportRequest {
|
|
|
1004
1009
|
operation: Record<string, JSONValue>;
|
|
1005
1010
|
qsos: Record<string, JSONValue>[];
|
|
1006
1011
|
segments?: OperationSegmentPayload[];
|
|
1012
|
+
events?: Record<string, JSONValue>[];
|
|
1007
1013
|
exportType?: string;
|
|
1008
1014
|
exportKey?: string;
|
|
1009
1015
|
compactFilenames?: boolean;
|
|
@@ -1037,6 +1043,7 @@ export interface ExportOption {
|
|
|
1037
1043
|
priority?: number;
|
|
1038
1044
|
selectedByDefault?: boolean;
|
|
1039
1045
|
refType?: string;
|
|
1046
|
+
ref?: string;
|
|
1040
1047
|
}
|
|
1041
1048
|
export interface ExportHook {
|
|
1042
1049
|
getExportTypes?(args: Record<string, never>, ctx: HookContext): Promise<ExportTypeDefinition[]>;
|
|
@@ -1278,9 +1285,21 @@ export interface CommandAction {
|
|
|
1278
1285
|
kind: "start" | "break" | "stop";
|
|
1279
1286
|
setLocation?: boolean;
|
|
1280
1287
|
};
|
|
1288
|
+
findInLog?: {
|
|
1289
|
+
query?: string;
|
|
1290
|
+
};
|
|
1281
1291
|
setCallField?: {
|
|
1282
1292
|
text: string;
|
|
1283
1293
|
};
|
|
1294
|
+
setTime?: {
|
|
1295
|
+
now?: boolean;
|
|
1296
|
+
deltaMillis?: number;
|
|
1297
|
+
hour?: number;
|
|
1298
|
+
minute?: number;
|
|
1299
|
+
second?: number;
|
|
1300
|
+
month?: number;
|
|
1301
|
+
day?: number;
|
|
1302
|
+
};
|
|
1284
1303
|
reloadDataFiles?: {
|
|
1285
1304
|
target: string;
|
|
1286
1305
|
};
|
|
@@ -1518,6 +1537,7 @@ export interface ExtensionManifest {
|
|
|
1518
1537
|
requiresLocation?: boolean;
|
|
1519
1538
|
requiresRadioRead?: boolean;
|
|
1520
1539
|
requiresRadioWrite?: boolean;
|
|
1540
|
+
requiresRadioRaw?: boolean;
|
|
1521
1541
|
requiresClusterRead?: boolean;
|
|
1522
1542
|
requiresClusterSpot?: boolean;
|
|
1523
1543
|
requiresClusterCommand?: boolean;
|
|
@@ -1685,6 +1705,7 @@ export interface FormField {
|
|
|
1685
1705
|
itemFields?: FormField[];
|
|
1686
1706
|
itemTitle?: string;
|
|
1687
1707
|
orderable?: boolean;
|
|
1708
|
+
itemSwitch?: string;
|
|
1688
1709
|
inline?: boolean;
|
|
1689
1710
|
itemLabel?: string;
|
|
1690
1711
|
defaultValue?: any[];
|
|
@@ -1786,6 +1807,7 @@ export interface SettingsPanelDescriptor {
|
|
|
1786
1807
|
order?: number;
|
|
1787
1808
|
sidebar?: boolean;
|
|
1788
1809
|
dataFilesSection?: boolean;
|
|
1810
|
+
section?: string;
|
|
1789
1811
|
}
|
|
1790
1812
|
export interface SettingsField extends Omit<FormField, "validate" | "transform" | "itemFields"> {
|
|
1791
1813
|
pattern?: string;
|
|
@@ -1801,6 +1823,7 @@ export interface SimpleSettingsPanel {
|
|
|
1801
1823
|
order?: number;
|
|
1802
1824
|
sidebar?: boolean;
|
|
1803
1825
|
dataFilesSection?: boolean;
|
|
1826
|
+
section?: string;
|
|
1804
1827
|
fields: (SettingsField | FormActionElement)[];
|
|
1805
1828
|
}
|
|
1806
1829
|
export interface DynamicSettingsPanel {
|
|
@@ -2073,15 +2096,44 @@ export interface PanelRadioSet extends PanelRadioTarget {
|
|
|
2073
2096
|
}
|
|
2074
2097
|
/** @deprecated `setRadio`'s argument through extension API 6. */
|
|
2075
2098
|
export type PanelRadioTune = PanelRadioSet;
|
|
2099
|
+
/**
|
|
2100
|
+
* Commands in the radio's own protocol — exactly one of `text` or `bytes`, up to 4096 bytes
|
|
2101
|
+
* (extension API 11). Where the app's driver keeps one question on the wire (CI-V, Yaesu binary,
|
|
2102
|
+
* HRD, Commander, flrig) each command waits its turn in that queue, and on a FlexRadio is numbered
|
|
2103
|
+
* like the driver's own; the reply is then the answers to these commands alone. Elsewhere the bytes
|
|
2104
|
+
* are written as given. See hooks.md for what each protocol takes.
|
|
2105
|
+
*/
|
|
2106
|
+
export interface PanelRadioRaw extends PanelRadioTarget {
|
|
2107
|
+
/** Sent as UTF-8: Kenwood, Yaesu and Elecraft ASCII, rigctld, N3FJP, SmartSDR, HRD, Commander, flrig. */
|
|
2108
|
+
text?: string;
|
|
2109
|
+
/** Each 0…255: Icom CI-V, Yaesu's binary protocol. */
|
|
2110
|
+
bytes?: number[];
|
|
2111
|
+
/**
|
|
2112
|
+
* How long to wait for the reply, up to 2000 ms; omitted or 0, the call answers once the commands
|
|
2113
|
+
* are handed to the driver. Where they are written as given, the reply is everything the radio
|
|
2114
|
+
* sent in that time — the answers to the app's own polls included.
|
|
2115
|
+
*/
|
|
2116
|
+
replyMs?: number;
|
|
2117
|
+
/** Ends the reply early, just after the first occurrence of this text or these bytes. Needs `replyMs`. */
|
|
2118
|
+
until?: string | number[];
|
|
2119
|
+
}
|
|
2120
|
+
export interface PanelRadioRawResult extends PanelRadioResult {
|
|
2121
|
+
/** What came back within `replyMs`, possibly nothing, at most 64 KiB; absent without `replyMs`. `text` is it as UTF-8. */
|
|
2122
|
+
reply?: {
|
|
2123
|
+
text: string;
|
|
2124
|
+
bytes: number[];
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2076
2127
|
export interface PanelRadioResult {
|
|
2077
2128
|
/** Accepted by the host; confirmation still comes from reported state. */
|
|
2078
2129
|
accepted: boolean;
|
|
2079
2130
|
/**
|
|
2080
2131
|
* `stopped`: a scope change while no display has the scope running — nothing was sent.
|
|
2132
|
+
* `busy`: eight raw commands to this radio are already waiting (extension API 11).
|
|
2081
2133
|
* `unsupported`: this radio, or the app's keying, cannot do it — keying calls on a radio that
|
|
2082
2134
|
* is not the logging radio, or with the app's keying off.
|
|
2083
2135
|
*/
|
|
2084
|
-
reason?: "changed" | "disconnected" | "transmitting" | "invalid" | "stopped" | "unsupported";
|
|
2136
|
+
reason?: "changed" | "disconnected" | "transmitting" | "invalid" | "stopped" | "unsupported" | "busy";
|
|
2085
2137
|
state: PanelRadioState | null;
|
|
2086
2138
|
}
|
|
2087
2139
|
/** @deprecated `PanelRadioResult`'s name through extension API 6. */
|
|
@@ -2543,6 +2595,11 @@ export declare const host: {
|
|
|
2543
2595
|
}): Promise<PanelRadioResult>;
|
|
2544
2596
|
/** Stops all CW, voice and PTT the app is keying, and a tune it started (extension API 9), as Escape does. Needs `requiresRadioWrite` and extension API 7. */
|
|
2545
2597
|
stopRadioTransmit(args: PanelRadioTarget): Promise<PanelRadioResult>;
|
|
2598
|
+
/**
|
|
2599
|
+
* Writes any command in the radio's own protocol — one that transmits included — and collects the
|
|
2600
|
+
* reply for `replyMs`. Needs `requiresRadioRaw` and extension API 11.
|
|
2601
|
+
*/
|
|
2602
|
+
sendRadioRaw(args: PanelRadioRaw): Promise<PanelRadioRawResult>;
|
|
2546
2603
|
/** The app's DX cluster connection, as Settings shows it. Needs `requiresClusterRead` and extension API 10. */
|
|
2547
2604
|
clusterStatus(): Promise<ClusterStatus>;
|
|
2548
2605
|
/** The node's recent lines after `after`, oldest first, at most `limit` of the newest. Needs `requiresClusterRead` and extension API 10. */
|
package/dist/index.js
CHANGED
|
@@ -197,6 +197,13 @@ const host = {
|
|
|
197
197
|
async stopRadioTransmit(args) {
|
|
198
198
|
return await hostCallFn()("stopRadioTransmit", { ...args });
|
|
199
199
|
},
|
|
200
|
+
/**
|
|
201
|
+
* Writes any command in the radio's own protocol — one that transmits included — and collects the
|
|
202
|
+
* reply for `replyMs`. Needs `requiresRadioRaw` and extension API 11.
|
|
203
|
+
*/
|
|
204
|
+
async sendRadioRaw(args) {
|
|
205
|
+
return await hostCallFn()("sendRadioRaw", { ...args });
|
|
206
|
+
},
|
|
200
207
|
/** The app's DX cluster connection, as Settings shows it. Needs `requiresClusterRead` and extension API 10. */
|
|
201
208
|
async clusterStatus() {
|
|
202
209
|
return await hostCallFn()("clusterStatus", {});
|
package/dist/settingsPanels.js
CHANGED
|
@@ -18,7 +18,8 @@ function settingsPanelEntriesFor(record, descriptors) {
|
|
|
18
18
|
order: hook.order ?? null,
|
|
19
19
|
kind: "simple",
|
|
20
20
|
sidebar: hook.sidebar ?? true,
|
|
21
|
-
dataFilesSection: hook.dataFilesSection ?? false
|
|
21
|
+
dataFilesSection: hook.dataFilesSection ?? false,
|
|
22
|
+
section: hook.section ?? null
|
|
22
23
|
}
|
|
23
24
|
];
|
|
24
25
|
}
|
|
@@ -33,7 +34,8 @@ function settingsPanelEntriesFor(record, descriptors) {
|
|
|
33
34
|
order: d.order ?? null,
|
|
34
35
|
kind: "dynamic",
|
|
35
36
|
sidebar: d.sidebar ?? true,
|
|
36
|
-
dataFilesSection: d.dataFilesSection ?? false
|
|
37
|
+
dataFilesSection: d.dataFilesSection ?? false,
|
|
38
|
+
section: d.section ?? null
|
|
37
39
|
}));
|
|
38
40
|
}
|
|
39
41
|
return [
|
|
@@ -46,7 +48,8 @@ function settingsPanelEntriesFor(record, descriptors) {
|
|
|
46
48
|
order: null,
|
|
47
49
|
kind: "dynamic",
|
|
48
50
|
sidebar: true,
|
|
49
|
-
dataFilesSection: false
|
|
51
|
+
dataFilesSection: false,
|
|
52
|
+
section: null
|
|
50
53
|
}
|
|
51
54
|
];
|
|
52
55
|
}
|
package/docs/distribution.md
CHANGED
|
@@ -388,7 +388,7 @@ The app builds in only its **core** extensions — the ones that are the app
|
|
|
388
388
|
itself: `adif`, `scoring`, `lookups`, `templates`, the command sets and
|
|
389
389
|
`ham2k-lofi` sync. Every other extension is a catalog bundle, and the app ships
|
|
390
390
|
some of them already packaged under `app/assets/preloaded-extensions/`,
|
|
391
|
-
installing
|
|
391
|
+
installing the ones the operator has switched on into the ordinary store. Afterwards they are
|
|
392
392
|
ordinary installed extensions — the same rows, the same update check, the same
|
|
393
393
|
Update and uninstall — so an operator starts with a working app and the catalog
|
|
394
394
|
can move it forward from there.
|
|
@@ -416,7 +416,30 @@ ship pre-loads that fail the check ([releases.md §6.4](https://github.com/ham2k
|
|
|
416
416
|
They are installed once, not every launch. A record of the highest version
|
|
417
417
|
ever pre-loaded per key is what makes that true: an app update carrying a
|
|
418
418
|
newer bundle installs it, a catalog release already ahead of it is left
|
|
419
|
-
alone, and a key the operator uninstalled stays gone
|
|
419
|
+
alone, and a key the operator uninstalled stays gone — its uninstall leaves
|
|
420
|
+
its switch off, which is what the pre-load asks before installing a newer copy.
|
|
421
|
+
|
|
422
|
+
**Only what is switched on is installed** — by the operator's choice for the
|
|
423
|
+
key, else its built-in's, else on. One switched off would be a dead row in the
|
|
424
|
+
Extensions panel, and for a new install that is most of them. Skipped, it is
|
|
425
|
+
not recorded as offered, so switching on one never offered brings it in; one
|
|
426
|
+
already offered comes back from the catalog. A shipped key's switch is kept
|
|
427
|
+
after it leaves the store, and an uninstall or a Forget leaves it off, so an
|
|
428
|
+
app update carrying a newer copy does not bring back one the operator
|
|
429
|
+
removed. Nothing is
|
|
430
|
+
installed until onboarding has been answered, since the activities it asks
|
|
431
|
+
about are pre-loads: installed on their defaults first, whichever the operator
|
|
432
|
+
then turns down would stay installed and switched off. Onboarding settles them
|
|
433
|
+
as it finishes (`settleOnboardingExtensions`), as `ONBOARD_AS` does.
|
|
434
|
+
|
|
435
|
+
An install can hold every bundle the app ships, whatever its switch. It takes
|
|
436
|
+
the pre-loaded bundles and built-ins' twins it holds switched off out of the
|
|
437
|
+
store once (`extensionsSwitchedOffRemoved`), keeping their settings,
|
|
438
|
+
credentials and switch for a reinstall, and records that done only once it has
|
|
439
|
+
read the whole store. Other catalog extensions are the operator's own and stay,
|
|
440
|
+
as do file installs. A replayed onboarding (DEVONBOARD) removes nothing. A
|
|
441
|
+
catalog install the operator makes is always switched on, whatever switch it
|
|
442
|
+
inherits.
|
|
420
443
|
|
|
421
444
|
An installed bundle may not claim a key the app builds in, and a pre-load is
|
|
422
445
|
no exception: it goes through the same door as any other install.
|
|
@@ -445,8 +468,8 @@ a callsign before its extension state was first recorded
|
|
|
445
468
|
a build without them, unasked: there is nothing left to run instead. The boot's
|
|
446
469
|
catch-up (`catchUpOnCatalogExtensions`) carries each built-in's on/off choice
|
|
447
470
|
onto its twin, over whatever an earlier attempt left there; installs the
|
|
448
|
-
pre-loads, which carry the built-in's settings, panel values and
|
|
449
|
-
a twin's first install
|
|
471
|
+
pre-loads switched on, which carry the built-in's settings, panel values and
|
|
472
|
+
credentials on a twin's first install; then
|
|
450
473
|
fetches the twin of any other built-in the operator had switched on. It records
|
|
451
474
|
the move done once the pre-load has run, so a boot killed half way carries the
|
|
452
475
|
choices again next time. It needs no network while every twin is pre-loaded.
|
|
@@ -469,8 +492,8 @@ refuses the bundle itself: a keychain that would not open or a full disk is
|
|
|
469
492
|
tried again next launch, and meanwhile the adoption fetches the catalog's copy
|
|
470
493
|
if the operator had it on. A twin the pre-load did install is never fetched
|
|
471
494
|
back after the operator uninstalls it. A choice stored under the pre-load's own
|
|
472
|
-
name — `ham2k-lookup`'s, which kept its name —
|
|
473
|
-
stale.
|
|
495
|
+
name — `ham2k-lookup`'s, which kept its name — decides whether it is installed,
|
|
496
|
+
and is kept rather than dropped as stale.
|
|
474
497
|
|
|
475
498
|
**What does not come along.** A few built-ins have no twin on purpose — `qp`,
|
|
476
499
|
which the catalog carries as one extension per party instead, and `stateparks`,
|
|
@@ -550,7 +573,7 @@ counts as the catalog's own here. The row and the details show the track, and
|
|
|
550
573
|
the details' **Follow stable releases only** puts it back on stable; an
|
|
551
574
|
uninstall or a file install clears it with the rest of the record.
|
|
552
575
|
|
|
553
|
-
At most once
|
|
576
|
+
At most once a day — on launch and on resume — the app asks the
|
|
554
577
|
catalog whether anything installed has a newer release, and says so in the
|
|
555
578
|
status bar and on the row; the notice comes down with the last update taken.
|
|
556
579
|
It never installs unasked: the notice's **Update all** is the panel's own,
|
package/docs/forms.md
CHANGED
|
@@ -66,15 +66,20 @@ export interface FormField {
|
|
|
66
66
|
// Record<string, any>[]; each item is shaped like itemFields. By default
|
|
67
67
|
// the field renders as one summary row (from itemTitle, templated
|
|
68
68
|
// against the item's values, e.g. "${name}" — NOT "{{name}}") that opens
|
|
69
|
-
// an editor dialog with per-item rows and
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
69
|
+
// an editor dialog with per-item rows and Add; `inline: true` instead
|
|
70
|
+
// renders those rows (with a reorder handle per row, and an Add row)
|
|
71
|
+
// directly in the form, skipping that outer dialog — for a list that's the
|
|
72
|
+
// main content of its own section. Either way, tapping a row (or Add)
|
|
73
|
+
// opens the same itemFields editor dialog (the form renderer recursively,
|
|
74
|
+
// scoped to itemFields), whose footer holds Delete for an existing item,
|
|
75
|
+
// which asks the operator to confirm.
|
|
75
76
|
itemFields?: FormField[];
|
|
76
77
|
itemTitle?: string;
|
|
77
78
|
orderable?: boolean;
|
|
79
|
+
// The key of an on/off value in each item, set from a switch at the end of
|
|
80
|
+
// the item's row; leave it out of itemFields. An item without the key reads
|
|
81
|
+
// as on. An app that predates this shows no switch.
|
|
82
|
+
itemSwitch?: string;
|
|
78
83
|
inline?: boolean;
|
|
79
84
|
// Singular noun for one item (e.g. "custom note file"), used in the
|
|
80
85
|
// editor dialog's Add/Edit titles and the Add button instead of `label`
|
package/docs/hooks.md
CHANGED
|
@@ -309,7 +309,7 @@ interface SpotsHook {
|
|
|
309
309
|
// Spot: {their: {call}, freq?, band?, mode?, refs?: Ref[],
|
|
310
310
|
// spot: {timeInMillis, source, label?, sourceInfo?}}
|
|
311
311
|
// SpotEligibility: {enabled, icon?}
|
|
312
|
-
// PostResult: {ok, message?}
|
|
312
|
+
// PostResult: {ok, message?, userMessage?, skipped?}
|
|
313
313
|
// PostSelfSpotRequest: {operation, freq, mode?, comment?}
|
|
314
314
|
// PostOtherSpotRequest: {qso, comment?, spotterCall?}
|
|
315
315
|
```
|
|
@@ -330,6 +330,15 @@ directly to the POTA API, no auth required). `sota`'s spot API requires an
|
|
|
330
330
|
authenticated account app-halo doesn't have yet, so `sota` stays
|
|
331
331
|
fetch-only for now.
|
|
332
332
|
|
|
333
|
+
A poster whose own rules decline a particular spot — a Discord channel whose
|
|
334
|
+
message template renders blank for it — answers `{ok: true, skipped: true}`.
|
|
335
|
+
Its icon stays neutral gray instead of lighting as sent, and no failure is
|
|
336
|
+
reported. The Spotting chip's color counts only the sources that did not skip.
|
|
337
|
+
When every one did, the chip stays neutral and the operator is told so, in
|
|
338
|
+
each source's `message` when it gives one; that spot is never what
|
|
339
|
+
auto-respotting repeats, and a round of it every source skips ends it.
|
|
340
|
+
`skipped` without `ok` is still a failure.
|
|
341
|
+
|
|
333
342
|
## ✅ `cluster` — the DX cluster connection
|
|
334
343
|
|
|
335
344
|
```ts
|
|
@@ -381,11 +390,17 @@ interface ExportHook {
|
|
|
381
390
|
generateExport(args: ExportRequest, ctx): Promise<ExportResult>
|
|
382
391
|
}
|
|
383
392
|
// ExportOptionsRequest: {operation, qsos, compactFilenames?}
|
|
384
|
-
// ExportOption: {exportType, exportKey?, format, label, filename?, icon?, color?, refType?, priority?, selectedByDefault?}
|
|
385
|
-
// ExportRequest: {operation, qsos, exportType?, exportKey?, compactFilenames?} — full QSON, one coarse call
|
|
393
|
+
// ExportOption: {exportType, exportKey?, format, label, filename?, icon?, color?, refType?, ref?, priority?, selectedByDefault?}
|
|
394
|
+
// ExportRequest: {operation, qsos, segments?, events?, exportType?, exportKey?, compactFilenames?} — full QSON, one coarse call
|
|
386
395
|
// ExportResult: {filename, mimeType, content}
|
|
387
396
|
```
|
|
388
397
|
|
|
398
|
+
`qsos` holds contacts alone. The log's event rows — `break`, `start`, notes —
|
|
399
|
+
come separately in `events`, for a file that writes the timeline (the full
|
|
400
|
+
ADIF does, with private data); `segments` is what they resolve to.
|
|
401
|
+
`operation` carries `startAtMillisMin`/`startAtMillisMax`: the span of the
|
|
402
|
+
contacts in `qsos`, not of the whole operation.
|
|
403
|
+
|
|
389
404
|
Two-step flow: the Exports Panel calls `suggestExportOptions` on every
|
|
390
405
|
`export` hook to build its selectable list (a hook that omits it never
|
|
391
406
|
appears in the panel), then calls `generateExport` — passing back the
|
|
@@ -471,6 +486,12 @@ whole-log ADIF — asks for a glyph of its own instead of borrowing one. An
|
|
|
471
486
|
`icon` name that doesn't resolve falls back to the activity's, same as an
|
|
472
487
|
unreadable `color` does.
|
|
473
488
|
|
|
489
|
+
**One reference's file.** An export claiming one reference of its `refType` —
|
|
490
|
+
one park's log, when the operation activated several — names it in `ref`, and
|
|
491
|
+
the core narrows the slice to the segments carrying that reference. Without
|
|
492
|
+
it, a park activated after a break gets every contact made at the park before
|
|
493
|
+
it. `activityExportHook` sets it for you.
|
|
494
|
+
|
|
474
495
|
**Filenames.** Build them with the SDK's `exportFilename` rather than
|
|
475
496
|
spelling one out, passing the request's `compactFilenames` straight through —
|
|
476
497
|
that is what makes the user's "Use compact file names" setting apply to your
|
|
@@ -491,7 +512,7 @@ filename: exportFilename({
|
|
|
491
512
|
Set `filename` on the OPTION, not just on the result: the panel shows the
|
|
492
513
|
option's name, and the core writes the file under it. A name computed again
|
|
493
514
|
in `generateExport` sees a different input — that call is handed only the
|
|
494
|
-
QSOs the option's `refType` covers — so the two would otherwise disagree in
|
|
515
|
+
QSOs the option's `refType` (and `ref`) covers — so the two would otherwise disagree in
|
|
495
516
|
front of the user.
|
|
496
517
|
|
|
497
518
|
## ✅ `ref:<type>` — reference handlers
|
|
@@ -681,14 +702,15 @@ interface ActivityHook {
|
|
|
681
702
|
// The input catalog. A new kind is earned by a different WIDGET, never by
|
|
682
703
|
// different validation — so a pattern-constrained field is `text` with a
|
|
683
704
|
// `pattern`, not a kind of its own (docs/design/contests.md §5.3).
|
|
684
|
-
// {kind: 'refList', refType, placeholder?, pattern?, transforms
|
|
705
|
+
// {kind: 'refList', refType, placeholder?, pattern?, transforms?,
|
|
706
|
+
// numbersRowKey?}
|
|
685
707
|
// {kind: 'text', refType, field, placeholder?, pattern?, transforms?,
|
|
686
|
-
// numeric?, uppercase? /* default true */,
|
|
687
|
-
// suggestedValue?}
|
|
708
|
+
// numeric?, uppercase? /* default true */, numbersRowKey?,
|
|
709
|
+
// maxLength?, suggestedValue?}
|
|
688
710
|
// {kind: 'options', refType, field, options: [{code, name?}], matchOn?,
|
|
689
711
|
// allowFreeform?, preferredCodes?, minCharsForSuggestions?,
|
|
690
712
|
// placeholder?, pattern?, transforms?, numeric?, uppercase?,
|
|
691
|
-
// maxLength?, suggestedValue?}
|
|
713
|
+
// numbersRowKey?, maxLength?, suggestedValue?}
|
|
692
714
|
// {kind: 'serial', refType, field, sequence: {key, scope?, start?, padTo?},
|
|
693
715
|
// editable?}
|
|
694
716
|
// {kind: 'form', refType, form} // operationControls only
|
|
@@ -700,6 +722,12 @@ interface ActivityHook {
|
|
|
700
722
|
// exports as typed. The pattern is HaLo's idea of the program rather than the
|
|
701
723
|
// program's own, so it may never cost the operator a reference.
|
|
702
724
|
//
|
|
725
|
+
// `numbersRowKey` is the one character the mobile numbers row offers after
|
|
726
|
+
// its digits while the field has focus, where the operator would otherwise
|
|
727
|
+
// switch keyboard layers for it. Without it a `refList` field offers `'-'`,
|
|
728
|
+
// which joins most programs' prefixes to their digits, and the other kinds
|
|
729
|
+
// the operator's decimal separator.
|
|
730
|
+
//
|
|
703
731
|
// `text`, `options` and `serial` values round-trip onto a ref of `refType` under
|
|
704
732
|
// `field`, through the same core-owned path refList writes refs — several may
|
|
705
733
|
// share one refType and are merged into a single ref. `suggestedValue` fills the
|
|
@@ -919,6 +947,14 @@ returning several is describing individual records. Declaring both methods is
|
|
|
919
947
|
fine and expected — a hook that does is asked only for its combinations, so
|
|
920
948
|
its plain fields aren't written twice. Implemented by: `pota`.
|
|
921
949
|
|
|
950
|
+
Only a program's **own** export asks for combinations. The full ADIF export is
|
|
951
|
+
one record per contact — the copy the operator keeps, which a reimport or a
|
|
952
|
+
log processor must not read as several contacts — so it asks every hook for
|
|
953
|
+
`fieldsForOneQSO` alone, as app-polo's does. A splitting program answers that
|
|
954
|
+
method too, for the one record — POTA names its first hunted park in `SIG_INFO`
|
|
955
|
+
and every one in `POTA_REF`. A hook answering only combinations is left out of
|
|
956
|
+
the full export, and the kernel logs an error for it as it activates.
|
|
957
|
+
|
|
922
958
|
**Only the export's OWN hook is asked.** A file submitted to one program is
|
|
923
959
|
that program's log, so an export that delegates the ADIF build names itself as
|
|
924
960
|
`mainHandler` in the `ExportRequest` — app-polo's "main handler"
|
|
@@ -992,9 +1028,9 @@ writes the contact's own operator on every record, falling back to the station
|
|
|
992
1028
|
call's base call or the station call itself. This is a
|
|
993
1029
|
deliberate divergence from app-polo, whose contact fields come first and whose
|
|
994
1030
|
main handler's are appended unchecked, so a name both answer is written twice. The **full export**, which asks every
|
|
995
|
-
program on the contact, has no
|
|
996
|
-
|
|
997
|
-
|
|
1031
|
+
program on the contact, has no main handler to order by: hooks are asked in priority order,
|
|
1032
|
+
so which of two programs keeps a shared `MY_SIG` there is not something either
|
|
1033
|
+
of them chose.
|
|
998
1034
|
|
|
999
1035
|
Because the winner is picked per field name, **answer a pair whole or not at
|
|
1000
1036
|
all**: a hook offering `MY_SIG` without `MY_SIG_INFO` takes the name and leaves
|
|
@@ -1113,7 +1149,7 @@ interface CommandHook {
|
|
|
1113
1149
|
// CommandCatalogEntry: {command, describe, params?, category?, needsOperation?, expectsParams?, suggest?}
|
|
1114
1150
|
// CommandAction (closed set): {setVfo?, setPower?, updateQso?, addQso?,
|
|
1115
1151
|
// setKeyerSpeed?, showKeyer?, setEnterSends?, sendSpots?, autoRespot?, syncAll?, updateSettings?, updateSetting?, openSettings?,
|
|
1116
|
-
// updateOperation?, markSegment?, setCallField?, reloadDataFiles?, devNotice?, devStatus?,
|
|
1152
|
+
// updateOperation?, markSegment?, findInLog?, setCallField?, setTime?, reloadDataFiles?, devNotice?, devStatus?,
|
|
1117
1153
|
// devProgress?, devHammer?, devSplash?, devOnboard?, devTester?, devPopup?,
|
|
1118
1154
|
// devTimeTravel?, toggleExperiment?}
|
|
1119
1155
|
```
|
|
@@ -2079,7 +2115,9 @@ same shape as `requiresLocation`, and shown to the operator at install:
|
|
|
2079
2115
|
`host.setRadioConnection()` and `host.setRadioSpectrum()` — and transmitting
|
|
2080
2116
|
with them: `host.sendRadioCw()`, `host.playRadioVoice()`, `host.setRadioPtt()`
|
|
2081
2117
|
and `host.stopRadioTransmit()` — and implies read, since a command answers with the
|
|
2082
|
-
radio's state.
|
|
2118
|
+
radio's state. `"requiresRadioRaw": true` (extension API 11) also allows
|
|
2119
|
+
`host.sendRadioRaw()`, any command in the radio's own protocol, and implies
|
|
2120
|
+
write. An undeclared call rejects rather than answering null.
|
|
2083
2121
|
A capability belongs to the bundle that declared it, and to the sub-extensions
|
|
2084
2122
|
that bundle defines under its own key. A separately installed bundle never
|
|
2085
2123
|
inherits one through its name: `rigpanel-themes` gets nothing from `rigpanel`.
|
|
@@ -2195,6 +2233,61 @@ not paced, and not refused for a transmission in progress.
|
|
|
2195
2233
|
connection using the same selection and identity checks. Acceptance starts the
|
|
2196
2234
|
operation; follow reported connection state for completion. It does not switch
|
|
2197
2235
|
the hardware's mains power or initiate transmission.
|
|
2236
|
+
|
|
2237
|
+
`host.sendRadioRaw({id, selection?, text | bytes, replyMs?, until?})`
|
|
2238
|
+
(extension API 11, `requiresRadioRaw`) sends commands in the radio's own
|
|
2239
|
+
protocol — exactly one of `text`, sent as UTF-8, or `bytes`, each 0…255, up
|
|
2240
|
+
to 4096 bytes. **Nothing about them is checked.** They may transmit, change
|
|
2241
|
+
any setting or put the radio somewhere the app's own controls never would;
|
|
2242
|
+
that is what the operator agreed to at install, where the grant is shown in
|
|
2243
|
+
those words. It reaches any local radio through the same `id`/`selection`
|
|
2244
|
+
check as `setRadio`, with keying on or off, and is neither paced nor refused
|
|
2245
|
+
for a transmission in progress. One radio's calls go one at a time, in order,
|
|
2246
|
+
so that one call's reply window never holds the next one's answer, and past
|
|
2247
|
+
eight waiting the next is refused as `busy`; the app's own polls may come
|
|
2248
|
+
between two calls.
|
|
2249
|
+
|
|
2250
|
+
- `replyMs`, up to 2000: how long to wait for the reply. Omitted or 0, the
|
|
2251
|
+
call answers as soon as the commands are handed to the driver, with no
|
|
2252
|
+
`reply`.
|
|
2253
|
+
- `until`, text or bytes, needs `replyMs`: the reply ends just after its first
|
|
2254
|
+
occurrence.
|
|
2255
|
+
- The answer carries `reply: {text, bytes}`, possibly empty, at most 64 KiB,
|
|
2256
|
+
with `text` its UTF-8 reading.
|
|
2257
|
+
|
|
2258
|
+
What the commands are, and what the reply holds, is the protocol's. Where the
|
|
2259
|
+
app's driver keeps one question on the wire and reads the next reply as its
|
|
2260
|
+
answer, a raw command waits its turn in that queue — written beside it, its
|
|
2261
|
+
answer would be read as the answer to the driver's question — or, on a
|
|
2262
|
+
FlexRadio, is numbered as the driver's own are. The reply is then the answers
|
|
2263
|
+
to the raw commands alone, as the radio sent them, once each has come or the
|
|
2264
|
+
window closes:
|
|
2265
|
+
|
|
2266
|
+
| Radio | Commands | Reply |
|
|
2267
|
+
|---|---|---|
|
|
2268
|
+
| Icom CI-V | `bytes`: whole frames, `FE FE` … `FD`, one at a time; the app addresses each to the radio from itself, whatever its address bytes say | each frame's answer frame (`FB`/`FA` or its data) |
|
|
2269
|
+
| Yaesu binary (FT-817/857/897) | `bytes`: five-byte commands | what the radio sends for each before it falls quiet; not read by the app |
|
|
2270
|
+
| FlexRadio | `text`: one command per line, without `C<seq>\|` — the app numbers each like its own | each one's `R` line |
|
|
2271
|
+
| Ham Radio Deluxe | `text`: one command per line, without the radio's context — the app adds it | each one's reply text, one per line |
|
|
2272
|
+
| DXLab Commander | `text`: one whole message (`<command:10>CmdGetFreq<parameters:0>`) | the field it is answered with; without `replyMs` it goes out as a set, waiting for nothing |
|
|
2273
|
+
| flrig | `text`: one XML-RPC call (`<?xml …?><methodCall>…`) — the app posts it | the response's body |
|
|
2274
|
+
|
|
2275
|
+
Every other radio's commands are written as given — several back to back
|
|
2276
|
+
(`FA00014074000;MD2;`) in one write, none of the app's own between them —
|
|
2277
|
+
since each of their answers names what it answers: Kenwood, Elecraft and
|
|
2278
|
+
Yaesu ASCII, rigctld, N3FJP, Guohetec. The reply there is everything the
|
|
2279
|
+
connection received in the window, the answers to the app's own polls
|
|
2280
|
+
included, so `until` is worth setting.
|
|
2281
|
+
|
|
2282
|
+
The app's driver still reads what the radio sends, so a raw command's effect
|
|
2283
|
+
shows in `readRadio` as a change made at the front panel would — and a command
|
|
2284
|
+
that confuses the driver confuses the app. A radio that is not connected
|
|
2285
|
+
answers `disconnected`; one whose driver takes no raw commands (a placeholder
|
|
2286
|
+
radio), `unsupported`; a malformed call, or commands that are not the
|
|
2287
|
+
protocol's (CI-V bytes that are not whole frames), `invalid`. A refusal heard
|
|
2288
|
+
within a second of a raw command on a protocol whose refusals name no command
|
|
2289
|
+
(`?;`, rigctld's `RPRT`) is taken as the raw command's, not counted against the
|
|
2290
|
+
app's own reads.
|
|
2198
2291
|
On a host with the panadapter enabled, `readRadio()` carries `spectrum`: null
|
|
2199
2292
|
for a radio without a scope, otherwise its settings as the radio reports them
|
|
2200
2293
|
(`mode`, `spanHz`, `lowHz`/`highHz`, `refLevelDb`, `speed`), what it accepts
|
|
@@ -2386,6 +2479,39 @@ row/entry→`LookupRow` transform. See `DataFileManager`
|
|
|
2386
2479
|
(`fetchType: 'raw'` — the parsed Hams of Note index is cached as JSON and
|
|
2387
2480
|
replayed through `onLoadRawData` at startup, issue #17).
|
|
2388
2481
|
|
|
2482
|
+
### N1MM call history files
|
|
2483
|
+
|
|
2484
|
+
N1MM Logger+'s call history files are uploaded to
|
|
2485
|
+
[n1mmwp.hamdocs.com](https://n1mmwp.hamdocs.com/mmfiles/categories/callhistory/)
|
|
2486
|
+
under a new name with every update (`CWOPS_4002.txt`), and downloaded by a
|
|
2487
|
+
form POST that carries a nonce from the file's own page, so no URL points at
|
|
2488
|
+
the current one. A `url` of `n1mm-history:<stem>` asks the host to find it
|
|
2489
|
+
(extension API 11):
|
|
2490
|
+
|
|
2491
|
+
```ts
|
|
2492
|
+
{ key: 'cwops-history', url: 'n1mm-history:CWOPS', fetchType: 'raw', maxAgeInDays: 7, /* … */ }
|
|
2493
|
+
```
|
|
2494
|
+
|
|
2495
|
+
- **Which file:** the newest one, by publish date, whose name is the stem or
|
|
2496
|
+
the stem followed by `-` or `_` (extension and case aside). `CWOPS` takes
|
|
2497
|
+
`CWOPS_4002.txt` and `CWOPS_4000-CCC.txt`, never `CWOPSOPEN_2026-002.txt`,
|
|
2498
|
+
though N1MM's search lists all three; to follow one variant only, name it
|
|
2499
|
+
(`n1mm-history:CWOPS_4000-CCC`). The search is read newest first, page by
|
|
2500
|
+
page, until a page lists one of the stem's files.
|
|
2501
|
+
- **When:** N1MM sends no ETag; the chosen file's name, publish date and page
|
|
2502
|
+
stand in for one, so a re-upload under the same name still counts as new. A
|
|
2503
|
+
refresh whose newest file is the one last downloaded stops after the search,
|
|
2504
|
+
like a 304. A forced refresh (RELOAD, Refresh) downloads it regardless.
|
|
2505
|
+
- **What arrives:** the file as served, in N1MM's format — a `!!Order!!,…`
|
|
2506
|
+
column line, `#` comment lines, then CSV rows. `fetchType: 'raw'` is the one
|
|
2507
|
+
that fits; `rawToJSONData` parses it. `url` in its arguments is the
|
|
2508
|
+
`n1mm-history:` string, not the file's address.
|
|
2509
|
+
- **Failures:** a stem nothing matches, a refused download, or a page that no
|
|
2510
|
+
longer has the shape the host reads fails the download with
|
|
2511
|
+
`N1mmCallHistoryUnavailable` — a notice on a refresh the operator asked for,
|
|
2512
|
+
an upload for the background passes. The requests and parsing are
|
|
2513
|
+
`halo_core`'s `n1mm_call_history.dart`, shared by the app and the CLI.
|
|
2514
|
+
|
|
2389
2515
|
### How CSV rows reach `csvToLookupEntry`
|
|
2390
2516
|
|
|
2391
2517
|
An extension writes a **per-row** mapper; the kernel synthesizes the batch
|
package/docs/settings.md
CHANGED
|
@@ -79,13 +79,19 @@ export interface SimpleSettingsPanel {
|
|
|
79
79
|
// below — this only controls the top-level nav.
|
|
80
80
|
sidebar?: boolean;
|
|
81
81
|
// Defaults false. Set true to also show this panel as a row in the Data
|
|
82
|
-
// Files screen's own "Extensions" section —
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// sidebar-opt-out panel in here.
|
|
82
|
+
// Files screen's own "Extensions" section — leave this false for anything
|
|
83
|
+
// not actually about managing data files. Independent of `sidebar`; a
|
|
84
|
+
// panel usually wants both false-then-true, but the two aren't coupled at
|
|
85
|
+
// the type level — inferring this from `sidebar: false` alone would sweep
|
|
86
|
+
// any sidebar-opt-out panel in here.
|
|
88
87
|
dataFilesSection?: boolean;
|
|
88
|
+
// The key of one of the app's own settings sections ('spotting', the
|
|
89
|
+
// Spotting section) to show this panel inside, as one row that opens it
|
|
90
|
+
// in a dialog — instead of a section of its own in Settings and a sidebar
|
|
91
|
+
// entry, whatever `sidebar` says. An app without that section (an older
|
|
92
|
+
// one, or a key it doesn't know) shows the panel as if this weren't set,
|
|
93
|
+
// so it needs no newer API level.
|
|
94
|
+
section?: string;
|
|
89
95
|
fields: (SettingsField | FormActionElement)[];
|
|
90
96
|
}
|
|
91
97
|
```
|
|
@@ -136,6 +142,7 @@ export interface SettingsPanelDescriptor {
|
|
|
136
142
|
order?: number;
|
|
137
143
|
sidebar?: boolean; // see SimpleSettingsPanel.sidebar above; defaults true
|
|
138
144
|
dataFilesSection?: boolean; // see SimpleSettingsPanel.dataFilesSection above; defaults false
|
|
145
|
+
section?: string; // see SimpleSettingsPanel.section above
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
export interface DynamicSettingsPanel {
|
|
@@ -204,7 +211,14 @@ values). Tapping it opens an editor dialog with a row per item plus an
|
|
|
204
211
|
dialog. Editing or adding an item opens the same form renderer
|
|
205
212
|
recursively, scoped to `itemFields`, in a nested dialog — there is no
|
|
206
213
|
separate "list item editor" widget, just `FormRenderer` given a smaller
|
|
207
|
-
`FormDefinition`.
|
|
214
|
+
`FormDefinition`. An existing item is deleted from that dialog's footer,
|
|
215
|
+
once the operator confirms.
|
|
216
|
+
Saving it merges what it edited into the item, so keys it has no field
|
|
217
|
+
for are kept.
|
|
218
|
+
|
|
219
|
+
Set `itemSwitch` to the key of an on/off value in each item to give every
|
|
220
|
+
row a switch for it, and leave that key out of `itemFields`. An item without
|
|
221
|
+
it reads as on.
|
|
208
222
|
|
|
209
223
|
Set `orderable: true` to allow drag-to-reorder in the editor; omit it for
|
|
210
224
|
lists where order doesn't matter.
|