import { For, Show, createMemo, createSignal } from "solid-js"; import { devices, noContext, referencesFor, registersIn, type Context, type Device, type Register, } from "../devices"; import { BAUD_RATE_WRITE, BAUD_RATE_READ, relay } from "../devices/relay"; import { readAll, write } from "../serial/client"; import type { Connection } from "../serial/connection"; import { registerAddress, toHex } from "./format"; type State = { values: Map; refused: { start: number; quantity: number; message: string }[]; at?: Date; }; export default function Devices(props: { connection: Connection }) { const [device, setDevice] = createSignal(devices[0]!); const [address, setAddress] = createSignal(devices[0]!.defaults.address); const [state, setState] = createSignal({ values: new Map(), refused: [] }); const [busy, setBusy] = createSignal(false); const [error, setError] = createSignal(); const context = createMemo(() => ({ holding: state().values })); function chooseDevice(id: string) { const chosen = devices.find((entry) => entry.id === id); if (!chosen) return; setDevice(chosen); setAddress(chosen.defaults.address); setState({ values: new Map(), refused: [] }); } async function readEverything() { setBusy(true); setError(undefined); try { const values = new Map(); const refused: State["refused"] = []; // The references first, and on their own: almost every RadiCal input register is a fraction // of one of them, so reading them last would mean a first pass that can state nothing const references = referencesFor(device().registers); if (references.length > 0) { const result = await readAll( props.connection, address(), "holding", references.map((reference) => ({ address: reference }) as Register), ); for (const [key, value] of result.values) values.set(key, value); } for (const space of ["holding", "input", "coil", "discreteInput"] as const) { const registers = registersIn(device(), space); if (registers.length === 0) continue; const result = await readAll(props.connection, address(), space, registers); for (const [key, value] of result.values) values.set(key, value); refused.push(...result.refused); } setState({ values, refused, at: new Date() }); } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); } finally { setBusy(false); } } return (

Device

Active. This sends requests, so do not use it while the fan controller is driving the same bus — two masters on one RS-485 line collide.

{/* The hint sits above the field, so an autocomplete popover cannot cover it */} {device().defaults.addressRange[0]}–{device().defaults.addressRange[1]}, default{" "} {device().defaults.address} setAddress(event.currentTarget.valueAsNumber)} />

Documented in {device().documentation}. Expects {device().defaults.baudRate} baud,{" "} {device().defaults.parity} parity — set the port to match before reading.

{(message) =>

{message()}

}
{(at) =>

Last read {at().toLocaleTimeString()}

}
{(refusal) => (

{registerAddress(refusal.start)} +{refusal.quantity}: {refusal.message}

)}
{(space) => ( 0}>

{spaceHeading[space]}

{(register) => ( void readEverything()} /> )}
Address Name Value Write
)}
); } const spaceHeading = { input: "Input registers — read only", holding: "Holding registers — read and write", coil: "Coils — read and write", discreteInput: "Discrete inputs — read only", } as const; function RegisterRow(props: { connection: Connection; deviceAddress: number; register: Register; raw: number | undefined; context: Context; onWritten: () => void; }) { const [pending, setPending] = createSignal(); const [failure, setFailure] = createSignal(); const [writing, setWriting] = createSignal(false); // The browser already knows whether the value is within the min/max/step the register declares, // so its judgement is the one used rather than a second copy of the bounds check here const [valid, setValid] = createSignal(false); let field: HTMLInputElement | undefined; const decoded = createMemo(() => { const raw = props.raw; return raw === undefined ? undefined : props.register.decode(raw, props.context ?? noContext); }); async function send() { const value = pending(); const writable = props.register.write; if (value === undefined || !writable) return; // Refuse to put a value on the bus that the device is bound to reject anyway if (field && !field.validity.valid) { field.reportValidity(); return; } setWriting(true); setFailure(undefined); try { await write(props.connection, props.deviceAddress, props.register, writable.encode(value), { // The relay's manual only ever writes its holding registers with 0x10, and reads its baud // rate from a different address than it writes it to useMultiple: props.register.space === "holding" && isRelayRegister(props.register), writeAddress: props.register.address === BAUD_RATE_READ && isRelayRegister(props.register) ? BAUD_RATE_WRITE : undefined, }); props.onWritten(); } catch (cause) { setFailure(cause instanceof Error ? cause.message : String(cause)); } finally { setWriting(false); } } const inputId = () => `write-${props.register.space}-${props.register.address}`; return ( {registerAddress(props.register.address)} §{props.register.reference.replace(/^docs\/.*, /, "")} {props.register.name} {(gloss) => — {gloss()}} not read}> {(value) => ( <> {value().text} {(reason) => — {(reason() as { because: string }).because}} raw 0x{toHex(props.raw!, 4)} )} —}> {(writable) => (
{ setPending(props.raw ? 0 : 1); void send(); }}> {props.raw ? "Open" : "Close"} } > {(numeric) => ( <> { setPending(event.currentTarget.valueAsNumber); setValid(event.currentTarget.validity.valid); }} /> {/* Revealed by :user-invalid, so nothing is flagged until the field is left */} {(numeric() as { min: number }).min} to {(numeric() as { max: number }).max} )} } > {(choice) => ( )} {(message) => {message()}}
)}
); } function isRelayRegister(register: Register): boolean { return relay.registers.includes(register); }