import { For, Show, createEffect, createSignal, onCleanup } from "solid-js"; import { BusMonitor, type MonitorEvent, type Observed } from "../modbus/monitor"; import type { Connection } from "../serial/connection"; import { describe, elapsed, frameToHex, registerAddress, toHex } from "./format"; /** * How many events are kept. A busy bus produces a few hundred frames a minute, and the interesting * part is nearly always the recent end, so the list is capped rather than grown without limit */ const KEEP = 500; export default function Monitor(props: { connection: Connection }) { const [events, setEvents] = createSignal([]); const [paused, setPaused] = createSignal(false); const [origin, setOrigin] = createSignal(); // Solid 2.0 splits an effect into a compute and an effect function: the first tracks, the second // acts on what it produced. Here the tracked value is the connection, and re-subscribing when it // changes is the side effect — the returned function tears the old subscription down createEffect( () => props.connection, (connection) => { const monitor = new BusMonitor(); const unsubscribe = connection.subscribe(({ bytes, at }) => { if (paused()) return; const produced = monitor.push(bytes, at); if (produced.length === 0) return; setOrigin((current) => current ?? produced[0]!.at); setEvents((current) => [...current, ...produced].slice(-KEEP)); }); onCleanup(unsubscribe); }, ); return (

Bus monitor

Passive. Nothing is written to the port, so this is safe to leave running while the fan controller drives the bus.

0} fallback={

Nothing on the bus yet. Frames appear here as soon as something transmits — check the baud rate and parity if the controller is running and this stays empty.

} >
{(event) => }
Time From Address Function Meaning Bytes
); } function Row(props: { event: MonitorEvent; origin: number }) { return ( {elapsed(props.event.at, props.origin)} Unaccounted bytes — the monitor could not fit these into a frame {frameToHex(props.event.bytes)} } > {(frame) => ( {elapsed(frame().at, props.origin)} {frame().role === "request" ? "Master" : "Device"} {frame().address} 0x{toHex(frame().functionCode)} {describe(frame().decoded)} {/* A read response carries values but not the addresses they came from. Where the monitor managed to pair it with its request, say which range they belong to */} {(start) => — from {registerAddress(start())}} {frameToHex(frame().bytes)} )} ); } /** * The start address a paired request was asking about, when it was a read. * * Narrowing here rather than inline, because `decoded` is a union in which only some members have * a `start` at all and TypeScript cannot follow that through a call inside a ternary */ function answeredRange(decoded: Observed["decoded"] | undefined): number | undefined { if (!decoded) return undefined; return decoded.kind === "read" ? decoded.start : undefined; }