diff --git a/packages/serial/src/index.ts b/packages/serial/src/index.ts index 9721ee83f0e4..295b05ed49d8 100644 --- a/packages/serial/src/index.ts +++ b/packages/serial/src/index.ts @@ -8,13 +8,16 @@ export * from "./message/SuccessIndicator.js"; export * from "./message/ZnifferMessages.js"; export * from "./parsers/BootloaderParsers.js"; export * from "./parsers/SerialAPIParser.js"; -export * from "./serialport/ZWaveSerialPort.js"; -export * from "./serialport/ZWaveSerialPortBase.js"; +export * from "./parsers/ZWaveSerialFrame.js"; +export * from "./parsers/ZnifferSerialFrame.js"; +export * from "./plumbing/Faucet.js"; +export * from "./serialport/LegacyBindingWrapper.js"; +export * from "./serialport/NodeSerialPort.js"; +export * from "./serialport/NodeSocket.js"; export * from "./serialport/ZWaveSerialPortImplementation.js"; -export * from "./serialport/ZWaveSocket.js"; +export * from "./serialport/ZWaveSerialStream.js"; export * from "./serialport/ZWaveSocketOptions.js"; -export * from "./zniffer/ZnifferSerialPort.js"; -export * from "./zniffer/ZnifferSerialPortBase.js"; -export * from "./zniffer/ZnifferSocket.js"; +export * from "./serialport/definitions.js"; +export * from "./zniffer/ZnifferSerialStream.js"; export * from "./index_serialapi.js"; diff --git a/packages/serial/src/index_mock.ts b/packages/serial/src/index_mock.ts index e077354e3e17..59e501aeb6cc 100644 --- a/packages/serial/src/index_mock.ts +++ b/packages/serial/src/index_mock.ts @@ -1,3 +1,3 @@ -export * from "./mock/MockSerialPort.js"; +export * from "./mock/MockPort.js"; export * from "./mock/SerialPortBindingMock.js"; export * from "./mock/SerialPortMock.js"; diff --git a/packages/serial/src/mock/MockPort.ts b/packages/serial/src/mock/MockPort.ts new file mode 100644 index 000000000000..17f2756acc7a --- /dev/null +++ b/packages/serial/src/mock/MockPort.ts @@ -0,0 +1,81 @@ +import { ZWaveLogContainer } from "@zwave-js/core"; +import type { UnderlyingSink, UnderlyingSource } from "node:stream/web"; +import { + type ZWaveSerialBindingFactory, + type ZWaveSerialStream, + ZWaveSerialStreamFactory, +} from "../serialport/ZWaveSerialStream.js"; + +export class MockPort { + public constructor() { + const { readable, writable: sink } = new TransformStream(); + this.#sink = sink; + this.readable = readable; + } + + // Remembers the last written data + public lastWrite: Uint8Array | undefined; + + // Internal stream to allow emitting data from the port + #sourceController: ReadableStreamDefaultController | undefined; + + // Public readable stream to allow handling the written data + #sink: WritableStream; + /** Exposes the data written by the host as a readable stream */ + public readonly readable: ReadableStream; + + public factory(): ZWaveSerialBindingFactory { + return () => { + const sink: UnderlyingSink = { + write: async (chunk, _controller) => { + // Remember the last written data + this.lastWrite = chunk; + // Only write to the sink if its readable side has a reader attached. + // Otherwise, we get backpressure on the writable side of the mock port + if (this.readable.locked) { + const writer = this.#sink.getWriter(); + try { + await writer.write(chunk); + } finally { + writer.releaseLock(); + } + } + }, + }; + + const source: UnderlyingSource = { + start: (controller) => { + this.#sourceController = controller; + }, + }; + + return Promise.resolve({ sink, source }); + }; + } + + public emitData(data: Uint8Array): void { + this.#sourceController?.enqueue(data); + } + + public destroy(): void { + try { + this.#sourceController?.close(); + this.#sourceController = undefined; + } catch { + // Ignore - the controller might already be closed + } + } +} + +export async function createAndOpenMockedZWaveSerialPort(): Promise<{ + port: MockPort; + serial: ZWaveSerialStream; +}> { + const port = new MockPort(); + const factory = new ZWaveSerialStreamFactory( + port.factory(), + new ZWaveLogContainer({ enabled: false }), + ); + const serial = await factory.createStream(); + return { port, serial }; +} diff --git a/packages/serial/src/mock/MockSerialPort.ts b/packages/serial/src/mock/_MockSerialPort.ts.txt similarity index 100% rename from packages/serial/src/mock/MockSerialPort.ts rename to packages/serial/src/mock/_MockSerialPort.ts.txt diff --git a/packages/serial/src/parsers/BootloaderParsers.ts b/packages/serial/src/parsers/BootloaderParsers.ts index e35a7d209584..6f36b74f7d1b 100644 --- a/packages/serial/src/parsers/BootloaderParsers.ts +++ b/packages/serial/src/parsers/BootloaderParsers.ts @@ -1,39 +1,13 @@ -import { Transform, type TransformCallback } from "node:stream"; +import { Bytes } from "@zwave-js/shared"; +import { type Transformer } from "node:stream/web"; import type { SerialLogger } from "../log/Logger.js"; import { XModemMessageHeaders } from "../message/MessageHeaders.js"; - -export enum BootloaderChunkType { - Error, - Menu, - Message, - FlowControl, -} - -export type BootloaderChunk = - | { - type: BootloaderChunkType.Error; - error: string; - _raw: string; - } - | { - type: BootloaderChunkType.Menu; - version: string; - options: { num: number; option: string }[]; - _raw: string; - } - | { - type: BootloaderChunkType.Message; - message: string; - _raw: string; - } - | { - type: BootloaderChunkType.FlowControl; - command: - | XModemMessageHeaders.ACK - | XModemMessageHeaders.NAK - | XModemMessageHeaders.CAN - | XModemMessageHeaders.C; - }; +import { + type BootloaderChunk, + BootloaderChunkType, + type ZWaveSerialFrame, + ZWaveSerialFrameType, +} from "./ZWaveSerialFrame.js"; function isFlowControl(byte: number): boolean { return ( @@ -44,27 +18,24 @@ function isFlowControl(byte: number): boolean { ); } -/** Parses the screen output from the bootloader, either waiting for a NUL char or a timeout */ -export class BootloaderScreenParser extends Transform { - constructor(private logger?: SerialLogger) { - // We read byte streams but emit messages - super({ readableObjectMode: true }); - } +class BootloaderScreenParserTransformer + implements Transformer +{ + constructor(private logger?: SerialLogger) {} private receiveBuffer = ""; private flushTimeout: NodeJS.Timeout | undefined; - _transform( - chunk: any, - encoding: string, - callback: TransformCallback, - ): void { + transform( + chunk: Uint8Array, + controller: TransformStreamDefaultController, + ) { if (this.flushTimeout) { clearTimeout(this.flushTimeout); this.flushTimeout = undefined; } - this.receiveBuffer += chunk.toString("utf8"); + this.receiveBuffer += Bytes.view(chunk).toString("utf8"); // Correct buggy ordering of NUL char in error codes. // The bootloader may send errors as "some error 0x\012" instead of "some error 0x12\0" @@ -82,7 +53,7 @@ export class BootloaderScreenParser extends Transform { if (screen === "") continue; this.logger?.bootloaderScreen(screen); - this.push(screen); + controller.enqueue(screen); } // Emit single flow-control bytes @@ -91,7 +62,7 @@ export class BootloaderScreenParser extends Transform { if (!isFlowControl(charCode)) break; this.logger?.data("inbound", Uint8Array.from([charCode])); - this.push(charCode); + controller.enqueue(charCode); this.receiveBuffer = this.receiveBuffer.slice(1); } @@ -99,12 +70,21 @@ export class BootloaderScreenParser extends Transform { if (this.receiveBuffer) { this.flushTimeout = setTimeout(() => { this.flushTimeout = undefined; - this.push(this.receiveBuffer); + controller.enqueue(this.receiveBuffer); this.receiveBuffer = ""; }, 500); } + } +} - callback(); +/** Parses the screen output from the bootloader, either waiting for a NUL char or a timeout */ +export class BootloaderScreenParser + extends TransformStream +{ + constructor( + logger?: SerialLogger, + ) { + super(new BootloaderScreenParserTransformer(logger)); } } @@ -116,77 +96,84 @@ const menuSuffix = "BL >"; const optionsRegex = /^(?\d+)\. (?