@ham2k/extension-sdk 0.5.6 → 0.6.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/AGENTS.md +10 -9
- package/README.md +6 -6
- package/dist/activityExports.js +38 -23
- package/dist/activityScoring.js +3 -9
- package/dist/base64.js +38 -0
- package/dist/copyOnWrite.js +71 -0
- package/dist/exportNames.js +16 -15
- package/dist/exportSettings.js +11 -1
- package/dist/index.d.ts +39 -3
- package/dist/index.js +85 -0
- package/dist/scoring.js +57 -34
- package/docs/distribution.md +11 -8
- package/docs/hooks.md +53 -21
- package/docs/templates.md +1 -1
- package/package.json +1 -1
- package/samples/k2hrc-cqww/build.mjs +3 -3
- package/samples/k2hrc-cqww/src/index.ts +6 -15
- package/samples/k2hrc-hamqth/build.mjs +3 -3
- package/samples/k2hrc-hamqth/src/index.ts +4 -8
- package/samples/k2hrc-llota/build.mjs +3 -3
- package/samples/k2hrc-llota/src/index.ts +4 -7
- package/samples/k2hrc-radio/build.mjs +3 -3
- package/samples/k2hrc-radio/src/index.ts +1 -5
- package/samples/k2hrc-svg-scenes/build.mjs +3 -3
- package/samples/k2hrc-svg-scenes/manifest.json +2 -10
- package/samples/k2hrc-svg-scenes/src/index.ts +4 -4
- package/samples/k2hrc-svg-scenes/src/scenes.ts +195 -80
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
// Copyright ©️ 2026 Sebastian Delmont <sd@ham2k.com>
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import type { PanelContent, PanelHook, SceneBinding, SvgSceneLayer } from
|
|
3
|
+
import type { PanelContent, PanelHook, SceneBinding, SvgSceneLayer } from "@ham2k/extension-sdk"
|
|
4
4
|
|
|
5
|
-
const svg = (body: string, width = 640, height = 360) =>
|
|
6
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${body}</svg>`
|
|
5
|
+
const svg = (body: string, width = 640, height = 360) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${body}</svg>`
|
|
7
6
|
const bind = (value: string, input: [number, number], output: [number, number]): SceneBinding => ({ value, input, output })
|
|
8
|
-
const label = (x: number, y: number, text: string, size = 16, color =
|
|
7
|
+
const label = (x: number, y: number, text: string, size = 16, color = "#9aaec6") =>
|
|
9
8
|
`<text x="${x}" y="${y}" fill="${color}" font-family="sans-serif" font-size="${size}">${text}</text>`
|
|
10
|
-
const button = (x: number, text: string) => `<rect x="${x}" y="298" width="172" height="38" rx="8" fill="#283b52"/>${label(x + 12, 322, text, 14,
|
|
11
|
-
const textLayer = (id: string, value: string, x: number, y: number, width: number,
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const button = (x: number, text: string) => `<rect x="${x}" y="298" width="172" height="38" rx="8" fill="#283b52"/>${label(x + 12, 322, text, 14, "#e7edf4")}`
|
|
10
|
+
const textLayer = (id: string, value: string, x: number, y: number, width: number, size: number, prefix = "", suffix = "", decimals = 0): SvgSceneLayer => ({
|
|
11
|
+
id,
|
|
12
|
+
x,
|
|
13
|
+
y,
|
|
14
|
+
width,
|
|
15
|
+
height: size + 12,
|
|
16
|
+
text: { value, prefix, suffix, decimals, size, color: "#a9efcf" },
|
|
17
|
+
})
|
|
14
18
|
|
|
15
19
|
type RadioState = { requested: number; reported: number; connected: number; due: number; slow: number }
|
|
16
20
|
/** Inject time for deterministic tests; no timers or per-frame extension work. */
|
|
@@ -24,103 +28,214 @@ export function createScenePanels(now: () => number = () => Date.now()): PanelHo
|
|
|
24
28
|
s = { requested: 14074, reported: 14074, connected: 1, due: 0, slow: 1 }
|
|
25
29
|
radios.set(id, s)
|
|
26
30
|
}
|
|
27
|
-
if (s.connected && s.due && now() >= s.due) {
|
|
31
|
+
if (s.connected && s.due && now() >= s.due) {
|
|
32
|
+
s.reported = s.requested
|
|
33
|
+
s.due = 0
|
|
34
|
+
}
|
|
28
35
|
return s
|
|
29
36
|
}
|
|
30
37
|
return {
|
|
31
38
|
async getPanels() {
|
|
32
39
|
return [
|
|
33
|
-
{ key:
|
|
34
|
-
{ key:
|
|
40
|
+
{ key: "radio", title: "Radio Front Face · Simulation", icon: "radio-tower", multiple: true, on: ["tick:1"] },
|
|
41
|
+
{ key: "weather", title: "Weather Chart · Simulation", icon: "weather-partly-cloudy", multiple: true },
|
|
35
42
|
]
|
|
36
43
|
},
|
|
37
44
|
async render(args): Promise<PanelContent> {
|
|
38
|
-
if (args.panelKey ===
|
|
45
|
+
if (args.panelKey === "weather") {
|
|
39
46
|
const temperatures = [17, 18, 21, 24, 25, 23, 20, 18]
|
|
40
|
-
const points = temperatures.map((t, i) => `${60 + i * 74},${265 - (t - 15) * 14}`).join(
|
|
41
|
-
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const points = temperatures.map((t, i) => `${60 + i * 74},${265 - (t - 15) * 14}`).join(" ")
|
|
48
|
+
return {
|
|
49
|
+
kind: "svgScene",
|
|
50
|
+
scene: {
|
|
51
|
+
version: 1,
|
|
52
|
+
width: 640,
|
|
53
|
+
height: 360,
|
|
54
|
+
values: { hour: 0 },
|
|
55
|
+
layers: [
|
|
56
|
+
{
|
|
57
|
+
id: "chart",
|
|
58
|
+
x: 0,
|
|
59
|
+
y: 0,
|
|
60
|
+
width: 640,
|
|
61
|
+
height: 360,
|
|
62
|
+
svg: svg(`
|
|
45
63
|
<rect width="640" height="360" rx="18" fill="#142337"/>
|
|
46
|
-
${label(30, 38,
|
|
47
|
-
${[15, 20, 25].map(t => `<path d="M60 ${265 - (t - 15) * 14}H580" stroke="#31445b"/>${label(20, 270 - (t - 15) * 14, String(t), 12)}`).join(
|
|
64
|
+
${label(30, 38, "FORECAST / SIMULATED DATA", 18, "#e7edf4")}
|
|
65
|
+
${[15, 20, 25].map((t) => `<path d="M60 ${265 - (t - 15) * 14}H580" stroke="#31445b"/>${label(20, 270 - (t - 15) * 14, String(t), 12)}`).join("")}
|
|
48
66
|
<polyline points="${points}" fill="none" stroke="#8bdccc" stroke-width="4"/>
|
|
49
|
-
${temperatures.map((t, i) => `<circle cx="${60 + i * 74}" cy="${265 - (t - 15) * 14}" r="5" fill="#e7edf4"/>${label(52 + i * 74, 288,
|
|
50
|
-
${label(30, 334,
|
|
51
|
-
`)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
${temperatures.map((t, i) => `<circle cx="${60 + i * 74}" cy="${265 - (t - 15) * 14}" r="5" fill="#e7edf4"/>${label(52 + i * 74, 288, "+" + i + "h", 12)}`).join("")}
|
|
68
|
+
${label(30, 334, "Drag the chart to inspect · Arrow keys to step", 14)}
|
|
69
|
+
`),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "cursor",
|
|
73
|
+
x: 59,
|
|
74
|
+
y: 100,
|
|
75
|
+
width: 2,
|
|
76
|
+
height: 172,
|
|
77
|
+
svg: svg('<rect width="2" height="172" fill="#f7c576"/>', 2, 172),
|
|
78
|
+
translateX: bind("hour", [0, 7], [0, 518]),
|
|
79
|
+
},
|
|
80
|
+
textLayer("hour", "hour", 30, 52, 250, 24, "In ", " hours"),
|
|
81
|
+
{
|
|
82
|
+
...textLayer("temperature", "hour", 370, 52, 240, 24, "", " °C"),
|
|
83
|
+
text: { value: "hour", samples: temperatures, suffix: " °C", size: 24, color: "#a9efcf" },
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
controls: [
|
|
87
|
+
{ id: "inspect", kind: "slider", label: "Forecast hour", x: 60, y: 100, width: 518, height: 185, value: "hour", min: 0, max: 7, step: 1 },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
}
|
|
62
91
|
}
|
|
63
92
|
const s = state(args.instanceId ?? args.panelKey)
|
|
64
|
-
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
return {
|
|
94
|
+
kind: "svgScene",
|
|
95
|
+
scene: {
|
|
96
|
+
version: 1,
|
|
97
|
+
width: 640,
|
|
98
|
+
height: 360,
|
|
99
|
+
values: { requested: s.requested, reported: s.reported, connected: s.connected, slow: s.slow },
|
|
100
|
+
layers: [
|
|
101
|
+
{
|
|
102
|
+
id: "face",
|
|
103
|
+
x: 0,
|
|
104
|
+
y: 0,
|
|
105
|
+
width: 640,
|
|
106
|
+
height: 360,
|
|
107
|
+
svg: svg(`
|
|
69
108
|
<rect width="640" height="360" rx="18" fill="#142337"/>
|
|
70
|
-
${label(28, 35,
|
|
109
|
+
${label(28, 35, "RADIO FRONT FACE / SIMULATED CAT", 18, "#e7edf4")}
|
|
71
110
|
<rect x="28" y="58" width="350" height="114" rx="12" fill="#0a1622"/>
|
|
72
|
-
${label(42, 78,
|
|
111
|
+
${label(42, 78, "REPORTED VFO", 12)}${label(42, 143, "REQUESTED", 12)}
|
|
73
112
|
<path d="M417 150 A90 90 0 0 1 597 150" fill="none" stroke="#52657d" stroke-width="5"/>
|
|
74
|
-
${label(423, 170,
|
|
75
|
-
${label(424, 197,
|
|
113
|
+
${label(423, 170, "14.0", 12)}${label(565, 170, "14.35", 12)}
|
|
114
|
+
${label(424, 197, "VFO POSITION", 12)}
|
|
76
115
|
<path d="M42 236 H332" stroke="#52657d" stroke-width="6"/>
|
|
77
|
-
${label(42, 268,
|
|
78
|
-
${label(548, 256,
|
|
79
|
-
${button(28,
|
|
80
|
-
`)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
${label(42, 268, "Band position / kHz", 14)}
|
|
117
|
+
${label(548, 256, "LINK", 12)}
|
|
118
|
+
${button(28, "Connect / disconnect")}${button(234, "External tune +5 kHz")}${button(440, "Toggle 2 s / 0 s delay")}
|
|
119
|
+
`),
|
|
120
|
+
},
|
|
121
|
+
textLayer("reported", "reported", 42, 78, 320, 34, "", " kHz", 1),
|
|
122
|
+
textLayer("requested", "requested", 168, 121, 210, 22, "", "", 1),
|
|
123
|
+
{
|
|
124
|
+
id: "needle",
|
|
125
|
+
x: 503,
|
|
126
|
+
y: 72,
|
|
127
|
+
width: 8,
|
|
128
|
+
height: 80,
|
|
129
|
+
pivot: [0.5, 1],
|
|
130
|
+
transitionMs: 450,
|
|
131
|
+
svg: svg('<path d="M4 0 L7 80 H1 Z" fill="#f6c673"/>', 8, 80),
|
|
132
|
+
rotation: bind("reported", [14000, 14350], [-80, 80]),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "knob",
|
|
136
|
+
x: 400,
|
|
137
|
+
y: 204,
|
|
138
|
+
width: 72,
|
|
139
|
+
height: 72,
|
|
140
|
+
svg: svg(
|
|
141
|
+
'<circle cx="36" cy="36" r="34" fill="#31455d" stroke="#7187a0" stroke-width="2"/><path d="M36 8V22" stroke="#f6c673" stroke-width="4"/>',
|
|
142
|
+
72,
|
|
143
|
+
72,
|
|
144
|
+
),
|
|
145
|
+
rotation: bind("requested", [14000, 14350], [-150, 150]),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "handle",
|
|
149
|
+
x: 36,
|
|
150
|
+
y: 224,
|
|
151
|
+
width: 12,
|
|
152
|
+
height: 24,
|
|
153
|
+
svg: svg('<rect width="12" height="24" rx="4" fill="#a9efcf"/>', 12, 24),
|
|
154
|
+
translateX: bind("requested", [14000, 14350], [0, 290]),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "led",
|
|
158
|
+
x: 548,
|
|
159
|
+
y: 208,
|
|
160
|
+
width: 40,
|
|
161
|
+
height: 40,
|
|
162
|
+
svg: svg(
|
|
163
|
+
'<defs><radialGradient id="glow"><stop stop-color="#a9ffbd"/><stop offset="0.3" stop-color="#58e8a2"/><stop offset="1" stop-color="#58e8a2" stop-opacity="0"/></radialGradient></defs><circle cx="20" cy="20" r="20" fill="url(#glow)"/>',
|
|
164
|
+
40,
|
|
165
|
+
40,
|
|
166
|
+
),
|
|
167
|
+
opacity: bind("connected", [0, 1], [0.08, 1]),
|
|
168
|
+
pulse: { periodMs: 1700, minOpacity: 0.55, flicker: true },
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
controls: [
|
|
172
|
+
{
|
|
173
|
+
id: "tune",
|
|
174
|
+
label: "Tuning knob, kHz",
|
|
175
|
+
kind: "knob",
|
|
176
|
+
x: 398,
|
|
177
|
+
y: 202,
|
|
178
|
+
width: 76,
|
|
179
|
+
height: 76,
|
|
180
|
+
value: "requested",
|
|
181
|
+
min: 14000,
|
|
182
|
+
max: 14350,
|
|
183
|
+
step: 0.1,
|
|
184
|
+
sensitivity: 0.1,
|
|
185
|
+
event: "tune",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "band",
|
|
189
|
+
label: "Band position, kHz",
|
|
190
|
+
kind: "slider",
|
|
191
|
+
x: 42,
|
|
192
|
+
y: 215,
|
|
193
|
+
width: 290,
|
|
194
|
+
height: 42,
|
|
195
|
+
value: "requested",
|
|
196
|
+
min: 14000,
|
|
197
|
+
max: 14350,
|
|
198
|
+
step: 0.1,
|
|
199
|
+
event: "tune",
|
|
200
|
+
},
|
|
201
|
+
...(["connect", "external", "delay"] as const).map((event, i) => ({
|
|
202
|
+
id: event,
|
|
203
|
+
label: ["Toggle connection", "Simulate external tuning", "Toggle CAT delay"][i],
|
|
204
|
+
kind: "button" as const,
|
|
205
|
+
x: 28 + i * 206,
|
|
206
|
+
y: 298,
|
|
207
|
+
width: 172,
|
|
208
|
+
height: 38,
|
|
209
|
+
event,
|
|
210
|
+
})),
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
}
|
|
107
214
|
},
|
|
108
215
|
async onEvent(args) {
|
|
109
216
|
const s = state(args.instanceId ?? args.panelKey)
|
|
110
217
|
const event = args.event
|
|
111
|
-
if (event.action ===
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
218
|
+
if (event.action === "connect") {
|
|
219
|
+
s.connected = 1 - s.connected
|
|
220
|
+
s.due = 0
|
|
221
|
+
s.requested = s.reported
|
|
222
|
+
}
|
|
223
|
+
if (event.action === "delay") s.slow = 1 - s.slow
|
|
224
|
+
if (event.action === "external") {
|
|
225
|
+
if (!s.connected) throw new Error("Radio disconnected")
|
|
115
226
|
s.reported = s.reported + 5 > 14350 ? 14000 : s.reported + 5
|
|
116
|
-
s.requested = s.reported
|
|
227
|
+
s.requested = s.reported
|
|
228
|
+
s.due = 0
|
|
117
229
|
}
|
|
118
|
-
if (event.action ===
|
|
119
|
-
if (!s.connected) throw new Error(
|
|
120
|
-
if (typeof event.value !==
|
|
230
|
+
if (event.action === "tune") {
|
|
231
|
+
if (!s.connected) throw new Error("Radio disconnected")
|
|
232
|
+
if (typeof event.value !== "number" || !Number.isFinite(event.value)) throw new Error("Invalid frequency")
|
|
121
233
|
s.requested = Math.max(14000, Math.min(14350, event.value))
|
|
122
234
|
s.due = now() + (s.slow ? 2000 : 0)
|
|
123
|
-
if (!s.slow) {
|
|
235
|
+
if (!s.slow) {
|
|
236
|
+
s.reported = s.requested
|
|
237
|
+
s.due = 0
|
|
238
|
+
}
|
|
124
239
|
}
|
|
125
240
|
return { values: { requested: s.requested, reported: s.reported, connected: s.connected, slow: s.slow } }
|
|
126
241
|
},
|