@ham2k/extension-sdk 0.12.14 → 1.12.0

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/README.md CHANGED
@@ -53,6 +53,13 @@ offline and describe the version installed here rather than whatever the
53
53
  repository says today. If an agent is writing your extension, point it at
54
54
  `node_modules/@ham2k/extension-sdk/AGENTS.md` — `h2kext-init` does that for you.
55
55
 
56
+ ## Versions
57
+
58
+ The minor version is the extension API: SDK 1.12 describes API 12, and
59
+ exports it as `API_VERSION`. `@ham2k/extension-tools` follows the same rule,
60
+ so a `^1.12.0` range says the extension needs API 12 and admits every later
61
+ one. A major version is a change that breaks an extension already written.
62
+
56
63
  ## Export types and settings
57
64
 
58
65
  Export hooks can register shared types with `getExportTypes()`. Use
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.12.14
1
+ // @ham2k/extension-sdk 1.12.0
2
2
  /** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface PanelScene {
4
4
  version: 1;
@@ -21,6 +21,20 @@ export interface PanelScene {
21
21
  radio?: {
22
22
  node?: string;
23
23
  };
24
+ /**
25
+ * A scene longer than its pane along `axis` (default `y`), scrolled by the app itself — wheel,
26
+ * trackpad and drag, a drag begun on a button included — with no call to the extension. Scaled
27
+ * to fill the pane across, it runs on past it along the axis. The position, in scene units from
28
+ * the start, is written to the scene value `value` as it scrolls; a render that changes that
29
+ * value moves the view there, and one that leaves it alone leaves the operator's scroll alone.
30
+ * `event`, when given, is sent as a `commit` with the position as `value` once scrolling comes
31
+ * to rest. Layers and controls marked `fixed` stay put over the pane. No `layout`. Extension API 12.
32
+ */
33
+ scroll?: {
34
+ axis?: "x" | "y";
35
+ value: string;
36
+ event?: string;
37
+ };
24
38
  }
25
39
  /** Host-fed values of a scene with `radio`: its meters, and `radio.transmitting` as 1 or 0. Reserved: a scene may not declare them. */
26
40
  export type PanelSceneRadioValue = "radio.signalDbm" | "radio.powerOut" | "radio.swr" | "radio.alc" | "radio.supplyVolts" | "radio.transmitting"
@@ -52,13 +66,28 @@ export interface PanelSceneLayer {
52
66
  y: number;
53
67
  width: number;
54
68
  height: number;
55
- /** Supply exactly one of svg or text. SVG must be self-contained. */
69
+ /** Supply exactly one of svg, text or icon. SVG must be self-contained. */
56
70
  svg?: string;
71
+ /**
72
+ * A named icon — MDI, or FontAwesome with `fa-` — drawn by the app to fit the layer, in `color`
73
+ * (`#RRGGBB`) or the theme's. Extension API 12.
74
+ */
75
+ icon?: {
76
+ name: string;
77
+ color?: string;
78
+ };
57
79
  /**
58
80
  * Take the clicks that land on this layer. Off by default: layers are click-through, so
59
81
  * artwork painted over a control does not stop it working. On, it covers the controls under it.
60
82
  */
61
83
  blocksPointer?: boolean;
84
+ /** In a scene with `scroll`: drawn over the pane where it is, in pane coordinates, not scrolled. Extension API 12. */
85
+ fixed?: boolean;
86
+ /**
87
+ * While a `zoom` strip is dragged, scaled along the scroll about the pane's middle, rather than
88
+ * moved there at its own size: for a background, or a line that runs the whole way. Extension API 12.
89
+ */
90
+ stretch?: boolean;
62
91
  /** Exactly one of literal/value. A value naming a string shows it as-is. Use text layers, not SVG <text>, for font support.
63
92
  * Size is unscaled; the host applies OS text scaling once. */
64
93
  text?: {
@@ -121,10 +150,16 @@ export interface PanelSceneLayer {
121
150
  * reach the extension. Needs `requiresRadioRead`; a host without the feature, or a radio
122
151
  * without a scope, shows why instead. A click sends `commit` with the frequency as `value`.
123
152
  * Takes no `value`; see `PanelSceneSpectrum` for the rest. Extension API 6: declare `"api": 6`.
153
+ * - `zoom`: a strip of a scene with `scroll`, drawn by nothing, where a drag along the scroll zooms
154
+ * about the pane's middle — away from it in, toward it out, by the ratio of the distances. While
155
+ * the drag lasts the app moves each layer and control to where the zoom puts its middle, at its
156
+ * own size (a `stretch` layer scales instead), and on release sends `commit` with the factor as
157
+ * `value`; `min` and `max` are the factors allowed. A tap on it still reaches what lies there,
158
+ * and a drag elsewhere still scrolls. Needs `event`. Extension API 12.
124
159
  *
125
160
  * `label` is every control's accessible name. There are no tooltips.
126
161
  */
127
- export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText" | "spectrum";
162
+ export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText" | "spectrum" | "zoom";
128
163
  /** A literal, or the name of a scene value — which a slider with no `event` retunes with no bridge call. */
129
164
  export type PanelSceneSpectrumNumber = number | string;
130
165
  /**
@@ -361,6 +396,18 @@ export interface PanelSceneControl extends PanelSceneSpectrum {
361
396
  multi?: boolean;
362
397
  /** Shown but inert: no clicks, drags, keys or screen-reader adjustments act on it. */
363
398
  disabled?: boolean;
399
+ /** In a scene with `scroll`: placed over the pane, in pane coordinates, not scrolled. Needs a rect. Extension API 12. */
400
+ fixed?: boolean;
401
+ /**
402
+ * A drawn control only (`button`, `slider`, `knob`): while the pointer is over it, the scene value
403
+ * `value` reads `to`, set by the host with no call to the extension; leaving puts back the scene's
404
+ * own value. Artwork bound to it lights up under the pointer — a spot's line with its call.
405
+ * Survives renders while the pointer stays. Extension API 12.
406
+ */
407
+ hoverSet?: {
408
+ value: string;
409
+ to: number;
410
+ };
364
411
  /**
365
412
  * 0 to 1, fixed or bound like a layer's. At 0 the control is gone: it takes no clicks,
366
413
  * keyboard focus or screen-reader node. Anything between is only drawn fainter.
@@ -387,6 +434,17 @@ export interface PanelSceneControl extends PanelSceneSpectrum {
387
434
  valueLabels?: string[];
388
435
  /** Knobs use relative horizontal/upward dragging. Value per scene unit; default 1. */
389
436
  sensitivity?: number;
437
+ /**
438
+ * A knob only: a drag moves the value the other way — content held under the finger, scrolled
439
+ * by dragging, rather than a dial turned. Scrolling is unchanged. Extension API 12.
440
+ */
441
+ invertDrag?: boolean;
442
+ /**
443
+ * A slider or knob only: it also takes the scrolls that land on drawn buttons over it — labels
444
+ * drawn over a knob that scrolls the artwork. A native control, a `blocksPointer` layer or another
445
+ * slider or knob in between keeps the scroll. Extension API 12.
446
+ */
447
+ scrollThrough?: boolean;
390
448
  /**
391
449
  * A knob's second axis: with `valueY`, horizontal scrolling, dragging and the left/right keys set
392
450
  * `value`, and vertical ones (and up/down) set `valueY` — sent as its own event, `eventY`, which a
@@ -687,8 +745,22 @@ export interface GetSpotsOptions {
687
745
  annotate?: boolean;
688
746
  operationUuid?: string;
689
747
  }
748
+ export interface SuggestedQso {
749
+ their: {
750
+ call: string;
751
+ };
752
+ freq?: number;
753
+ band?: string;
754
+ mode?: string;
755
+ refs?: SpotRef[];
756
+ }
690
757
  export interface BoardSpot extends Spot {
691
758
  hookKey?: string;
759
+ display?: {
760
+ icon: string;
761
+ iconColor?: string;
762
+ emoji?: string;
763
+ };
692
764
  annotations?: SpotAnnotations;
693
765
  }
694
766
  export interface SpotAnnotations {
@@ -1288,6 +1360,9 @@ export interface CommandAction {
1288
1360
  findInLog?: {
1289
1361
  query?: string;
1290
1362
  };
1363
+ navigate?: {
1364
+ to: string;
1365
+ };
1291
1366
  setCallField?: {
1292
1367
  text: string;
1293
1368
  };
@@ -1384,6 +1459,8 @@ export interface CommandCatalogEntry {
1384
1459
  params?: string;
1385
1460
  category?: string;
1386
1461
  needsOperation?: boolean;
1462
+ needsTestData?: boolean;
1463
+ keywords?: string[];
1387
1464
  expectsParams?: boolean;
1388
1465
  suggest?: boolean;
1389
1466
  }
@@ -1994,7 +2071,7 @@ export interface PanelRadioState {
1994
2071
  endedAtMillis: number;
1995
2072
  } | null;
1996
2073
  /**
1997
- * The radio's scope, on hosts with the panadapter enabled: absent there otherwise, and null
2074
+ * The radio's scope: absent on an app without a panadapter, and null
1998
2075
  * for a radio without one. The sweeps are never sent — a `spectrum` scene control draws them.
1999
2076
  */
2000
2077
  spectrum?: PanelRadioSpectrum | null;
@@ -2542,6 +2619,14 @@ export interface TemplateContextArgs {
2542
2619
  }
2543
2620
  export declare function templateContext(args: TemplateContextArgs): Record<string, unknown>;
2544
2621
  export declare function isTestOperation(stationCall: JSONValue | undefined): boolean;
2622
+ /**
2623
+ * The extension API this SDK describes: the highest `api` a manifest written
2624
+ * against it can declare, and the one its types and host calls are for. The
2625
+ * SDK's and `@ham2k/extension-tools`' minor version is this number, so a
2626
+ * `^1.<api>.0` range names the oldest API an extension needs and admits every
2627
+ * later one.
2628
+ */
2629
+ export declare const API_VERSION = 12;
2545
2630
  export declare function defineExtension(def: ExtensionDefinition): void;
2546
2631
  export declare const hooks: {
2547
2632
  invokeAll(category: HookCategoryName, method: string, args: unknown, online?: boolean): Promise<{
@@ -2614,6 +2699,20 @@ export declare const host: {
2614
2699
  clusterPostSpot(args: ClusterPostSpotRequest): Promise<ClusterReply>;
2615
2700
  /** Sends any one command line, in the operator's name. Needs `requiresClusterCommand` and extension API 10. */
2616
2701
  clusterSend(command: string): Promise<ClusterReply>;
2702
+ /**
2703
+ * Puts a station into the logging fields of the operation in front, as tapping its spot in the
2704
+ * Spots Panel does — call, frequency, band, mode and references; nothing else of `qso` is used.
2705
+ * With `operationUuid` (a panel's `operation.uuid`), only that operation's: `accepted` is false
2706
+ * when it is not the one in front, or none is open. It tunes the radio as a tap does, so it needs
2707
+ * `requiresRadioWrite`, and it is refused outside the extension's own panel `onEvent`. Extension
2708
+ * API 12.
2709
+ */
2710
+ suggestQso(args: {
2711
+ qso: SuggestedQso;
2712
+ operationUuid?: string;
2713
+ }): Promise<{
2714
+ accepted: boolean;
2715
+ }>;
2617
2716
  fetch(url: string, options?: FetchOptions): Promise<FetchResponse>;
2618
2717
  webSocket(url: string, options?: WebSocketOptions): ExtensionWebSocket;
2619
2718
  kvGet(key: string): Promise<JSONValue | null>;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { base64ToBytes, bytesToBase64 } from "./base64.js";
2
2
  import { adoptExtensionTimers, clearInterval, clearTimeout, setInterval, setTimeout } from "./timers.js";
3
+ const API_VERSION = 12;
3
4
  export * from "./types.js";
4
5
  export * from "./hookCategories.js";
5
6
  export * from "./panelScene.js";
@@ -227,6 +228,17 @@ const host = {
227
228
  async clusterSend(command) {
228
229
  return await hostCallFn()("clusterSend", { command });
229
230
  },
231
+ /**
232
+ * Puts a station into the logging fields of the operation in front, as tapping its spot in the
233
+ * Spots Panel does — call, frequency, band, mode and references; nothing else of `qso` is used.
234
+ * With `operationUuid` (a panel's `operation.uuid`), only that operation's: `accepted` is false
235
+ * when it is not the one in front, or none is open. It tunes the radio as a tap does, so it needs
236
+ * `requiresRadioWrite`, and it is refused outside the extension's own panel `onEvent`. Extension
237
+ * API 12.
238
+ */
239
+ async suggestQso(args) {
240
+ return await hostCallFn()("suggestQso", { ...args });
241
+ },
230
242
  async fetch(url, options) {
231
243
  return await hostCallFn()("fetch", { url, ...options });
232
244
  },
@@ -415,6 +427,7 @@ const host = {
415
427
  }
416
428
  };
417
429
  export {
430
+ API_VERSION,
418
431
  defineExtension,
419
432
  hooks,
420
433
  host
@@ -540,6 +540,13 @@ catalog's own bundle links (`…/api/v1/extensions/<key>/versions/<version>/bund
540
540
  take this verified path when they arrive as a deep link; every other host
541
541
  takes the plain one above.
542
542
 
543
+ An extension's screenshots — published beside its bundles, never inside them
544
+ (the tools README says how) — show as a strip of thumbnails on its row in the
545
+ operation sheet's activity search, on the consent screen of a catalog install,
546
+ and in an installed extension's details; a tap opens them full size. The app
547
+ loads them only from the catalog's own host: a listing naming any other is
548
+ shown without them.
549
+
543
550
  ### Release tracks
544
551
 
545
552
  A catalog channel — `stable`, `unstable` or `bleeding` — says how finished a
package/docs/hooks.md CHANGED
@@ -73,6 +73,17 @@ hook method receives `(args, ctx)` where `ctx: HookContext` is:
73
73
  reads above it is cheap enough for the lookup path; it resolves `[]`
74
74
  whenever the cache is cold or the host keeps no spots. Used by
75
75
  `spot-history` to pull a hunted park or summit into the QSO being logged.
76
+ * `host.suggestQso({qso, operationUuid?})` (extension API 12) — puts a
77
+ station into the logging fields of the operation in front, as tapping its
78
+ spot in the Spots Panel does: `qso.their.call`, and `freq`, `band`, `mode`
79
+ and `refs` where given; nothing else of `qso` reaches the fields. With
80
+ `operationUuid` — a panel's own `operation.uuid` — only that operation's
81
+ fields, so a panel never fills a log the operator is not looking at.
82
+ Answers `{accepted}`, false when no such operation is open. It tunes the
83
+ radio as a tap does, so it needs `requiresRadioWrite`, and it is refused
84
+ except from inside the extension's own panel `onEvent`: it answers the
85
+ operator clicking something, never a timer or a render, which would
86
+ overwrite what they are typing.
76
87
  * `getSpots?(options?)` — the whole spots board the host already holds,
77
88
  for a panel showing spots its own way. Like `getSpotsForCall` it reads the
78
89
  cache and never fetches, and resolves `[]` while the cache is cold. By
@@ -92,7 +103,12 @@ hook method receives `(args, ctx)` where `ctx: HookContext` is:
92
103
  ones the app already holds for the operation open on screen, so the call
93
104
  never waits on a scorer; another operation's spots, and that one's until
94
105
  its first verdicts land, come back unannotated rather than guessed.
95
- `annotate: false` returns the spots as their sources gave them.
106
+ From extension API 12 each also carries `display` — `icon` (an MDI or
107
+ `fa-` name), `iconColor` (`#RRGGBB`, where the activity declares one) and
108
+ `emoji` (the call's note emoji, where it has one): what the Spots Panel's row
109
+ shows beside the call, the activity's icon and tint for the spot's first
110
+ reference, else the source's own icon. `annotate: false` returns the spots
111
+ as their sources gave them.
96
112
  A panel's `'spots'` trigger fires whenever this answer may have moved: a
97
113
  refresh, new verdicts, the operator's spot filters or bands, or the radio's
98
114
  band or mode. A malformed or unknown option rejects the call. The CLI host
@@ -287,6 +303,14 @@ interface LookupServiceHook {
287
303
  unset or name itself; tagging one `'prefix'` gets its coordinate
288
304
  discarded.
289
305
 
306
+ - **A guess field of the wrong type is dropped.** The bridge enforces none of
307
+ `CallInfo`'s declared types, so the app normalizes the answer
308
+ (`normalizeLookupResult`, halo_core) before anything reads it: a field its
309
+ readers cannot use — a numeric `continent`, a boolean `emoji` — is dropped
310
+ and reported once by name, and the rest of the guess is still used. A
311
+ numeric string where a number is read (`cqZone: '05'`) is taken as that
312
+ number.
313
+
290
314
  Implemented by: `lookups` (key `core`). The merge algorithm itself
291
315
  (`mergeLookupIntoGuess`/`runLookupPipeline` in
292
316
  `extensions/core/lookups/src/mergeLookup.ts`) is a pure, host-independent
@@ -1146,10 +1170,10 @@ interface CommandHook {
1146
1170
  catalog?(ctx): Promise<CommandCatalogEntry[]>
1147
1171
  }
1148
1172
  // CommandInterpretation: {expectsParams?, mixedCase?, error?, describe?, confirm?, commands?: CommandAction[]}
1149
- // CommandCatalogEntry: {command, describe, params?, category?, needsOperation?, expectsParams?, suggest?}
1173
+ // CommandCatalogEntry: {command, describe, params?, category?, needsOperation?, needsTestData?, keywords?, expectsParams?, suggest?}
1150
1174
  // CommandAction (closed set): {setVfo?, setPower?, updateQso?, addQso?,
1151
1175
  // setKeyerSpeed?, showKeyer?, setEnterSends?, sendSpots?, autoRespot?, syncAll?, updateSettings?, updateSetting?, openSettings?,
1152
- // updateOperation?, markSegment?, findInLog?, setCallField?, setTime?, reloadDataFiles?, devNotice?, devStatus?,
1176
+ // updateOperation?, markSegment?, findInLog?, navigate?, setCallField?, setTime?, reloadDataFiles?, devNotice?, devStatus?,
1153
1177
  // devProgress?, devHammer?, devSplash?, devOnboard?, devTester?, devPopup?,
1154
1178
  // devTimeTravel?, toggleExperiment?}
1155
1179
  ```
@@ -1342,9 +1366,12 @@ Three things differ from `interpret`, all deliberate:
1342
1366
  - **Called once and cached**, not per keystroke — dropped when the runtime is
1343
1367
  replaced (an extension enabled/disabled) or the hook context is
1344
1368
  invalidated (locale change, since `describe` is localized by the hook).
1345
- - **Matching is host-side**, plain case-insensitive substring against
1346
- `command` and `describe`. Same rule `interpret`'s own matching follows: the
1347
- host never compiles extension-supplied patterns.
1369
+ - **Matching is host-side**, case-insensitive, at the start of a word in
1370
+ `command` or in `keywords` — never mid-word, and `describe` is never
1371
+ searched. Every word that should find a command belongs in its
1372
+ `keywords`: the aliases it accepts when typed, and plain words for what it
1373
+ does. Same rule `interpret`'s own matching follows: the host never
1374
+ compiles extension-supplied patterns.
1348
1375
  - **List commands, not variants.** `radio-commands` advertises one BAND
1349
1376
  entry, not fifteen; `call-notes` advertises `//`, not every key the user
1350
1377
  has defined.
@@ -1876,9 +1903,10 @@ drawing part of it.
1876
1903
  | `width`, `height` | The artwork's viewBox, in scene units. Size it to `environment.width`/`height` and a scene unit is a logical pixel. |
1877
1904
  | `values` | `{name: number}`: what bindings, numeric text and numeric controls read. At most 256. |
1878
1905
  | `strings` | `{name: string}`: what text fields and choices hold. At most 256, each up to 4096 characters. A name is in `values` or `strings`, never both. |
1879
- | `layers` | Painted in order, each over the ones before: `svg` artwork or `text`. At most 128, and 1 MiB of artwork and text in all. |
1880
- | `controls` | At most 64. Drawn or native; see below. |
1906
+ | `layers` | Painted in order, each over the ones before: `svg` artwork, `text`, or an `icon`. At most 512 (128 before extension API 12), and 1 MiB of artwork and text in all. |
1907
+ | `controls` | At most 256 (64 before extension API 12). Drawn or native; see below. |
1881
1908
  | `layout` | Optional. Where rect-less native controls and the artwork go; omitted, the artwork fills the pane. |
1909
+ | `scroll` | Optional, `{axis?, value, event?}` (extension API 12): a scene longer than its pane along `axis` (`y` by default), scrolled natively by the app — wheel, trackpad and drag, a drag begun on a button included — with no call to the extension. It fills the pane across and runs on past it. The position, in scene units, is written to the scene value `value`; a render that changes that value moves the view there, one that leaves it alone leaves the operator's scroll alone. `event` is sent as a `commit`, the position as `value`, once a scroll comes to rest. Layers and controls with `fixed: true` stay put over the pane, in pane coordinates. A drag along the scroll on a slider or knob that scrolls with the content is the scroll's: make one that must be dragged `fixed`. Not with `layout`. |
1882
1910
  | `radio` | Optional, `{node?}`: the host feeds that radio's live readings into the reserved `radio.*` values. `node` is an id from `listRadios`; omitted, the logging radio. See *Radio values*. |
1883
1911
 
1884
1912
  **Controls.** Every control has an `id` and a `label`, which is its accessible
@@ -1889,8 +1917,8 @@ the extension. `value` names what it shows and sets.
1889
1917
  | Kind | Binds to | Sends | Also takes |
1890
1918
  |---|---|---|---|
1891
1919
  | `button` | — | `activate` | `menu` |
1892
- | `slider` | number | `commit` | `min`, `max`, `step`, `discrete`, `hover`, `valueLabels`, `continuous` |
1893
- | `knob` | number | `commit` | `min`, `max`, `step`, `sensitivity`, `valueLabels`, `continuous`, `menu`, a second axis (`valueY`, `eventY`, `minY`, `maxY`, `stepY`, `sensitivityY`) |
1920
+ | `slider` | number | `commit` | `min`, `max`, `step`, `discrete`, `hover`, `valueLabels`, `continuous`, `scrollThrough` |
1921
+ | `knob` | number | `commit` | `min`, `max`, `step`, `sensitivity`, `invertDrag`, `scrollThrough`, `valueLabels`, `continuous`, `menu`, a second axis (`valueY`, `eventY`, `minY`, `maxY`, `stepY`, `sensitivityY`) |
1894
1922
  | `nativeButton` | — | `activate` | `variant` (`filled`, `tonal`, `outlined`, `text`), `icon`, `menu`; needs `event` or `menu` |
1895
1923
  | `nativeSlider` | number | `commit` | `min`, `max`, `step`, `valueLabels`, `continuous` |
1896
1924
  | `nativeTextField` | string | `commit` on Enter, or on leaving it after typing | `placeholder`, `continuous` |
@@ -1901,6 +1929,7 @@ the extension. `value` names what it shows and sets.
1901
1929
  | `nativeChip` | number, 1 or 0; or none | `commit`; `activate` without a value | `icon`; needs `event` without a value |
1902
1930
  | `nativeText` | number, string, or none | nothing | `style` (a typography role), `align` (`start`, `center`, `end`) |
1903
1931
  | `spectrum` | — | `commit` with the frequency clicked; `activate` from an annotation | see *Spectrum* below |
1932
+ | `zoom` | — | `commit` with the factor, on release | `min`, `max` (the factors allowed); only in a scene with `scroll`; extension API 12 |
1904
1933
 
1905
1934
  The three drawn kinds are invisible hit areas over artwork the extension paints;
1906
1935
  the native kinds are Material widgets the app paints and themes. Every control
@@ -1912,12 +1941,37 @@ may also take:
1912
1941
  - `after`: with a rect, the layer it paints directly after.
1913
1942
  - `opacity`: 0 to 1, fixed or bound like a layer's. At 0 the control is gone.
1914
1943
  - `disabled`: shown, but no click, drag, key or screen-reader adjustment acts on it.
1944
+ - `hoverSet` (drawn kinds only; extension API 12): `{value, to}`. While the
1945
+ pointer is over the control, the scene value `value` reads `to` — set by the
1946
+ host, with no call to the extension — and leaving puts back the scene's own
1947
+ value. A layer bound to it lights up under the pointer, and several controls
1948
+ naming one value with different `to`s light up one thing each. It survives
1949
+ renders while the pointer stays.
1915
1950
 
1916
1951
  `options` is `[{label, value}]`, up to 64, with each `value` unique. A choice's
1917
1952
  string is `""` or one of them; a `multi` choice holds its values joined with
1918
1953
  commas in option order, so no option may contain one. `menu` is
1919
1954
  `[{label, event}]`, up to 32: picking an item sends its `event` as the action.
1920
1955
 
1956
+ A `zoom` strip is drawn by nothing: it claims a drag that begins on it along a scrolling
1957
+ scene's axis. Away from the pane's middle zooms in and toward it zooms out, by the ratio of
1958
+ the two distances from the middle, clamped to `min`…`max`. While the drag lasts the app moves
1959
+ each layer and control to where the zoom puts its middle, keeping its size, so text and dots
1960
+ stay as drawn and only spread apart or close in; a layer with `stretch: true` — a background,
1961
+ a line the length of the scene — scales along the scroll instead. That preview holds until
1962
+ the render that answers the `commit` arrives, so a scene meant to zoom draws its pieces as
1963
+ layers of their own rather than as one picture. A tap on the strip still reaches the button there, and a drag that starts off it
1964
+ still scrolls.
1965
+
1966
+ A scroll steps a slider or knob by its `step`, down and left lowering it; a one-axis
1967
+ control takes a sideways scroll by its own direction. A slider or knob with
1968
+ `scrollThrough` (API 12) also takes the scrolls landing on drawn buttons over it, so labels
1969
+ drawn over a knob that scrolls the artwork do not stop the scroll; anything else in the
1970
+ way — a native control, a `blocksPointer` layer, another slider or knob — keeps the scroll
1971
+ from it.
1972
+ A knob with `invertDrag` (API 12) moves the other way to a drag, as content held under
1973
+ the finger does — what a knob that scrolls the artwork wants — while its scroll is unchanged.
1974
+
1921
1975
  A knob with `valueY` sets two numbers from one patch of artwork — a passband's
1922
1976
  shift across and its width up, say. Horizontal dragging, horizontal scrolling
1923
1977
  and the left/right keys set `value`; vertical ones and the up/down keys set
@@ -2029,9 +2083,8 @@ control faded in over the artwork, and a click-through layer painted over it.
2029
2083
  #### Spectrum
2030
2084
 
2031
2085
  A `spectrum` control is a radio's panadapter and waterfall, drawn by the host
2032
- from sweeps that never reach the extension, on a host with the panadapter
2033
- enabled. It is **extension API 6**, as is `host.setRadioSpectrum`: a bundle
2034
- using either declares `"api": 6`, and an app speaking less refuses it at install
2086
+ from sweeps that never reach the extension. It is **extension API 6**, as is
2087
+ `host.setRadioSpectrum`: a bundle using either declares `"api": 6`, and an app speaking less refuses it at install
2035
2088
  rather than failing to render the scene. It needs `requiresRadioRead`; without it, or on a host or radio
2036
2089
  without a spectrum, the control says why instead. It is placed like a native
2037
2090
  control and takes the fields of `PanelSceneSpectrum` in the SDK:
@@ -2288,7 +2341,7 @@ protocol's (CI-V bytes that are not whole frames), `invalid`. A refusal heard
2288
2341
  within a second of a raw command on a protocol whose refusals name no command
2289
2342
  (`?;`, rigctld's `RPRT`) is taken as the raw command's, not counted against the
2290
2343
  app's own reads.
2291
- On a host with the panadapter enabled, `readRadio()` carries `spectrum`: null
2344
+ `readRadio()` carries `spectrum` (absent on an app without a panadapter): null
2292
2345
  for a radio without a scope, otherwise its settings as the radio reports them
2293
2346
  (`mode`, `spanHz`, `lowHz`/`highHz`, `refLevelDb`, `speed`), what it accepts
2294
2347
  (`spansHz`, `refMinDb`…`refStepDb`, `modes`, `speeds`) and `problem`, why it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.12.14",
3
+ "version": "1.12.0",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",
@@ -4,9 +4,17 @@ The radio's panadapter and waterfall in a scene panel, with its span, reference
4
4
  level and sweep speed, a local contrast control, and click-to-tune. The host
5
5
  draws the sweeps in the `spectrum` control; they never reach the extension.
6
6
 
7
- Needs a host with the panadapter enabled (`EXP FALL`) and a radio whose CAT
8
- protocol carries its scope: an Icom IC-705/7300/7610 (at 115200 bps on a serial
9
- line), or a FlexRadio, which is mirrored through a DAX IQ channel.
7
+ Needs a supported radio:
8
+
9
+ - Icom IC-705/7300/7610, at 115200 bps when using a serial line.
10
+ - FlexRadio, mirrored through a DAX IQ channel.
11
+ - Elecraft K4 over Ethernet, with a separate local TCP spectrum connection on
12
+ port 9201 alongside CAT. It displays the main panadapter with local zoom,
13
+ reference and speed controls. Levels are displayed as received, without an
14
+ RF calibration claim.
15
+
16
+ The panel shows only the controls each radio supports; contrast, averaging,
17
+ frame rate and waterfall rows remain local.
10
18
 
11
19
  In this repository:
12
20
 
@@ -14,3 +22,8 @@ In this repository:
14
22
  node build.mjs
15
23
  node ../../tools/h2kext-pack.mjs build -o /tmp/k2hrc-panafall.h2kext
16
24
  ```
25
+
26
+ Samples are not included in the app's regular extension build. Install the
27
+ resulting `.h2kext` through **Settings → Extensions & Features → Install from
28
+ file…**, then add its **Panadapter** panel to an operation. The host must have
29
+ the matching radio support.
@@ -42,15 +42,25 @@ const spanLabel = (hz: number) => `±${hz >= 1000 ? `${hz / 1000} kHz` : `${hz}
42
42
  export function scene(radio: PanelRadioState | null, display: Display, step = "100"): PanelScene {
43
43
  const spectrum: PanelRadioSpectrum | null | undefined = radio?.spectrum
44
44
  const knobs = spectrum && !spectrum.problem
45
+ const hasSpan = knobs && spectrum.spansHz.length > 0
46
+ const hasSpeed = knobs && spectrum.speeds.length > 0
47
+ const hasRef =
48
+ knobs &&
49
+ Number.isFinite(spectrum.refMinDb) &&
50
+ Number.isFinite(spectrum.refMaxDb) &&
51
+ Number.isFinite(spectrum.refStepDb) &&
52
+ spectrum.refMinDb < spectrum.refMaxDb &&
53
+ spectrum.refStepDb > 0
54
+ const ref = hasRef ? Math.max(spectrum.refMinDb, Math.min(spectrum.refMaxDb, spectrum.refLevelDb ?? spectrum.refMinDb)) : 0
45
55
  const band = passband(radio?.mode)
46
56
  return {
47
57
  version: 1,
48
58
  width: 600,
49
59
  height: 24,
50
- values: { floor: 0, average: 40, fps: 15, rows: 10, hoverHz: 0, hoverDb: 0, ref: spectrum?.refLevelDb ?? 0 },
60
+ values: { floor: 0, average: 40, fps: 15, rows: 10, hoverHz: 0, hoverDb: 0, ref },
51
61
  strings: {
52
- span: spectrum?.spanHz != null ? String(spectrum.spanHz) : "",
53
- speed: spectrum?.speed ?? "",
62
+ span: hasSpan && spectrum.spanHz != null && spectrum.spansHz.includes(spectrum.spanHz) ? String(spectrum.spanHz) : "",
63
+ speed: hasSpeed && spectrum.speed != null && spectrum.speeds.includes(spectrum.speed) ? spectrum.speed : "",
54
64
  display,
55
65
  step,
56
66
  },
@@ -65,7 +75,7 @@ export function scene(radio: PanelRadioState | null, display: Display, step = "1
65
75
  },
66
76
  ],
67
77
  controls: [
68
- ...(knobs
78
+ ...(hasSpan
69
79
  ? [
70
80
  {
71
81
  id: "span",
@@ -75,6 +85,10 @@ export function scene(radio: PanelRadioState | null, display: Display, step = "1
75
85
  event: "span",
76
86
  options: spectrum.spansHz.map((hz) => ({ label: spanLabel(hz), value: String(hz) })),
77
87
  },
88
+ ]
89
+ : []),
90
+ ...(hasRef
91
+ ? [
78
92
  {
79
93
  id: "ref",
80
94
  label: "Reference level",
@@ -85,6 +99,10 @@ export function scene(radio: PanelRadioState | null, display: Display, step = "1
85
99
  max: spectrum.refMaxDb,
86
100
  step: spectrum.refStepDb,
87
101
  },
102
+ ]
103
+ : []),
104
+ ...(hasSpeed
105
+ ? [
88
106
  {
89
107
  id: "speed",
90
108
  label: "Speed",
@@ -100,7 +118,7 @@ export function scene(radio: PanelRadioState | null, display: Display, step = "1
100
118
  { id: "fps", label: "Panadapter FPS", kind: "nativeSlider" as const, value: "fps", min: 1, max: 30, step: 1 },
101
119
  { id: "rows", label: "Waterfall rows per second", kind: "nativeSlider" as const, value: "rows", min: 1, max: 30, step: 1 },
102
120
  // A control the layout does not place fails the scene: Ref's label goes with its slider.
103
- ...[...(knobs ? ["Ref"] : []), "Contrast", "Avg", "FPS", "Rows"].map((text) => ({
121
+ ...[...(hasRef ? ["Ref"] : []), "Contrast", "Avg", "FPS", "Rows"].map((text) => ({
104
122
  id: `label-${text}`,
105
123
  label: text,
106
124
  kind: "nativeText" as const,
@@ -179,7 +197,9 @@ export function scene(radio: PanelRadioState | null, display: Display, step = "1
179
197
  column: [
180
198
  {
181
199
  wrap: [
182
- ...(knobs ? [{ control: "span" }, labeled("Ref", "ref"), { control: "speed" }] : []),
200
+ ...(hasSpan ? [{ control: "span" }] : []),
201
+ ...(hasRef ? [labeled("Ref", "ref")] : []),
202
+ ...(hasSpeed ? [{ control: "speed" }] : []),
183
203
  labeled("Contrast", "floor"),
184
204
  labeled("Avg", "average"),
185
205
  labeled("FPS", "fps"),