inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
193 lines (170 loc) • 5.62 kB
text/typescript
import type { ZWaveLogContainer } from "@zwave-js/core";
import { Mixin } from "@zwave-js/shared";
import { isObject } from "alcalzone-shared/typeguards";
import { EventEmitter } from "events";
import { Duplex, PassThrough, Readable, Writable } from "stream";
import { SerialLogger } from "./Logger";
import { MessageHeaders } from "./MessageHeaders";
import { SerialAPIParser } from "./SerialAPIParser";
export type ZWaveSerialChunk =
| MessageHeaders.ACK
| MessageHeaders.NAK
| MessageHeaders.CAN
| Buffer;
export interface ZWaveSerialPortEventCallbacks {
error: (e: Error) => void;
data: (data: ZWaveSerialChunk) => void;
}
export type ZWaveSerialPortEvents = Extract<
keyof ZWaveSerialPortEventCallbacks,
string
>;
export interface ZWaveSerialPortBase {
on<TEvent extends ZWaveSerialPortEvents>(
event: TEvent,
callback: ZWaveSerialPortEventCallbacks[TEvent],
): this;
addListener<TEvent extends ZWaveSerialPortEvents>(
event: TEvent,
callback: ZWaveSerialPortEventCallbacks[TEvent],
): this;
once<TEvent extends ZWaveSerialPortEvents>(
event: TEvent,
callback: ZWaveSerialPortEventCallbacks[TEvent],
): this;
off<TEvent extends ZWaveSerialPortEvents>(
event: TEvent,
callback: ZWaveSerialPortEventCallbacks[TEvent],
): this;
removeListener<TEvent extends ZWaveSerialPortEvents>(
event: TEvent,
callback: ZWaveSerialPortEventCallbacks[TEvent],
): this;
removeAllListeners(event?: ZWaveSerialPortEvents): this;
emit<TEvent extends ZWaveSerialPortEvents>(
event: TEvent,
...args: Parameters<ZWaveSerialPortEventCallbacks[TEvent]>
): boolean;
}
export function isZWaveSerialPortImplementation(
obj: unknown,
): obj is ZWaveSerialPortImplementation {
return (
isObject(obj) &&
typeof obj.create === "function" &&
typeof obj.open === "function" &&
typeof obj.close === "function"
);
}
export interface ZWaveSerialPortImplementation {
create(): Duplex & EventEmitter;
open(
port: ReturnType<ZWaveSerialPortImplementation["create"]>,
): Promise<void>;
close(
port: ReturnType<ZWaveSerialPortImplementation["create"]>,
): Promise<void>;
}
// This is basically a duplex transform stream wrapper around any stream (network, serial, ...)
// 0 ┌─────────────────┐ ┌─────────────────┐ ┌──
// 1 <-- <-- PassThrough <-- write
// 1 │ any stream │ │ ZWaveSerialPort │ │
// 0 --> --> SerialAPIParser --> read
// 1 └─────────────────┘ └─────────────────┘ └──
// The implementation idea is based on https://stackoverflow.com/a/17476600/10179833
export class ZWaveSerialPortBase extends PassThrough {
protected serial: ReturnType<ZWaveSerialPortImplementation["create"]>;
private parser: SerialAPIParser;
protected logger: SerialLogger;
// Allow strongly-typed async iteration
public declare [Symbol.asyncIterator]: () => AsyncIterableIterator<ZWaveSerialChunk>;
constructor(
private implementation: ZWaveSerialPortImplementation,
loggers: ZWaveLogContainer,
) {
super({ readableObjectMode: true });
// Route the data event handlers to the parser and handle everything else ourselves
for (const method of [
"on",
"once",
"off",
"addListener",
"removeListener",
"removeAllListeners",
] as const) {
const original = this[method].bind(this);
this[method] = (event: any, ...args: any[]) => {
if (event === "data") {
// @ts-expect-error
this.parser[method]("data", ...args);
} else {
(original as any)(event, ...args);
}
return this;
};
}
this.logger = new SerialLogger(loggers);
this.serial = implementation.create().on("error", (e) => {
// Pass errors through
this.emit("error", e);
});
// Hook up a parser to the serial port
this.parser = new SerialAPIParser(this.logger);
this.serial.pipe(this.parser);
// When the wrapper is piped to a stream, pipe the parser instead
this.pipe = this.parser.pipe.bind(this.parser);
this.unpipe = (destination) => {
this.parser.unpipe(destination);
return this;
};
// When something is piped to us, pipe it to the serial port instead
// Also pass all written data to the serialport unchanged
this.on("pipe" as any, (source: Readable) => {
source.unpipe(this as any);
// Pass all written data to the serialport unchanged
source.pipe(this.serial as unknown as Writable, { end: false });
});
// Delegate iterating to the parser stream
this[Symbol.asyncIterator] = () => this.parser[Symbol.asyncIterator]();
}
public open(): Promise<void> {
return this.implementation.open(this.serial).then(() => {
this._isOpen = true;
});
}
public close(): Promise<void> {
this._isOpen = false;
return this.implementation.close(this.serial);
}
private _isOpen: boolean = false;
public get isOpen(): boolean {
return this._isOpen;
}
public async writeAsync(data: Buffer): Promise<void> {
if (!this.isOpen) {
throw new Error("The serial port is not open!");
}
if (data.length === 1) {
switch (data[0]) {
case MessageHeaders.ACK:
this.logger.ACK("outbound");
break;
case MessageHeaders.CAN:
this.logger.CAN("outbound");
break;
case MessageHeaders.NAK:
this.logger.NAK("outbound");
break;
}
} else {
this.logger.data("outbound", data);
}
return new Promise((resolve, reject) => {
this.serial.write(data, (err) => {
if (err) reject(err);
else resolve();
});
});
}
}