@ham2k/extension-sdk 0.12.2 → 0.12.5

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/dxcc.js CHANGED
@@ -1,16 +1,22 @@
1
1
  import { parseCallsign } from "@ham2k/lib-callsigns";
2
- import { annotateFromCountryFile, useBuiltinCountryFile } from "@ham2k/lib-country-files";
2
+ import { annotateFromCountryFile, BIGCTY, useBuiltinCountryFile } from "@ham2k/lib-country-files";
3
3
  import { DXCC_BY_CODE } from "@ham2k/lib-dxcc-data";
4
4
  useBuiltinCountryFile();
5
5
  function annotateCallAgainstCountryFile(call, options) {
6
6
  const annotated = annotateFromCountryFile(parseCallsign(call), options);
7
- return annotated.tz ? { ...annotated, tz: ordinarySignTz(annotated.tz) } : annotated;
7
+ if (!WEST_POSITIVE) return annotated;
8
+ return {
9
+ ...annotated,
10
+ ...typeof annotated.lon === "number" ? { lon: 0 - annotated.lon } : {},
11
+ ...annotated.tz ? { tz: flippedTz(annotated.tz) } : {}
12
+ };
8
13
  }
9
- function ordinarySignTz(tz) {
14
+ const WEST_POSITIVE = (BIGCTY.entities.K?.lon ?? 0) > 0;
15
+ function flippedTz(tz) {
10
16
  const match = /^GMT([+-]?\d+(?:\.\d+)?)$/.exec(tz);
11
17
  if (!match) return tz;
12
- const offset = -Number(match[1]);
13
- return offset > 0 ? `GMT+${offset}` : `GMT${offset === 0 ? 0 : offset}`;
18
+ const offset = 0 - Number(match[1]);
19
+ return offset > 0 ? `GMT+${offset}` : `GMT${offset}`;
14
20
  }
15
21
  function entityPrefixForCall(call) {
16
22
  if (!call) return void 0;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.12.2
1
+ // @ham2k/extension-sdk 0.12.5
2
2
  /** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface PanelScene {
4
4
  version: 1;
@@ -97,11 +97,193 @@ export interface PanelSceneLayer {
97
97
  * - `nativeSwitch`, `nativeCheckbox`: a number, 1 or 0. Shows `label` beside it.
98
98
  * - `nativeChip`: with `value`, a toggle (1 or 0); without, an action that sends `activate`.
99
99
  * - `nativeText`: shows `label`, or its bound value. Sends nothing.
100
+ * - `spectrum`: the radio's panadapter and waterfall, drawn by the host from sweeps that never
101
+ * reach the extension. Needs `requiresRadioRead`; a host without the feature, or a radio
102
+ * without a scope, shows why instead. A click sends `commit` with the frequency as `value`.
103
+ * Takes no `value`; see `PanelSceneSpectrum` for the rest. Extension API 6: declare `"api": 6`.
100
104
  *
101
105
  * `label` is every control's accessible name. There are no tooltips.
102
106
  */
103
- export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText";
104
- export interface PanelSceneControl {
107
+ export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText" | "spectrum";
108
+ /** A literal, or the name of a scene value — which a slider with no `event` retunes with no bridge call. */
109
+ export type PanelSceneSpectrumNumber = number | string;
110
+ /**
111
+ * A `spectrum` control's display. All of it happens on the host after the sweep arrives, so it
112
+ * means the same on every radio; the radio's own scope (span, reference level, sweep speed) is
113
+ * `host.setRadioSpectrum`'s. Colors are `#RRGGBB`, or `#AARRGGBB` with transparency. Defaults
114
+ * follow the app's theme.
115
+ */
116
+ export interface PanelSceneSpectrum {
117
+ /** A radio's id from `listRadios`. Omitted, the designated radio. */
118
+ radio?: string;
119
+ /** Default `both`. */
120
+ display?: "both" | "pan" | "waterfall";
121
+ /**
122
+ * Which way the waterfall moves; the panadapter sits on the side it comes from. Frequency rises
123
+ * left to right for `down` (the default) and `up`, bottom to top for `right` and `left`. Text
124
+ * stays upright; offsets and `corner`s are measured as if flowing down.
125
+ */
126
+ flow?: "down" | "up" | "right" | "left";
127
+ /** The panadapter's share of the height with `both`, 0.1–0.9. Default 0.4. */
128
+ split?: number;
129
+ /** dB added to the radio's own floor. */
130
+ floor?: PanelSceneSpectrumNumber;
131
+ /** The panadapter's height in dB. Omitted, the radio's. */
132
+ range?: PanelSceneSpectrumNumber;
133
+ /** Written as the pointer moves: the frequency and level under it. */
134
+ hoverHz?: string;
135
+ hoverDb?: string;
136
+ /** 0–99: the share of the trace each sweep keeps from the last. Default 40. The waterfall stays raw. */
137
+ average?: PanelSceneSpectrumNumber;
138
+ /** The most times a second the panadapter redraws, 1–60. Default 15. The waterfall keeps its own pace. */
139
+ fps?: PanelSceneSpectrumNumber;
140
+ /** Waterfall rows a second, 0.1–60; each row keeps the loudest reading since the last. Omitted, one per sweep. */
141
+ waterfallSpeed?: PanelSceneSpectrumNumber;
142
+ /** The waterfall's own dB window, like `floor` and `range`. Omitted, the panadapter's. */
143
+ waterfallFloor?: PanelSceneSpectrumNumber;
144
+ waterfallRange?: PanelSceneSpectrumNumber;
145
+ /** Default `classic`. */
146
+ palette?: "classic" | "gray" | "heat";
147
+ /** 2 to 8 stops, lowest level first, in place of `palette`. */
148
+ waterfallColors?: string[];
149
+ traceColor?: string;
150
+ /** 0.25–8. Default 1. */
151
+ traceWidth?: PanelSceneSpectrumNumber;
152
+ /** Under the trace. With `fillColorBottom`, a gradient from the top of the panadapter to its bottom. */
153
+ fillColor?: string;
154
+ fillColorBottom?: string;
155
+ backgroundColor?: string;
156
+ /** The panadapter's dB and frequency lines. */
157
+ gridColor?: string;
158
+ /** The frequency lines carried down through the waterfall. Omitted, none. */
159
+ waterfallGridColor?: string;
160
+ /** A click tunes to the nearest multiple of this many Hz. Omitted, wherever it lands. */
161
+ tuneStep?: PanelSceneSpectrumNumber;
162
+ /** Dashes the panadapter's grid lines. */
163
+ gridDashed?: boolean;
164
+ /** The gap between the panadapter and the waterfall, with frequency labels and ticks. `true` takes the defaults. */
165
+ scale?: boolean | PanelSceneSpectrumScale;
166
+ /** The tuned-frequency line, styled per part; a part it leaves out has none. Omitted, the theme's on both. */
167
+ center?: {
168
+ pan?: PanelSceneSpectrumStyle;
169
+ waterfall?: PanelSceneSpectrumStyle;
170
+ };
171
+ /** Up to 16 lines and areas at Hz offsets — a passband, a CW offset. */
172
+ overlays?: PanelSceneSpectrumOverlay[];
173
+ /** Up to 64 pieces of text at frequencies — a spot, a net, a beacon. */
174
+ annotations?: PanelSceneSpectrumAnnotation[];
175
+ }
176
+ /**
177
+ * The gap sits between the panadapter and the waterfall — above the waterfall with no panadapter,
178
+ * below the panadapter with no waterfall. Labels and ticks are drawn over the whole control, so an
179
+ * `offset` can place them anywhere: it is pixels from the base of the panadapter (the top of the
180
+ * waterfall where there is none), negative rising into the panadapter.
181
+ */
182
+ export interface PanelSceneSpectrumScale {
183
+ /** Pixels, 0–200. Default 20. */
184
+ gap?: PanelSceneSpectrumNumber;
185
+ background?: string;
186
+ borderTop?: {
187
+ color?: string;
188
+ width?: PanelSceneSpectrumNumber;
189
+ };
190
+ borderBottom?: {
191
+ color?: string;
192
+ width?: PanelSceneSpectrumNumber;
193
+ };
194
+ /** On by default; `false` hides them. */
195
+ labels?: boolean | {
196
+ /** Hz between labels. Omitted, the gridlines' step. */
197
+ interval?: PanelSceneSpectrumNumber;
198
+ fontFamily?: string;
199
+ size?: PanelSceneSpectrumNumber;
200
+ /** 100–900. */
201
+ weight?: PanelSceneSpectrumNumber;
202
+ italic?: boolean;
203
+ color?: string;
204
+ /** To the top of the text. Omitted, centered in the gap. */
205
+ offset?: PanelSceneSpectrumNumber;
206
+ };
207
+ /** Off by default. */
208
+ ticks?: boolean | {
209
+ /** Hz between ticks. Omitted, the labels'. */
210
+ interval?: PanelSceneSpectrumNumber;
211
+ color?: string;
212
+ width?: PanelSceneSpectrumNumber;
213
+ /** To the top of the tick. Default 0. */
214
+ offset?: PanelSceneSpectrumNumber;
215
+ /** Default 4. */
216
+ length?: PanelSceneSpectrumNumber;
217
+ };
218
+ }
219
+ /**
220
+ * Text at a frequency, drawn by the host. With an `event`, a click sends `activate` with the
221
+ * frequency as `value` and this `id` as `annotation`, and does not tune; without one, a click
222
+ * on it tunes like anywhere else.
223
+ */
224
+ export interface PanelSceneSpectrumAnnotation {
225
+ /** Unique within the control. */
226
+ id: string;
227
+ /** At most 256 characters. */
228
+ text: string;
229
+ /** A frequency, or an offset from the tuned frequency or the sweep's middle with `anchor`. */
230
+ hz: PanelSceneSpectrumNumber;
231
+ /** Default `absolute`. */
232
+ anchor?: "vfo" | "center" | "absolute";
233
+ /** Default `pan`. */
234
+ part?: "pan" | "waterfall";
235
+ /** Pixels from the panadapter's bottom up to the text's, or from the waterfall's top down to the text's. Default 4. */
236
+ offset?: PanelSceneSpectrumNumber;
237
+ /** Which end of the text sits at `hz`. Default `center`. */
238
+ align?: "start" | "center" | "end";
239
+ /**
240
+ * The point the box makes at `hz`: a drawn-out corner for `start`/`end`, a notch for `center`.
241
+ * With the default `corner`, its tip sits at `offset`; turned the other way, the box's edge
242
+ * does. Shown by default (size 4, pointing toward the panadapter's bottom
243
+ * or the waterfall's top); `false` or `show: false` hides it.
244
+ */
245
+ pointer?: boolean | {
246
+ show?: boolean;
247
+ size?: PanelSceneSpectrumNumber;
248
+ corner?: "top" | "bottom";
249
+ };
250
+ fontFamily?: string;
251
+ size?: PanelSceneSpectrumNumber;
252
+ weight?: PanelSceneSpectrumNumber;
253
+ italic?: boolean;
254
+ color?: string;
255
+ background?: string;
256
+ borderColor?: string;
257
+ borderWidth?: PanelSceneSpectrumNumber;
258
+ /** Default 2. */
259
+ padding?: PanelSceneSpectrumNumber;
260
+ /** Default 2. */
261
+ radius?: PanelSceneSpectrumNumber;
262
+ event?: string;
263
+ }
264
+ /** `color` and `width` draw a line, or an area's edges; `fill` fills an area. */
265
+ export interface PanelSceneSpectrumStyle {
266
+ color?: string;
267
+ width?: PanelSceneSpectrumNumber;
268
+ fill?: string;
269
+ }
270
+ /**
271
+ * A line at `at`, or an area between `from` and `to`, in Hz from the tuned frequency — from the
272
+ * middle of the sweep with `anchor: "center"` (a Flex's panadapter is not centered on its slice),
273
+ * or as frequencies with `anchor: "absolute"` (a band edge, a segment). On the panadapter an area
274
+ * draws behind the trace and a line in front of it unless `behind` — which absolute ones default to.
275
+ * On the waterfall all of it draws over the rows. A part with no style is not drawn on.
276
+ */
277
+ export interface PanelSceneSpectrumOverlay {
278
+ at?: PanelSceneSpectrumNumber;
279
+ from?: PanelSceneSpectrumNumber;
280
+ to?: PanelSceneSpectrumNumber;
281
+ anchor?: "vfo" | "center" | "absolute";
282
+ behind?: boolean;
283
+ pan?: PanelSceneSpectrumStyle;
284
+ waterfall?: PanelSceneSpectrumStyle;
285
+ }
286
+ export interface PanelSceneControl extends PanelSceneSpectrum {
105
287
  id: string;
106
288
  label: string;
107
289
  kind: PanelSceneControlKind;
@@ -177,6 +359,8 @@ export interface PanelSceneEvent {
177
359
  value?: number;
178
360
  /** The control's string, for a control bound to `strings`. */
179
361
  text?: string;
362
+ /** The `id` of the spectrum annotation clicked; `value` is its frequency. */
363
+ annotation?: string;
180
364
  }
181
365
  /** Patch only named scene values and strings. The host ignores stale responses. */
182
366
  export interface PanelSceneEventResult {
@@ -492,6 +676,7 @@ export interface Ref {
492
676
  program?: string;
493
677
  activationRadiusInMeters?: number;
494
678
  activationRadiusInFeet?: number;
679
+ largeArea?: boolean;
495
680
  retired?: boolean;
496
681
  for?: "operation" | "qso";
497
682
  [key: string]: JSONValue | undefined;
@@ -1544,6 +1729,40 @@ export interface PanelRadioState {
1544
1729
  transmitting: boolean | null;
1545
1730
  meters: Partial<Record<"signalDbm" | "powerOut" | "swr" | "alc" | "supplyVolts", number>>;
1546
1731
  canTune: boolean;
1732
+ /**
1733
+ * The radio's scope, on hosts with the panadapter enabled: absent there otherwise, and null
1734
+ * for a radio without one. The sweeps are never sent — a `spectrum` scene control draws them.
1735
+ */
1736
+ spectrum?: PanelRadioSpectrum | null;
1737
+ }
1738
+ export type PanelRadioSpectrumSpeed = "slow" | "mid" | "fast";
1739
+ export interface PanelRadioSpectrum {
1740
+ /** Why nothing can be drawn, in the operator's language, or null. */
1741
+ problem: string | null;
1742
+ mode: "center" | "fixed" | null;
1743
+ /** Half the width shown while centered — what the radio's menu shows as ±. */
1744
+ spanHz: number | null;
1745
+ /** The edges while fixed. */
1746
+ lowHz: number | null;
1747
+ highHz: number | null;
1748
+ refLevelDb: number | null;
1749
+ speed: PanelRadioSpectrumSpeed | null;
1750
+ /** What `setRadioSpectrum` accepts. A span between two is snapped to the nearer. */
1751
+ spansHz: number[];
1752
+ refMinDb: number;
1753
+ refMaxDb: number;
1754
+ refStepDb: number;
1755
+ modes: ("center" | "fixed")[];
1756
+ speeds: PanelRadioSpectrumSpeed[];
1757
+ }
1758
+ export interface PanelRadioSpectrumSet {
1759
+ /** Reject if this identity no longer matches the selected radio. */
1760
+ id: string;
1761
+ selection?: string;
1762
+ mode?: "center" | "fixed";
1763
+ spanHz?: number;
1764
+ refLevelDb?: number;
1765
+ speed?: PanelRadioSpectrumSpeed;
1547
1766
  }
1548
1767
  export interface PanelRadioTune {
1549
1768
  /** Reject if this identity no longer matches the selected radio. */
@@ -1556,7 +1775,8 @@ export interface PanelRadioTune {
1556
1775
  export interface PanelRadioTuneResult {
1557
1776
  /** Accepted by the host; confirmation still comes from reported state. */
1558
1777
  accepted: boolean;
1559
- reason?: "changed" | "disconnected" | "transmitting" | "invalid";
1778
+ /** `stopped`: a scope change while no display has the scope running — nothing was sent. */
1779
+ reason?: "changed" | "disconnected" | "transmitting" | "invalid" | "stopped";
1560
1780
  state: PanelRadioState | null;
1561
1781
  }
1562
1782
  export interface PanelRenderArgs {
@@ -1650,7 +1870,8 @@ export interface ParsedCallsign {
1650
1870
  indicators?: string[];
1651
1871
  ssid?: string;
1652
1872
  }
1653
- export type CombinedCallInfo = ParsedCallsign & {
1873
+ export type CombinedCallInfo = Omit<ParsedCallsign, "call"> & {
1874
+ call?: string;
1654
1875
  isoPrefix?: string;
1655
1876
  dxccCode?: number;
1656
1877
  regionCode?: string;
@@ -1998,6 +2219,8 @@ export declare const host: {
1998
2219
  connected: boolean;
1999
2220
  }): Promise<PanelRadioTuneResult>;
2000
2221
  tuneRadio(args: PanelRadioTune): Promise<PanelRadioTuneResult>;
2222
+ /** Changes the radio's scope. Needs `requiresRadioWrite` and extension API 6; the result's state reports what the radio took. */
2223
+ setRadioSpectrum(args: PanelRadioSpectrumSet): Promise<PanelRadioTuneResult>;
2001
2224
  fetch(url: string, options?: FetchOptions): Promise<FetchResponse>;
2002
2225
  webSocket(url: string, options?: WebSocketOptions): ExtensionWebSocket;
2003
2226
  kvGet(key: string): Promise<JSONValue | null>;
package/dist/index.js CHANGED
@@ -172,6 +172,10 @@ const host = {
172
172
  async tuneRadio(args) {
173
173
  return await hostCallFn()("tuneRadio", { ...args });
174
174
  },
175
+ /** Changes the radio's scope. Needs `requiresRadioWrite` and extension API 6; the result's state reports what the radio took. */
176
+ async setRadioSpectrum(args) {
177
+ return await hostCallFn()("setRadioSpectrum", { ...args });
178
+ },
175
179
  async fetch(url, options) {
176
180
  return await hostCallFn()("fetch", { url, ...options });
177
181
  },
@@ -262,7 +262,7 @@ It reports every problem at once rather than one per run:
262
262
  h2kext-pack: ./build is not ready to package:
263
263
  manifest.json: missing required field 'name'
264
264
  manifest.json: key 'my-clock' must start with your callsign — 'my' is not one (e.g. ki2d-my-clock)
265
- manifest.json: missing 'api' — declare the extension API this was built against (currently 5)
265
+ manifest.json: missing 'api' — declare the extension API this was built against (currently 6)
266
266
  ```
267
267
 
268
268
  `--force-name` waives the naming convention below. It exists for Ham2K's own
package/docs/hooks.md CHANGED
@@ -247,14 +247,17 @@ interface LookupServiceHook {
247
247
  carried one (`preferStationValues`) — only while the guess's scope is
248
248
  `qth`, since on a portable or prefixed contact a source describes the
249
249
  home QTH. `tz` is a UTC offset with its ordinary sign, `"GMT-5"` being
250
- five hours behind; the SDK's `annotateCallAgainstCountryFile` flips the
251
- country file's west-positive offsets to match.
250
+ five hours behind. lib-country-files states it that way from 1.1; the
251
+ SDK's `annotateCallAgainstCountryFile` answers with the ordinary signs
252
+ whichever version the app ships, flipping an older one's west-positive
253
+ `tz` and `lon`.
252
254
  - **`locSource: 'prefix'` is reserved**, and a `lat`/`lon` merged under it is
253
255
  never read as a coordinate (`locationFromGuess`, halo_core). It marks
254
- lib-country-files' DXCC entity centroid, whose longitude follows cty.dat's
255
- west-positive convention — the opposite of every other coordinate here. A
256
- source reporting a real position must leave `locSource` unset or name
257
- itself; tagging one `'prefix'` gets its coordinate discarded.
256
+ lib-country-files' DXCC entity centroid, which synced records carry signed
257
+ either way — west-positive from versions before 1.1 — with nothing to tell
258
+ the two apart. A source reporting a real position must leave `locSource`
259
+ unset or name itself; tagging one `'prefix'` gets its coordinate
260
+ discarded.
258
261
 
259
262
  Implemented by: `lookups` (key `core`). The merge algorithm itself
260
263
  (`mergeLookupIntoGuess`/`runLookupPipeline` in
@@ -441,7 +444,7 @@ interface RefHandlerHook {
441
444
  geojsonUrlForRef?(args: { ref: Ref }, ctx): Promise<RefGeojson | null>
442
445
  }
443
446
  // Ref: {type, ref?, name?, label?, shortLabel?, grid?, lat?, lon?, location?, program?,
444
- // activationRadiusInMeters?, activationRadiusInFeet?, ...}
447
+ // activationRadiusInMeters?, activationRadiusInFeet?, largeArea?, ...}
445
448
  // TitleSuggestion: {title?, for?, at?, subtitle?, priority?} — a fragment of
446
449
  // the operation's title/subtitle sentence (ham2k/halo-dist#174). Only
447
450
  // return one for the ref type that means "this operation is happening AT/FOR
@@ -558,6 +561,15 @@ and is not offered for an outline report. Programs built on the SDK's
558
561
  `referenceActivity` factory get this from one `activationRadius` config line —
559
562
  a value, or a function of the list row for rules that vary.
560
563
 
564
+ **A reference too large to stand for one place says so.** An operation with
565
+ no location of its own takes the first of its references that has one
566
+ (docs/design/locations.md), which is right for a summit or a park a few
567
+ kilometres across and wrong for a 3,500 km trail. A reference whose point is
568
+ representative rather than where an activator stands — POTA's parks that span
569
+ several states — carries `largeArea: true`, and that reference never places
570
+ the operation. Nothing else reads it: the maps draw it, and the nearby search
571
+ finds it, exactly as before.
572
+
561
573
  `suggestOperationTitle` lets an activity build a natural-language operation
562
574
  title/subtitle from its refs — "at Yosemite NP" (POTA), "for Field Day"
563
575
  (contest programs, when ported) — mirroring app-polo's
@@ -1647,6 +1659,7 @@ the extension. `value` names what it shows and sets.
1647
1659
  | `nativeSwitch`, `nativeCheckbox` | number, 1 or 0 | `commit` | — (shows `label` beside it) |
1648
1660
  | `nativeChip` | number, 1 or 0; or none | `commit`; `activate` without a value | `icon`; needs `event` without a value |
1649
1661
  | `nativeText` | number, string, or none | nothing | `style` (a typography role), `align` (`start`, `center`, `end`) |
1662
+ | `spectrum` | — | `commit` with the frequency clicked; `activate` from an annotation | see *Spectrum* below |
1650
1663
 
1651
1664
  The three drawn kinds are invisible hit areas over artwork the extension paints;
1652
1665
  the native kinds are Material widgets the app paints and themes. Every control
@@ -1758,6 +1771,38 @@ export const spotter: PanelHook = {
1758
1771
  }
1759
1772
  ```
1760
1773
 
1774
+ #### Spectrum
1775
+
1776
+ A `spectrum` control is a radio's panadapter and waterfall, drawn by the host
1777
+ from sweeps that never reach the extension, on a host with the panadapter
1778
+ enabled. It is **extension API 6**, as is `host.setRadioSpectrum`: a bundle
1779
+ using either declares `"api": 6`, and an app speaking less refuses it at install
1780
+ rather than failing to render the scene. It needs `requiresRadioRead`; without it, or on a host or radio
1781
+ without a spectrum, the control says why instead. It is placed like a native
1782
+ control and takes the fields of `PanelSceneSpectrum` in the SDK:
1783
+
1784
+ - `radio` (an id from `listRadios`; omitted, the designated radio), `display`
1785
+ (`both`, `pan`, `waterfall`), `split`, and `flow` — which way the waterfall
1786
+ moves (`down`, `up`, `right`, `left`), the panadapter on the side it comes
1787
+ from.
1788
+ - `floor`, `range`, `average` (0–99, default 40), `fps` (default 15),
1789
+ `waterfallSpeed`, `waterfallFloor`, `waterfallRange`: numbers, or the names
1790
+ of scene values the control reads on every sweep.
1791
+ - Colors (`#RRGGBB` or `#AARRGGBB`): `palette` or `waterfallColors`,
1792
+ `traceColor`/`traceWidth`, `fillColor`/`fillColorBottom`, `backgroundColor`,
1793
+ `gridColor`/`gridDashed`, `waterfallGridColor`.
1794
+ - `center`: the tuned-frequency line, styled per part.
1795
+ - `overlays`: up to 16 lines (`at`) and areas (`from`, `to`) in Hz from the
1796
+ tuned frequency, the sweep's middle, or absolute, each styled per part.
1797
+ - `scale`: the gap between the panadapter and the waterfall, its fill and
1798
+ borders, and frequency `labels` and `ticks`.
1799
+ - `annotations`: up to 64 pieces of text at frequencies, with a pointer to
1800
+ where they sit. One with an `event` sends `activate` when clicked, its
1801
+ frequency as `value` and its `id` as `annotation`, and does not tune.
1802
+ - `hoverHz`, `hoverDb`: scene values the pointer writes, for a text layer.
1803
+ - `tuneStep`: a click elsewhere sends `commit` with the frequency under it,
1804
+ rounded to this many Hz. The extension decides whether to tune.
1805
+
1761
1806
  The callsign comes back upper-cased, and the field shows the extension's
1762
1807
  version once the typing is committed. A bundle carrying this declares
1763
1808
  `"api": 5`. The `k2hrc-panel-scenes` sample in the SDK has every kind, a
@@ -1773,8 +1818,8 @@ repository's dashboard README for building them.
1773
1818
  The radio host calls are manifest capabilities, refused unless declared — the
1774
1819
  same shape as `requiresLocation`, and shown to the operator at install:
1775
1820
  `"requiresRadioRead": true` allows `host.listRadios()` and `host.readRadio()`;
1776
- `"requiresRadioWrite": true` allows `host.tuneRadio()` and
1777
- `host.setRadioConnection()`, and implies read, since a command answers with the
1821
+ `"requiresRadioWrite": true` allows `host.tuneRadio()`,
1822
+ `host.setRadioConnection()` and `host.setRadioSpectrum()`, and implies read, since a command answers with the
1778
1823
  radio's state. An undeclared call rejects rather than answering null.
1779
1824
  A capability belongs to the bundle that declared it, and to the sub-extensions
1780
1825
  that bundle defines under its own key. A separately installed bundle never
@@ -1805,7 +1850,17 @@ reported state confirms it. Drivers retain their own protocol/echo handling.
1805
1850
  `host.setRadioConnection({id, selection?, connected})` starts or ends a local CAT
1806
1851
  connection using the same selection and identity checks. Acceptance starts the
1807
1852
  operation; follow reported connection state for completion. It does not switch
1808
- the hardware's mains power or initiate transmission. The reference panel samples
1853
+ the hardware's mains power or initiate transmission.
1854
+ On a host with the panadapter enabled, `readRadio()` carries `spectrum`: null
1855
+ for a radio without a scope, otherwise its settings as the radio reports them
1856
+ (`mode`, `spanHz`, `lowHz`/`highHz`, `refLevelDb`, `speed`), what it accepts
1857
+ (`spansHz`, `refMinDb`…`refStepDb`, `modes`, `speeds`) and `problem`, why it
1858
+ cannot run, in the operator's language. The settings are known only while a
1859
+ `spectrum` control has the scope running, and null otherwise.
1860
+ `host.setRadioSpectrum({id, selection?, mode?, spanHz?, refLevelDb?, speed?})`
1861
+ changes them; a span between two the radio offers is snapped to the nearer. It
1862
+ answers `stopped` when no control is running the scope: nothing is queued for
1863
+ later. The reference panel samples
1809
1864
  state once per second while visible; knob motion and LCD digit updates run locally,
1810
1865
  and tuning is sent continuously during dragging, paced by the CAT adapter. The
1811
1866
  main readout follows the requested value immediately; confirmation status distinguishes pending requests from the radio
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.12.2",
3
+ "version": "0.12.5",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",