zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
429 lines (365 loc) • 15.6 kB
text/typescript
/* v8 ignore start */
import EventEmitter from "node:events";
import {Socket} from "node:net";
import {Queue, Waitress, wait} from "../../utils";
import {logger} from "../../utils/logger";
import {SerialPort} from "../serialPort";
import SocketPortUtils from "../socketPortUtils";
import type {SerialPortOptions} from "../tstype";
import {SIGNATURE, ZBOSS_FLAG_FIRST_FRAGMENT, ZBOSS_FLAG_LAST_FRAGMENT, ZBOSS_NCP_API_HL} from "./consts";
import {type ZBOSSFrame, readZBOSSFrame, writeZBOSSFrame} from "./frame";
import {ZBOSSReader} from "./reader";
import {crc8, crc16} from "./utils";
import {ZBOSSWriter} from "./writer";
const NS = "zh:zboss:uart";
export class ZBOSSUart extends EventEmitter {
private readonly portOptions: SerialPortOptions;
private serialPort?: SerialPort;
private socketPort?: Socket;
private writer: ZBOSSWriter;
private reader: ZBOSSReader;
private closing = false;
private sendSeq = 0; // next frame number to send
private recvSeq = 0; // next frame number to receive
private ackSeq = 0; // next number after the last accepted frame
private waitress: Waitress<number, number>;
private queue: Queue;
public inReset = false;
constructor(options: SerialPortOptions) {
super();
this.portOptions = options;
this.serialPort = undefined;
this.socketPort = undefined;
this.writer = new ZBOSSWriter();
this.reader = new ZBOSSReader();
this.reader.on("data", this.onPackage.bind(this));
this.queue = new Queue(1);
this.waitress = new Waitress<number, number>(this.waitressValidator, this.waitressTimeoutFormatter);
}
public async resetNcp(): Promise<boolean> {
if (this.closing) {
return false;
}
logger.info("NCP reset", NS);
try {
if (!this.portOpen) {
await this.openPort();
}
return true;
} catch (err) {
logger.error(`Failed to init port with error ${err}`, NS);
return false;
}
}
get portOpen(): boolean | undefined {
if (this.closing) {
return false;
}
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
if (SocketPortUtils.isTcpPath(this.portOptions.path!)) {
return this.socketPort && !this.socketPort.closed;
}
return this.serialPort?.isOpen;
}
public async start(): Promise<boolean> {
if (!this.portOpen) {
return false;
}
logger.info("UART starting", NS);
try {
if (this.serialPort != null) {
// clear read/write buffers
await this.serialPort.asyncFlush();
}
} catch (err) {
logger.error(`Error while flushing before start: ${err}`, NS);
}
return true;
}
public async stop(): Promise<void> {
this.closing = true;
this.queue.clear();
await this.closePort();
this.closing = false;
logger.info("UART stopped", NS);
}
private async openPort(): Promise<void> {
await this.closePort();
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
if (!SocketPortUtils.isTcpPath(this.portOptions.path!)) {
const serialOpts = {
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
path: this.portOptions.path!,
baudRate: typeof this.portOptions.baudRate === "number" ? this.portOptions.baudRate : 115200,
rtscts: typeof this.portOptions.rtscts === "boolean" ? this.portOptions.rtscts : false,
autoOpen: false,
};
//@ts-expect-error Jest testing
if (this.portOptions.binding != null) {
//@ts-expect-error Jest testing
serialOpts.binding = this.portOptions.binding;
}
logger.debug(`Opening serial port with ${JSON.stringify(serialOpts)}`, NS);
this.serialPort = new SerialPort(serialOpts);
this.writer.pipe(this.serialPort);
this.serialPort.pipe(this.reader);
try {
await this.serialPort.asyncOpen();
logger.info("Serial port opened", NS);
this.serialPort.once("close", this.onPortClose.bind(this));
this.serialPort.on("error", this.onPortError.bind(this));
} catch (error) {
await this.stop();
throw error;
}
} else {
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
const info = SocketPortUtils.parseTcpPath(this.portOptions.path!);
logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS);
this.socketPort = new Socket();
this.socketPort.setNoDelay(true);
this.socketPort.setKeepAlive(true, 15000);
this.writer.pipe(this.socketPort);
this.socketPort.pipe(this.reader);
return await new Promise((resolve, reject): void => {
const openError = async (err: Error): Promise<void> => {
await this.stop();
reject(err);
};
this.socketPort?.on("connect", () => {
logger.debug("Socket connected", NS);
});
this.socketPort?.on("ready", (): void => {
logger.info("Socket ready", NS);
this.socketPort?.removeListener("error", openError);
this.socketPort?.once("close", this.onPortClose.bind(this));
this.socketPort?.on("error", this.onPortError.bind(this));
resolve();
});
this.socketPort?.once("error", openError);
this.socketPort?.connect(info.port, info.host);
});
}
}
public async closePort(): Promise<void> {
if (this.serialPort?.isOpen) {
try {
await this.serialPort.asyncFlushAndClose();
} catch (err) {
logger.error(`Failed to close serial port ${err}.`, NS);
}
this.serialPort.removeAllListeners();
this.serialPort = undefined;
} else if (this.socketPort !== undefined && !this.socketPort.closed) {
this.socketPort.destroy();
this.socketPort.removeAllListeners();
this.socketPort = undefined;
}
}
private async onPortClose(err: boolean | Error): Promise<void> {
logger.info(`Port closed. Error? ${err ?? "no"}`, NS);
if (this.inReset) {
await wait(3000);
await this.openPort();
this.inReset = false;
}
}
private onPortError(error: Error): void {
logger.info(`Port error: ${error}`, NS);
}
private async onPackage(data: Buffer): Promise<void> {
if (this.inReset) return;
const len = data.readUInt16LE(0);
const pType = data.readUInt8(2);
const pFlags = data.readUInt8(3);
const isACK = (pFlags & 0x1) === 1;
const retransmit = ((pFlags >> 1) & 0x1) === 1;
const sequence = (pFlags >> 2) & 0x3;
const ACKseq = (pFlags >> 4) & 0x3;
const isFirst = ((pFlags >> 6) & 0x1) === 1;
const isLast = ((pFlags >> 7) & 0x1) === 1;
logger.debug(
() =>
`<-- package type ${pType}, flags ${pFlags.toString(16)}` +
`${JSON.stringify({isACK, retransmit, sequence, ACKseq, isFirst, isLast})}`,
NS,
);
if (pType !== ZBOSS_NCP_API_HL) {
logger.error(`<-- Wrong package type: ${pType}`, NS);
return;
}
if (isACK) {
// ACKseq is received
this.handleACK(ACKseq);
return;
}
if (len <= 5) {
logger.debug("<-- Empty package", NS);
return;
}
// header crc
const hCRC = data.readUInt8(4);
const hCRC8 = crc8(data.subarray(0, 4));
if (hCRC !== hCRC8) {
logger.error(`<-- Wrong package header crc: is ${hCRC}, expected ${hCRC8}`, NS);
return;
}
// body crc
const bCRC = data.readUInt16LE(5);
const body = data.subarray(7);
const bodyCRC16 = crc16(body);
if (bCRC !== bodyCRC16) {
logger.error(`<-- Wrong package body crc: is ${bCRC}, expected ${bodyCRC16}`, NS);
return;
}
this.recvSeq = sequence;
// Send ACK
logger.debug(`--> ACK (${this.recvSeq})`, NS);
await this.sendACK(this.recvSeq);
try {
logger.debug(`<-- FRAME: ${body.toString("hex")}`, NS);
const frame = readZBOSSFrame(body);
if (frame) {
this.emit("frame", frame);
}
} catch (error) {
logger.debug(`<-- error ${(error as Error).stack}`, NS);
}
}
public async sendBuffer(buf: Buffer): Promise<void> {
try {
logger.debug(`--> FRAME: ${buf.toString("hex")}`, NS);
let flags = (this.sendSeq & 0x03) << 2; // sequence
flags = flags | ZBOSS_FLAG_FIRST_FRAGMENT | ZBOSS_FLAG_LAST_FRAGMENT;
const pack = this.makePack(flags, buf);
const isACK = (flags & 0x1) === 1;
const retransmit = ((flags >> 1) & 0x1) === 1;
const sequence = (flags >> 2) & 0x3;
const ACKseq = (flags >> 4) & 0x3;
const isFirst = ((flags >> 6) & 0x1) === 1;
const isLast = ((flags >> 7) & 0x1) === 1;
logger.debug(
() =>
`--> package type ${ZBOSS_NCP_API_HL}, flags ${flags.toString(16)}` +
`${JSON.stringify({isACK, retransmit, sequence, ACKseq, isFirst, isLast})}`,
NS,
);
logger.debug(`--> PACK: ${pack.toString("hex")}`, NS);
await this.sendDATA(pack);
} catch (error) {
logger.debug(`--> error ${(error as Error).stack}`, NS);
}
}
public async sendFrame(frame: ZBOSSFrame): Promise<void> {
return await this.sendBuffer(writeZBOSSFrame(frame));
}
private async sendDATA(data: Buffer, isACK = false): Promise<void> {
const seq = this.sendSeq;
const nextSeq = this.sendSeq;
const ackSeq = this.recvSeq;
return await this.queue.execute<void>(async (): Promise<void> => {
try {
logger.debug(`--> DATA (${seq},${ackSeq},0): ${data.toString("hex")}`, NS);
if (!isACK) {
const waiter = this.waitFor(nextSeq);
this.writeBuffer(data);
logger.debug(`-?- waiting (${nextSeq})`, NS);
if (!this.inReset) {
await waiter.start().promise;
}
logger.debug(`-+- waiting (${nextSeq}) success`, NS);
} else {
this.writeBuffer(data);
}
} catch (e1) {
logger.error(`--> Error: ${e1}`, NS);
logger.error(`-!- break waiting (${nextSeq})`, NS);
logger.error(`Can't send DATA frame (${seq},${ackSeq},0): ${data.toString("hex")}`, NS);
throw new Error(`sendDATA error: try 1: ${e1}`);
// try {
// await Wait(500);
// const waiter = this.waitFor(nextSeq);
// logger.debug(`->> DATA (${seq},${ackSeq},1): ${data.toString('hex')}`, NS);
// this.writeBuffer(data);
// logger.debug(`-?- rewaiting (${nextSeq})`, NS);
// await waiter.start().promise;
// logger.debug(`-+- rewaiting (${nextSeq}) success`, NS);
// } catch (e2) {
// logger.error(`--> Error: ${e2}`, NS);
// logger.error(`-!- break rewaiting (${nextSeq})`, NS);
// logger.error(`Can't resend DATA frame (${seq},${ackSeq},1): ${data.toString('hex')}`, NS);
// throw new Error(`sendDATA error: try 1: ${e1}, try 2: ${e2}`);
// }
}
});
}
private handleACK(ackSeq: number): boolean {
/* Handle an acknowledgement package */
// next number after the last accepted package
this.ackSeq = ackSeq & 0x03;
logger.debug(`<-- ACK (${this.ackSeq})`, NS);
const handled = this.waitress.resolve(this.ackSeq);
if (!handled && this.sendSeq !== this.ackSeq) {
// Packet confirmation received for {ackSeq}, but was expected {sendSeq}
// This happens when the chip has not yet received of the packet {sendSeq} from us,
// but has already sent us the next one.
logger.debug(`Unexpected packet sequence ${this.ackSeq} | ${this.sendSeq}`, NS);
} else {
// next
this.sendSeq = {0: 1, 1: 2, 2: 3, 3: 1}[this.sendSeq] || 1;
}
return handled;
}
private async sendACK(ackNum: number, retransmit = false): Promise<void> {
/* Construct a acknowledgement package */
let flags = (ackNum & 0x03) << 4; // ACKseq
flags |= 0x01; // isACK
if (retransmit) {
flags |= 0x02; // retransmit
}
const ackPackage = this.makePack(flags, undefined);
const isACK = (flags & 0x1) === 1;
const sequence = (flags >> 2) & 0x3;
const ACKseq = (flags >> 4) & 0x3;
const isFirst = ((flags >> 6) & 0x1) === 1;
const isLast = ((flags >> 7) & 0x1) === 1;
logger.debug(
() =>
`--> package type ${ZBOSS_NCP_API_HL}, flags ${flags.toString(16)}` +
`${JSON.stringify({isACK, retransmit, sequence, ACKseq, isFirst, isLast})}`,
NS,
);
logger.debug(`--> ACK: ${ackPackage.toString("hex")}`, NS);
await this.sendDATA(ackPackage, true);
}
private writeBuffer(buffer: Buffer): void {
logger.debug(`--> [${buffer.toString("hex")}]`, NS);
this.writer.push(buffer);
}
private makePack(flags: number, data?: Buffer): Buffer {
/* Construct a package */
const packLen = 5 + (data ? data.length + 2 : 0);
const header = Buffer.alloc(7);
header.writeUInt16BE(SIGNATURE);
header.writeUInt16LE(packLen, 2);
header.writeUInt8(ZBOSS_NCP_API_HL, 4);
header.writeUInt8(flags, 5);
const hCRC8 = crc8(header.subarray(2, 6));
header.writeUInt8(hCRC8, 6);
if (data) {
const pCRC16 = Buffer.alloc(2);
pCRC16.writeUInt16LE(crc16(data));
return Buffer.concat([header, pCRC16, data]);
}
return header;
}
private waitFor(sequence: number, timeout = 2000): {start: () => {promise: Promise<number>; ID: number}; ID: number} {
return this.waitress.waitFor(sequence, timeout);
}
private waitressTimeoutFormatter(matcher: number, timeout: number): string {
return `${matcher} after ${timeout}ms`;
}
private waitressValidator(sequence: number, matcher: number): boolean {
return sequence === matcher;
}
}