@tsplc/debug 0.2.16 → 0.2.18

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.
@@ -2,367 +2,440 @@ import type { DebugDevice } from "../manifest.ts";
2
2
  import { mapBits, readBits } from "./bits.ts";
3
3
  import type { Quality } from "../api.ts";
4
4
  import {
5
- type View,
6
- deviceHealth,
7
- empty,
8
- fill,
9
- h,
10
- healthBadge,
11
- pathLink,
12
- sourceLink,
5
+ type View,
6
+ deviceHealth,
7
+ deviceHref,
8
+ devicesWithHealth,
9
+ empty,
10
+ fill,
11
+ h,
12
+ healthBadge,
13
+ pathLink,
14
+ sourceLink,
15
+ table,
13
16
  } from "./dom.ts";
14
- import { formatScalar, formatValue, valueState, valueStateLabels } from "./format.ts";
17
+ import {
18
+ formatScalar,
19
+ formatValue,
20
+ valueState,
21
+ valueStateLabels,
22
+ } from "./format.ts";
15
23
  import type { Store } from "./store.ts";
16
24
 
17
- const deviceHref = (id: string) => `#/hardware/${encodeURIComponent(id)}`;
18
-
19
25
  const assemblySummary = (device: DebugDevice) =>
20
- device.assemblies
21
- .map((assembly) => `${assembly.direction === "input" ? "in" : "out"} ${assembly.bytes} B`)
22
- .join(" · ");
26
+ device.assemblies
27
+ .map(
28
+ (assembly) =>
29
+ `${assembly.direction === "input" ? "in" : "out"} ${assembly.bytes} B`,
30
+ )
31
+ .join(" · ");
32
+
33
+ /** Where an assembly's grid comes from, and how far to trust it. */
34
+ const rawBytesNote = (
35
+ bytes: number[] | null,
36
+ quality: string,
37
+ raw: string | undefined,
38
+ ) => {
39
+ if (bytes) {
40
+ return quality === "good" ? " · raw bytes" : ` · raw bytes, ${quality}`;
41
+ }
42
+ if (raw) {
43
+ return " · raw bytes not read yet";
44
+ }
45
+ return " · no raw bytes in this build: it has no generated device tree";
46
+ };
23
47
 
24
48
  const deviceList = (store: Store): View => {
25
- const body = h("tbody");
26
- const render = () =>
27
- body.replaceChildren(
28
- ...store.manifest.devices.map((device) =>
29
- h(
30
- "tr",
31
- { class: "link-row", onclick: () => (location.hash = deviceHref(device.id)) },
32
- h("td", {}, healthBadge(deviceHealth(store, device))),
33
- h("td", {}, h("a", { href: deviceHref(device.id), class: "mono" }, device.id)),
34
- h("td", {}, [device.vendor, device.product].filter(Boolean).join(" ")),
35
- h(
36
- "td",
37
- { class: "mono" },
38
- device.network
39
- ? `${device.network.address}${device.network.rpiMs === undefined ? "" : ` · RPI ${device.network.rpiMs} ms`}`
40
- : "",
41
- ),
42
- h("td", {}, device.connection ?? ""),
43
- h("td", { class: "mono" }, assemblySummary(device)),
44
- h("td", { class: "num" }, String(device.channels.length)),
45
- ),
46
- ),
47
- );
48
- render();
49
- store.show({
50
- health: store.manifest.devices.filter((device) => device.health).map(({ id }) => id),
51
- });
52
- return {
53
- element: h(
54
- "div",
55
- { class: "page" },
56
- h("h1", {}, "Hardware"),
57
- store.manifest.devices.length
58
- ? h(
59
- "table",
60
- { class: "grid" },
61
- h(
62
- "thead",
63
- {},
64
- h(
65
- "tr",
66
- {},
67
- ...[
68
- "Health",
69
- "Device",
70
- "Product",
71
- "Network",
72
- "Connection",
73
- "Assemblies",
74
- "Channels",
75
- ].map((label) => h("th", {}, label)),
76
- ),
77
- ),
78
- body,
79
- )
80
- : empty("This build configures no devices."),
81
- ),
82
- update(topics) {
83
- if (topics.has("devices") || topics.has("stream") || topics.has("session")) render();
84
- },
85
- };
49
+ const body = h("tbody");
50
+ const render = () =>
51
+ body.replaceChildren(
52
+ ...store.manifest.devices.map((device) =>
53
+ h(
54
+ "tr",
55
+ {
56
+ class: "link-row",
57
+ onclick: () => (location.hash = deviceHref(device.id)),
58
+ },
59
+ h("td", {}, healthBadge(deviceHealth(store, device))),
60
+ h(
61
+ "td",
62
+ {},
63
+ h("a", { href: deviceHref(device.id), class: "mono" }, device.id),
64
+ ),
65
+ h(
66
+ "td",
67
+ {},
68
+ [device.vendor, device.product].filter(Boolean).join(" "),
69
+ ),
70
+ h(
71
+ "td",
72
+ { class: "mono" },
73
+ device.network
74
+ ? `${device.network.address}${device.network.rpiMs === undefined ? "" : ` · RPI ${device.network.rpiMs} ms`}`
75
+ : "",
76
+ ),
77
+ h("td", {}, device.connection ?? ""),
78
+ h("td", { class: "mono" }, assemblySummary(device)),
79
+ h("td", { class: "num" }, String(device.channels.length)),
80
+ ),
81
+ ),
82
+ );
83
+ render();
84
+ store.show({ health: devicesWithHealth(store) });
85
+ return {
86
+ element: h(
87
+ "div",
88
+ { class: "page" },
89
+ h("h1", {}, "Hardware"),
90
+ store.manifest.devices.length
91
+ ? table(
92
+ { class: "grid" },
93
+ [
94
+ "Health",
95
+ "Device",
96
+ "Product",
97
+ "Network",
98
+ "Connection",
99
+ "Assemblies",
100
+ "Channels",
101
+ ],
102
+ body,
103
+ )
104
+ : empty("This build configures no devices."),
105
+ ),
106
+ update(topics) {
107
+ if (
108
+ topics.has("devices") ||
109
+ topics.has("stream") ||
110
+ topics.has("session")
111
+ ) {
112
+ render();
113
+ }
114
+ },
115
+ };
86
116
  };
87
117
 
88
118
  const deviceDetail = (store: Store, device: DebugDevice): View => {
89
- const io = store.manifest.io.filter((entry) => entry.device === device.id);
90
- const badge = h("span");
91
- const status = h("dl", { class: "facts readings" });
92
- const grids = h("div", { class: "assemblies" });
93
- const channelBody = h("tbody");
119
+ const io = store.manifest.io.filter((entry) => entry.device === device.id);
120
+ const badge = h("span");
121
+ const status = h("dl", { class: "facts readings" });
122
+ const grids = h("div", { class: "assemblies" });
123
+ const channelBody = h("tbody");
94
124
 
95
- /** The state for the manifest's `index`th assembly: the host lists them in the same order per direction. */
96
- const bytesOf = (index: number) => {
97
- const assembly = device.assemblies[index];
98
- if (!assembly) return undefined;
99
- const sameDirection = device.assemblies
100
- .slice(0, index)
101
- .filter((other) => other.direction === assembly.direction).length;
102
- return store.devices
103
- .get(device.id)
104
- ?.assemblies.filter((state) => state.direction === assembly.direction)[sameDirection];
105
- };
125
+ /** The state for the manifest's `index`th assembly: the host lists them in the same order per direction. */
126
+ const bytesOf = (index: number) => {
127
+ const assembly = device.assemblies[index];
128
+ if (!assembly) {
129
+ return undefined;
130
+ }
131
+ const sameDirection = device.assemblies
132
+ .slice(0, index)
133
+ .filter((other) => other.direction === assembly.direction).length;
134
+ return store.devices
135
+ .get(device.id)
136
+ ?.assemblies.filter((state) => state.direction === assembly.direction)[
137
+ sameDirection
138
+ ];
139
+ };
106
140
 
107
- const renderHealth = () => {
108
- const health = deviceHealth(store, device);
109
- badge.replaceChildren(
110
- healthBadge(health),
111
- health.detail ? h("span", { class: "dim" }, ` ${health.detail}`) : "",
112
- );
113
- };
141
+ const renderHealth = () => {
142
+ const health = deviceHealth(store, device);
143
+ badge.replaceChildren(
144
+ healthBadge(health),
145
+ health.detail ? h("span", { class: "dim" }, ` ${health.detail}`) : "",
146
+ );
147
+ };
114
148
 
115
- /** Device states carry no sample time, so only the connection marks their values stale. */
116
- const reading = (quality: Quality, value: string) => {
117
- const state = valueState(quality, 0, {
118
- connected: store.connected,
119
- intervalMs: store.session.reading.intervalMs,
120
- });
121
- return [
122
- h("span", { class: `value s-${state}` }, value),
123
- h("span", { class: `meta s-${state}` }, valueStateLabels[state]),
124
- ];
125
- };
149
+ /** Device states carry no sample time, so only the connection marks their values stale. */
150
+ const reading = (quality: Quality, value: string) => {
151
+ const state = valueState(quality, 0, {
152
+ connected: store.connected,
153
+ intervalMs: store.session.reading.intervalMs,
154
+ });
155
+ return [
156
+ h("span", { class: `value s-${state}` }, value),
157
+ h("span", { class: `meta s-${state}` }, valueStateLabels[state]),
158
+ ];
159
+ };
126
160
 
127
- const renderStatus = () => {
128
- const values = store.devices.get(device.id)?.status ?? {};
129
- fill(
130
- status,
131
- ...Object.keys(device.status ?? {}).flatMap((label) => {
132
- const entry = values[label];
133
- return [
134
- h("dt", {}, label),
135
- h(
136
- "dd",
137
- {},
138
- ...(entry
139
- ? reading(entry.quality, formatScalar(entry.value))
140
- : [h("span", { class: "value s-unavailable" }, "not read")]),
141
- ),
142
- ];
143
- }),
144
- );
145
- };
161
+ const renderStatus = () => {
162
+ const values = store.devices.get(device.id)?.status ?? {};
163
+ fill(
164
+ status,
165
+ ...Object.keys(device.status ?? {}).flatMap((label) => {
166
+ const entry = values[label];
167
+ return [
168
+ h("dt", {}, label),
169
+ h(
170
+ "dd",
171
+ {},
172
+ ...(entry
173
+ ? reading(entry.quality, formatScalar(entry.value))
174
+ : [h("span", { class: "value s-unavailable" }, "not read")]),
175
+ ),
176
+ ];
177
+ }),
178
+ );
179
+ };
146
180
 
147
- const renderGrids = () =>
148
- grids.replaceChildren(
149
- ...device.assemblies.map((assembly, index) => {
150
- const state = bytesOf(index);
151
- const bytes = state?.bytes ?? null;
152
- const bits = mapBits(device.id, assembly.direction, assembly.bytes, device.channels, io);
153
- const quality = !store.connected ? "stale" : state ? state.quality : "unavailable";
154
- return h(
155
- "section",
156
- { class: `panel assembly q-${quality}` },
157
- h(
158
- "h3",
159
- {},
160
- `${assembly.direction === "input" ? "Input" : "Output"} assembly · ${assembly.bytes} bytes`,
161
- assembly.address ? h("span", { class: "mono dim" }, ` ${assembly.address}`) : "",
162
- h(
163
- "span",
164
- { class: "dim" },
165
- bytes
166
- ? ` · ${quality === "good" ? "raw bytes" : `raw bytes, ${quality}`}`
167
- : assembly.raw
168
- ? " · raw bytes not read yet"
169
- : " · no raw bytes in this build: it has no generated device tree",
170
- ),
171
- ),
172
- h(
173
- "div",
174
- { class: "bitgrid" },
175
- h("span", { class: "bh" }, "byte"),
176
- ...Array.from({ length: 8 }, (_, bit) => h("span", { class: "bh" }, `.${bit}`)),
177
- h("span", { class: "bh" }, "hex"),
178
- h("span", { class: "bh legend-head" }, "mapped"),
179
- ...Array.from({ length: assembly.bytes }, (_, byte) => {
180
- const value = bytes?.[byte];
181
- const starts = device.channels.filter(
182
- (channel) =>
183
- channel.direction === assembly.direction && channel.offsetBits >> 3 === byte,
184
- );
185
- return [
186
- h("span", { class: "byte mono" }, `+${byte}`),
187
- ...Array.from({ length: 8 }, (_, bit) => {
188
- const mapped = bits[byte * 8 + bit];
189
- const on = value === undefined ? undefined : (value >> bit) & 1;
190
- const where = `byte ${byte} bit ${bit}`;
191
- return h(
192
- "span",
193
- {
194
- class: `bit${mapped ? " mapped" : ""}${on === 1 ? " on" : ""}${on === undefined ? " none" : ""}`,
195
- title: mapped
196
- ? `${where} · ${mapped.channel.name} (${mapped.channel.id})${mapped.paths.map((path) => `\n${path}`).join("")}`
197
- : `${where} · not mapped`,
198
- },
199
- on === undefined ? "·" : String(on),
200
- mapped && mapped.channel.offsetBits === byte * 8 + bit
201
- ? h("small", {}, mapped.channel.id)
202
- : undefined,
203
- );
204
- }),
205
- h(
206
- "span",
207
- { class: "hex mono" },
208
- value === undefined ? "··" : value.toString(16).padStart(2, "0").toUpperCase(),
209
- ),
210
- h(
211
- "span",
212
- { class: "legend" },
213
- ...starts.map((channel) =>
214
- h(
215
- "span",
216
- { class: "legend-item" },
217
- h("b", {}, channel.id),
218
- ...io
219
- .filter((entry) => entry.channel === channel.id)
220
- .map((entry) =>
221
- pathLink(entry.path, entry.path.split(".").slice(-3).join(".")),
222
- ),
223
- ),
224
- ),
225
- ),
226
- ];
227
- }).flat(),
228
- ),
229
- );
230
- }),
231
- );
181
+ const renderGrids = () =>
182
+ grids.replaceChildren(
183
+ ...device.assemblies.map((assembly, index) => {
184
+ const state = bytesOf(index);
185
+ const bytes = state?.bytes ?? null;
186
+ const bits = mapBits(
187
+ device.id,
188
+ assembly.direction,
189
+ assembly.bytes,
190
+ device.channels,
191
+ io,
192
+ );
193
+ const quality = store.connected
194
+ ? (state?.quality ?? "unavailable")
195
+ : "stale";
196
+ return h(
197
+ "section",
198
+ { class: `panel assembly q-${quality}` },
199
+ h(
200
+ "h3",
201
+ {},
202
+ `${assembly.direction === "input" ? "Input" : "Output"} assembly · ${assembly.bytes} bytes`,
203
+ assembly.address
204
+ ? h("span", { class: "mono dim" }, ` ${assembly.address}`)
205
+ : "",
206
+ h(
207
+ "span",
208
+ { class: "dim" },
209
+ rawBytesNote(bytes, quality, assembly.raw),
210
+ ),
211
+ ),
212
+ h(
213
+ "div",
214
+ { class: "bitgrid" },
215
+ h("span", { class: "bh" }, "byte"),
216
+ ...Array.from({ length: 8 }, (_, bit) =>
217
+ h("span", { class: "bh" }, `.${bit}`),
218
+ ),
219
+ h("span", { class: "bh" }, "hex"),
220
+ h("span", { class: "bh legend-head" }, "mapped"),
221
+ ...Array.from({ length: assembly.bytes }, (_, byte) => {
222
+ const value = bytes?.[byte];
223
+ const starts = device.channels.filter(
224
+ (channel) =>
225
+ channel.direction === assembly.direction &&
226
+ channel.offsetBits >> 3 === byte,
227
+ );
228
+ return [
229
+ h("span", { class: "byte mono" }, `+${byte}`),
230
+ ...Array.from({ length: 8 }, (_, bit) => {
231
+ const mapped = bits[byte * 8 + bit];
232
+ const on =
233
+ value === undefined ? undefined : (value >> bit) & 1;
234
+ const where = `byte ${byte} bit ${bit}`;
235
+ return h(
236
+ "span",
237
+ {
238
+ class: `bit${mapped ? " mapped" : ""}${on === 1 ? " on" : ""}${on === undefined ? " none" : ""}`,
239
+ title: mapped
240
+ ? `${where} · ${mapped.channel.name} (${mapped.channel.id})${mapped.paths.map((path) => `\n${path}`).join("")}`
241
+ : `${where} · not mapped`,
242
+ },
243
+ on === undefined ? "·" : String(on),
244
+ mapped && mapped.channel.offsetBits === byte * 8 + bit
245
+ ? h("small", {}, mapped.channel.id)
246
+ : undefined,
247
+ );
248
+ }),
249
+ h(
250
+ "span",
251
+ { class: "hex mono" },
252
+ value === undefined
253
+ ? "··"
254
+ : value.toString(16).padStart(2, "0").toUpperCase(),
255
+ ),
256
+ h(
257
+ "span",
258
+ { class: "legend" },
259
+ ...starts.map((channel) =>
260
+ h(
261
+ "span",
262
+ { class: "legend-item" },
263
+ h("b", {}, channel.id),
264
+ ...io
265
+ .filter((entry) => entry.channel === channel.id)
266
+ .map((entry) =>
267
+ pathLink(
268
+ entry.path,
269
+ entry.path.split(".").slice(-3).join("."),
270
+ ),
271
+ ),
272
+ ),
273
+ ),
274
+ ),
275
+ ];
276
+ }).flat(),
277
+ ),
278
+ );
279
+ }),
280
+ );
232
281
 
233
- const renderChannels = () =>
234
- channelBody.replaceChildren(
235
- ...device.channels.map((channel) => {
236
- const index = device.assemblies.findIndex(
237
- (assembly) => assembly.direction === channel.direction,
238
- );
239
- const bytes = index >= 0 ? bytesOf(index)?.bytes : undefined;
240
- const raw = bytes ? readBits(bytes, channel.offsetBits, channel.bits) : null;
241
- const mapped = io.filter((entry) => entry.channel === channel.id);
242
- return h(
243
- "tr",
244
- {},
245
- h("td", { class: "mono" }, channel.id),
246
- h(
247
- "td",
248
- {},
249
- channel.name,
250
- channel.description ? h("div", { class: "dim" }, channel.description) : "",
251
- ),
252
- h("td", {}, channel.direction),
253
- h(
254
- "td",
255
- { class: "mono" },
256
- `${channel.offsetBits >> 3}.${channel.offsetBits & 7} · ${channel.bits} bit${channel.bits === 1 ? "" : "s"}`,
257
- ),
258
- h("td", { class: "mono" }, channel.address ?? ""),
259
- h(
260
- "td",
261
- { class: "mono num", title: "Decoded from the raw assembly bytes" },
262
- raw === null ? "—" : String(raw),
263
- channel.unit ? ` ${channel.unit}` : "",
264
- ),
265
- h(
266
- "td",
267
- {},
268
- ...(mapped.length
269
- ? mapped.map((entry) => {
270
- const bound = store.devices
271
- .get(device.id)
272
- ?.channels.find(
273
- (candidate) => candidate.id === channel.id && candidate.path === entry.path,
274
- );
275
- const node = store.tree.resolve(entry.path);
276
- return h(
277
- "div",
278
- { class: "mapped-path" },
279
- pathLink(entry.path),
280
- entry.inverted ? h("span", { class: "tag" }, "inverted") : "",
281
- ...(bound
282
- ? reading(bound.quality, formatValue(store.manifest, entry.type, bound.value))
283
- : []),
284
- node?.declaration ? sourceLink(store, node.declaration) : "",
285
- );
286
- })
287
- : [h("span", { class: "dim" }, "not mapped")]),
288
- ),
289
- );
290
- }),
291
- );
282
+ const renderChannels = () =>
283
+ channelBody.replaceChildren(
284
+ ...device.channels.map((channel) => {
285
+ const index = device.assemblies.findIndex(
286
+ (assembly) => assembly.direction === channel.direction,
287
+ );
288
+ const bytes = index >= 0 ? bytesOf(index)?.bytes : undefined;
289
+ const raw = bytes
290
+ ? readBits(bytes, channel.offsetBits, channel.bits)
291
+ : null;
292
+ const mapped = io.filter((entry) => entry.channel === channel.id);
293
+ return h(
294
+ "tr",
295
+ {},
296
+ h("td", { class: "mono" }, channel.id),
297
+ h(
298
+ "td",
299
+ {},
300
+ channel.name,
301
+ channel.description
302
+ ? h("div", { class: "dim" }, channel.description)
303
+ : "",
304
+ ),
305
+ h("td", {}, channel.direction),
306
+ h(
307
+ "td",
308
+ { class: "mono" },
309
+ `${channel.offsetBits >> 3}.${channel.offsetBits & 7} · ${channel.bits} bit${channel.bits === 1 ? "" : "s"}`,
310
+ ),
311
+ h("td", { class: "mono" }, channel.address ?? ""),
312
+ h(
313
+ "td",
314
+ { class: "mono num", title: "Decoded from the raw assembly bytes" },
315
+ raw === null ? "—" : String(raw),
316
+ channel.unit ? ` ${channel.unit}` : "",
317
+ ),
318
+ h(
319
+ "td",
320
+ {},
321
+ ...(mapped.length
322
+ ? mapped.map((entry) => {
323
+ const bound = store.devices
324
+ .get(device.id)
325
+ ?.channels.find(
326
+ (candidate) =>
327
+ candidate.id === channel.id &&
328
+ candidate.path === entry.path,
329
+ );
330
+ const node = store.tree.resolve(entry.path);
331
+ return h(
332
+ "div",
333
+ { class: "mapped-path" },
334
+ pathLink(entry.path),
335
+ entry.inverted
336
+ ? h("span", { class: "tag" }, "inverted")
337
+ : "",
338
+ ...(bound
339
+ ? reading(
340
+ bound.quality,
341
+ formatValue(store.manifest, entry.type, bound.value),
342
+ )
343
+ : []),
344
+ node?.declaration
345
+ ? sourceLink(store, node.declaration)
346
+ : "",
347
+ );
348
+ })
349
+ : [h("span", { class: "dim" }, "not mapped")]),
350
+ ),
351
+ );
352
+ }),
353
+ );
292
354
 
293
- renderHealth();
294
- renderStatus();
295
- renderGrids();
296
- renderChannels();
297
- store.show({ devices: [device.id] });
355
+ renderHealth();
356
+ renderStatus();
357
+ renderGrids();
358
+ renderChannels();
359
+ store.show({ devices: [device.id] });
298
360
 
299
- const identity: [string, string | undefined][] = [
300
- ["Vendor", device.vendor],
301
- ["Product", device.product],
302
- ["Product code", device.productCode],
303
- ["Address", device.network?.address],
304
- ["RPI", device.network?.rpiMs === undefined ? undefined : `${device.network.rpiMs} ms`],
305
- ["Connection", device.connection],
306
- ];
307
- return {
308
- element: h(
309
- "div",
310
- { class: "page" },
311
- h("div", { class: "crumbs" }, h("a", { href: "#/hardware" }, "Hardware"), " / "),
312
- h("h1", { class: "mono" }, device.id, " ", badge),
313
- h(
314
- "dl",
315
- { class: "facts" },
316
- ...identity.flatMap(([label, value]) =>
317
- value === undefined ? [] : [h("dt", {}, label), h("dd", { class: "mono" }, value)],
318
- ),
319
- ),
320
- device.status && h("h2", {}, "Status"),
321
- status,
322
- grids,
323
- h("h2", {}, "Channels"),
324
- device.channels.length
325
- ? h(
326
- "table",
327
- { class: "grid" },
328
- h(
329
- "thead",
330
- {},
331
- h(
332
- "tr",
333
- {},
334
- ...["Id", "Name", "Dir", "Bits", "Address", "Raw", "TypeScript"].map((label) =>
335
- h("th", {}, label),
336
- ),
337
- ),
338
- ),
339
- channelBody,
340
- )
341
- : empty("No channels configured."),
342
- ),
343
- update(topics) {
344
- if (topics.has("devices") || topics.has("stream") || topics.has("session")) {
345
- renderHealth();
346
- renderStatus();
347
- renderGrids();
348
- renderChannels();
349
- }
350
- },
351
- };
361
+ const identity: [string, string | undefined][] = [
362
+ ["Vendor", device.vendor],
363
+ ["Product", device.product],
364
+ ["Product code", device.productCode],
365
+ ["Address", device.network?.address],
366
+ [
367
+ "RPI",
368
+ device.network?.rpiMs === undefined
369
+ ? undefined
370
+ : `${device.network.rpiMs} ms`,
371
+ ],
372
+ ["Connection", device.connection],
373
+ ];
374
+ return {
375
+ element: h(
376
+ "div",
377
+ { class: "page" },
378
+ h(
379
+ "div",
380
+ { class: "crumbs" },
381
+ h("a", { href: "#/hardware" }, "Hardware"),
382
+ " / ",
383
+ ),
384
+ h("h1", { class: "mono" }, device.id, " ", badge),
385
+ h(
386
+ "dl",
387
+ { class: "facts" },
388
+ ...identity.flatMap(([label, value]) =>
389
+ value === undefined
390
+ ? []
391
+ : [h("dt", {}, label), h("dd", { class: "mono" }, value)],
392
+ ),
393
+ ),
394
+ device.status && h("h2", {}, "Status"),
395
+ status,
396
+ grids,
397
+ h("h2", {}, "Channels"),
398
+ device.channels.length
399
+ ? table(
400
+ { class: "grid" },
401
+ ["Id", "Name", "Dir", "Bits", "Address", "Raw", "TypeScript"],
402
+ channelBody,
403
+ )
404
+ : empty("No channels configured."),
405
+ ),
406
+ update(topics) {
407
+ if (
408
+ topics.has("devices") ||
409
+ topics.has("stream") ||
410
+ topics.has("session")
411
+ ) {
412
+ renderHealth();
413
+ renderStatus();
414
+ renderGrids();
415
+ renderChannels();
416
+ }
417
+ },
418
+ };
352
419
  };
353
420
 
354
421
  export const hardwareView = (store: Store, id: string | undefined): View => {
355
- if (id === undefined) return deviceList(store);
356
- const device = store.manifest.devices.find((candidate) => candidate.id === id);
357
- if (device) return deviceDetail(store, device);
358
- store.show();
359
- return {
360
- element: h(
361
- "div",
362
- { class: "page" },
363
- h("h1", {}, "Hardware"),
364
- empty(`No device ${id} in this build.`),
365
- ),
366
- update() {},
367
- };
422
+ if (id === undefined) {
423
+ return deviceList(store);
424
+ }
425
+ const device = store.manifest.devices.find(
426
+ (candidate) => candidate.id === id,
427
+ );
428
+ if (device) {
429
+ return deviceDetail(store, device);
430
+ }
431
+ store.show();
432
+ return {
433
+ element: h(
434
+ "div",
435
+ { class: "page" },
436
+ h("h1", {}, "Hardware"),
437
+ empty(`No device ${id} in this build.`),
438
+ ),
439
+ update() {},
440
+ };
368
441
  };