Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/SantaClaas/embedded-fan-control.
Something went wrong. Try again.
8.7 kB · 231 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232import { describe, expect, it } from "vitest";import { append } from "./crc";import { bytes, relay, temperatureSensor } from "./frames.fixture";import { readInputRegisters } from "./pdu";import { BusMonitor, type MonitorEvent, type Observed } from "./monitor";
/** Joins frames into one buffer, the way they arrive when the adapter batches a burst */function run(chunks: Uint8Array[], options?: { gapMs?: number; at?: (index: number) => number }) { const monitor = new BusMonitor(options); const events: MonitorEvent[] = [];
chunks.forEach((chunk, index) => { events.push(...monitor.push(chunk, options?.at?.(index) ?? index)); });
return events;}
function frames(events: MonitorEvent[]): Observed[] { return events.filter((event): event is MonitorEvent & Observed => event.type === "frame");}
describe("BusMonitor", () => { it("recovers a request and its response from one chunk", () => { const events = run([concat(relay.readBaudRequest, relay.readBaudResponse)]); const seen = frames(events);
expect(seen).toHaveLength(2); expect(seen[0]).toMatchObject({ role: "request", address: 0xff, functionCode: 0x03 }); expect(seen[1]).toMatchObject({ role: "response", address: 0xff, functionCode: 0x03 }); expect(seen[1]!.decoded).toEqual({ kind: "registers", of: "holdingRegisters", values: [4] }); });
it("recovers a frame split across chunks", () => { const whole = concat(relay.readBaudRequest, relay.readBaudResponse);
// Split in the middle of the request, then again in the middle of the response const events = run([whole.subarray(0, 3), whole.subarray(3, 11), whole.subarray(11)]);
expect(frames(events)).toHaveLength(2); });
it("recovers frames arriving one byte at a time", () => { const whole = concat(relay.readBaudRequest, relay.readBaudResponse); const events = run(Array.from(whole, (byte) => Uint8Array.of(byte)));
expect(frames(events)).toHaveLength(2); });
/** * The reason the monitor tracks whose turn it is. A read request and the echo of a single write * are both eight bytes, so length alone cannot tell them apart — only the alternation can */ it("does not mistake a request for a response of the same length", () => { const events = run([concat(relay.closeRelayRequest, relay.closeRelayResponse)]); const seen = frames(events);
expect(seen.map((frame) => frame.role)).toEqual(["request", "response"]); });
/** * A read response carries values but never the addresses they came from. Pairing is the only * way the UI can say which register a number belongs to */ it("pairs a response with the request it answers", () => { const events = run([concat(temperatureSensor.readBothRequest, temperatureSensor.readBothResponse)]); const seen = frames(events);
expect(seen[1]!.inReplyTo?.decoded).toEqual({ kind: "read", of: "inputRegisters", start: 0x0001, quantity: 2, }); });
it("does not pair a response from a different device", () => { const events = run([concat(temperatureSensor.readBothRequest, relay.readBaudResponse)]); const seen = frames(events);
expect(seen[1]!.inReplyTo).toBeUndefined(); });
/** * Joining a live bus lands in the middle of a frame, which is the normal case rather than the * exceptional one: the app connects whenever the user clicks, not between frames */ it("resynchronises after joining mid-frame", () => { const whole = concat(relay.readBaudRequest, relay.readBaudResponse); // Start three bytes into the request, so the first frame is unrecoverable const events = run([whole.subarray(3)]); const seen = frames(events);
expect(seen).toHaveLength(1); expect(seen[0]).toMatchObject({ role: "response", functionCode: 0x03 }); });
/** * The guard on the scan-ahead. A long read response arrives over several chunks, and while it is * in flight the *request* interpretation of its head is both long enough to check and wrong. If * that alone were taken as grounds to go looking further along, a legitimate frame would be cut * apart on nothing more than a coincidental checksum */ it("does not cut apart a long response that is still arriving", () => { // 20 input registers, the shape of a RadiCal status poll const values = Array.from({ length: 20 }, (_, index) => 0x1000 + index); const response = registerResponse(0x02, values); const monitor = new BusMonitor();
monitor.push(readRequest(0x02, 0xd010, values.length), 0);
// Delivered a few bytes at a time, well inside the gap for (let offset = 0; offset < response.length; offset += 5) { monitor.push(response.subarray(offset, offset + 5), 1 + offset); }
const events = monitor.push(new Uint8Array(0), 100); expect(events.filter((event) => event.type === "noise")).toHaveLength(0);
// Re-run in one piece to confirm the frame itself is what we expected all along const whole = frames(run([concat(readRequest(0x02, 0xd010, values.length), response)])); expect(whole).toHaveLength(2); expect(whole[1]!.decoded).toMatchObject({ kind: "registers", values }); });
it("reports the bytes it could not account for instead of dropping them", () => { const events = run([concat(bytes("DE AD BE EF"), relay.readAddressRequest)]);
const noise = events.filter((event) => event.type === "noise"); expect(noise).toHaveLength(1); expect(frames(events)).toHaveLength(1); // Reported before the frame that followed it, so the order matches the wire expect(events[0]!.type).toBe("noise"); });
it("abandons a partial frame after a silence rather than letting it swallow the next one", () => { const monitor = new BusMonitor({ gapMs: 10 });
// Half a request, then nothing for long enough that it can never complete expect(frames(monitor.push(relay.readBaudRequest.subarray(0, 4), 0))).toHaveLength(0);
const after = monitor.push(relay.readAddressRequest, 100);
expect(frames(after)).toHaveLength(1); expect(frames(after)[0]).toMatchObject({ address: 0x00, functionCode: 0x03 }); });
it("keeps a frame that merely straddles a chunk boundary within the gap", () => { const monitor = new BusMonitor({ gapMs: 50 });
monitor.push(relay.readBaudRequest.subarray(0, 4), 0); const after = monitor.push(relay.readBaudRequest.subarray(4), 5);
expect(frames(after)).toHaveLength(1); });
it("decodes a whole conversation with several devices", () => { const events = run([ concat( relay.readRelayStateRequest, relay.readRelayStateResponse, temperatureSensor.readBothRequest, temperatureSensor.readBothResponse, relay.closeRelayRequest, relay.closeRelayResponse, ), ]); const seen = frames(events);
expect(seen).toHaveLength(6); expect(seen.map((frame) => frame.role)).toEqual([ "request", "response", "request", "response", "request", "response", ]); expect(seen.map((frame) => frame.address)).toEqual([0xff, 0xff, 0x01, 0x01, 0xff, 0xff]); });
it("hands back what is left when the port closes", () => { const monitor = new BusMonitor(); monitor.push(relay.readBaudRequest.subarray(0, 4), 0);
const remaining = monitor.flush(1);
expect(remaining).toHaveLength(1); expect(remaining[0]).toMatchObject({ type: "noise" }); });
it("copies frames out of the working buffer", () => { const source = concat(relay.readAddressRequest, relay.readAddressResponse); const seen = frames(run([source]));
// Scribbling on the source must not change what was already reported source.fill(0);
expect(Array.from(seen[0]!.bytes)).toEqual(Array.from(relay.readAddressRequest)); });});
/** A read-input-registers response carrying `values`, built the way a device would */function registerResponse(address: number, values: readonly number[]): Uint8Array { const frame = new Uint8Array(5 + values.length * 2); const view = new DataView(frame.buffer);
frame[0] = address; frame[1] = 0x04; frame[2] = values.length * 2; values.forEach((value, index) => view.setUint16(3 + index * 2, value)); append(frame);
return frame;}
function readRequest(address: number, start: number, quantity: number): Uint8Array { return readInputRegisters(address, start, quantity);}
function concat(...parts: Uint8Array[]): Uint8Array { const joined = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); let offset = 0;
for (const part of parts) { joined.set(part, offset); offset += part.length; }
return joined;}