UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

429 lines (365 loc) 15.6 kB
/* 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; } }