UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,121 lines 69.8 kB
"use strict"; /* v8 ignore start */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UartAsh = exports.CONFIG_TX_K = void 0; const node_events_1 = require("node:events"); const node_net_1 = require("node:net"); const utils_1 = require("../../../utils"); const logger_1 = require("../../../utils/logger"); const serialPort_1 = require("../../serialPort"); const socketPortUtils_1 = __importDefault(require("../../socketPortUtils")); const enums_1 = require("../enums"); const math_1 = require("../utils/math"); const consts_1 = require("./consts"); const enums_2 = require("./enums"); const parser_1 = require("./parser"); const queues_1 = require("./queues"); const writer_1 = require("./writer"); const NS = "zh:ember:uart:ash"; /** ASH get rflag in control byte */ // const ashGetRFlag = (ctrl: number): number => (ctrl & ASH_RFLAG_MASK) >> ASH_RFLAG_BIT; /** ASH get nflag in control byte */ // const ashGetNFlag = (ctrl: number): number => (ctrl & ASH_NFLAG_MASK) >> ASH_NFLAG_BIT; /** ASH get frmnum in control byte */ const ashGetFrmNum = (ctrl) => (ctrl & consts_1.ASH_FRMNUM_MASK) >> consts_1.ASH_FRMNUM_BIT; /** ASH get acknum in control byte */ const ashGetACKNum = (ctrl) => (ctrl & consts_1.ASH_ACKNUM_MASK) >> consts_1.ASH_ACKNUM_BIT; var SendState; (function (SendState) { SendState[SendState["IDLE"] = 0] = "IDLE"; SendState[SendState["SHFRAME"] = 1] = "SHFRAME"; SendState[SendState["TX_DATA"] = 2] = "TX_DATA"; SendState[SendState["RETX_DATA"] = 3] = "RETX_DATA"; })(SendState || (SendState = {})); // Bits in ashFlags var Flag; (function (Flag) { /** Reject Condition */ Flag[Flag["REJ"] = 1] = "REJ"; /** Retransmit Condition */ Flag[Flag["RETX"] = 2] = "RETX"; /** send NAK */ Flag[Flag["NAK"] = 4] = "NAK"; /** send ACK */ Flag[Flag["ACK"] = 8] = "ACK"; /** send RST */ Flag[Flag["RST"] = 16] = "RST"; /** send immediate CAN */ Flag[Flag["CAN"] = 32] = "CAN"; /** in CONNECTED state, else ERROR */ Flag[Flag["CONNECTED"] = 64] = "CONNECTED"; /** not ready to receive DATA frames */ Flag[Flag["NR"] = 256] = "NR"; /** last transmitted NR status */ Flag[Flag["NRTX"] = 512] = "NRTX"; })(Flag || (Flag = {})); /** max frames sent without being ACKed (1-7) */ exports.CONFIG_TX_K = 3; /** enables randomizing DATA frame payloads */ const CONFIG_RANDOMIZE = true; /** adaptive rec'd ACK timeout initial value */ const CONFIG_ACK_TIME_INIT = 800; /** " " " " " minimum value */ const CONFIG_ACK_TIME_MIN = 400; /** " " " " " maximum value */ const CONFIG_ACK_TIME_MAX = 2400; /** time allowed to receive RSTACK after ncp is reset */ const CONFIG_TIME_RST = 2500; /** time between checks for received RSTACK (CONNECTED status) */ const CONFIG_TIME_RST_CHECK = 100; /** if free buffers < limit, host receiver isn't ready, will hold off the ncp from sending normal priority frames */ const CONFIG_NR_LOW_LIMIT = 8; // RX_FREE_LW /** if free buffers > limit, host receiver is ready */ const CONFIG_NR_HIGH_LIMIT = 12; // RX_FREE_HW /** time until a set nFlag must be resent (max 2032) */ const CONFIG_NR_TIME = 480; /** Read/write max bytes count at stream level */ const CONFIG_HIGHWATER_MARK = 256; /** * ASH Protocol handler. */ class UartAsh extends node_events_1.EventEmitter { portOptions; serialPort; socketPort; writer; parser; /** True when serial/socket is currently closing. */ closing; /** time ackTimer started: 0 means not ready uint16_t */ ackTimer; /** time used to check ackTimer expiry (msecs) uint16_t */ ackPeriod; /** not ready timer (16 msec units). Set to (now + config.nrTime) when started. uint8_t */ nrTimer; /** frame decode in progress */ decodeInProgress; // Variables used in encoding frames /** true when preceding byte was escaped */ encodeEscFlag; /** byte to send after ASH_ESC uint8_t */ encodeFlip; /** uint16_t */ encodeCrc; /** encoder state: 0 = control/data bytes, 1 = crc low byte, 2 = crc high byte, 3 = flag. uint8_t */ encodeState; /** bytes remaining to encode. uint8_t */ encodeCount; // Variables used in decoding frames /** bytes in frame, plus CRC, clamped to limit +1: high values also used to record certain errors. uint8_t */ decodeLen; /** ASH_FLIP if previous byte was ASH_ESC. uint8_t */ decodeFlip; /** a 2 byte queue to avoid outputting crc bytes. uint8_t */ decodeByte1; /** at frame end, they contain the received crc. uint8_t */ decodeByte2; /** uint16_t */ decodeCrc; /** outgoing short frames */ txSHBuffer; /** incoming short frames */ rxSHBuffer; /** bit flags for top-level logic. uint16_t */ flags; /** frame ack'ed from remote peer. uint8_t */ ackRx; /** frame ack'ed to remote peer. uint8_t */ ackTx; /** next frame to be transmitted. uint8_t */ frmTx; /** next frame to be retransmitted. uint8_t */ frmReTx; /** next frame expected to be rec'd. uint8_t */ frmRx; /** frame at retx queue's head. uint8_t */ frmReTxHead; /** consecutive timeout counter. uint8_t */ timeouts; /** rec'd DATA frame buffer. uint8_t */ rxDataBuffer; /** rec'd frame length. uint8_t */ rxLen; /** tx frame offset. uint8_t */ txOffset; counters; /** * Errors reported by the NCP. * The `NcpFailedCode` from the frame reporting this is logged before this is set to make it clear where it failed: * - The NCP sent an ERROR frame during the initial reset sequence (before CONNECTED state) * - The NCP sent an ERROR frame * - The NCP sent an unexpected RSTACK */ ncpError; /** Errors reported by the Host. */ hostError; /** sendExec() state variable */ sendState; /** NCP is enabled to sleep, set by EZSP, not supported atm, always false */ ncpSleepEnabled; /** * Set when the ncp has indicated it has a pending callback by seting the callback flag in the frame control byte * or (uart version only) by sending an an ASH_WAKE byte between frames. */ ncpHasCallbacks; /** Transmit buffers */ txPool; txQueue; reTxQueue; txFree; /** Receive buffers */ rxPool; rxQueue; rxFree; constructor(options) { super(); this.portOptions = options; this.serialPort = undefined; this.socketPort = undefined; this.writer = new writer_1.AshWriter({ highWaterMark: CONFIG_HIGHWATER_MARK }); this.parser = new parser_1.AshParser({ readableHighWaterMark: CONFIG_HIGHWATER_MARK }); this.txPool = new Array(consts_1.TX_POOL_BUFFERS); this.txQueue = new queues_1.EzspQueue(); this.reTxQueue = new queues_1.EzspQueue(); this.txFree = new queues_1.EzspFreeList(); this.rxPool = new Array(consts_1.EZSP_HOST_RX_POOL_SIZE); this.rxQueue = new queues_1.EzspQueue(); this.rxFree = new queues_1.EzspFreeList(); this.closing = false; this.txSHBuffer = Buffer.alloc(consts_1.SH_TX_BUFFER_LEN); this.rxSHBuffer = Buffer.alloc(consts_1.SH_RX_BUFFER_LEN); this.ackTimer = 0; this.ackPeriod = 0; this.nrTimer = 0; this.flags = 0; this.decodeInProgress = false; this.ackRx = 0; this.ackTx = 0; this.frmTx = 0; this.frmReTx = 0; this.frmRx = 0; this.frmReTxHead = 0; this.timeouts = 0; this.rxDataBuffer = undefined; this.rxLen = 0; // init to "start of frame" default this.encodeCount = 0; this.encodeState = 0; this.encodeEscFlag = false; this.encodeFlip = 0; this.encodeCrc = 0xffff; this.txOffset = 0; // init to "start of frame" default this.decodeLen = 0; this.decodeByte1 = 0; this.decodeByte2 = 0; this.decodeFlip = 0; this.decodeCrc = 0xffff; this.ncpError = enums_1.EzspStatus.NO_ERROR; this.hostError = enums_1.EzspStatus.NO_ERROR; this.sendState = SendState.IDLE; this.ncpSleepEnabled = false; this.ncpHasCallbacks = false; this.stopAckTimer(); this.stopNrTimer(); this.counters = { txData: 0, txAllFrames: 0, txDataFrames: 0, txAckFrames: 0, txNakFrames: 0, txReDataFrames: 0, // txN0Frames: 0, txN1Frames: 0, txCancelled: 0, rxData: 0, rxAllFrames: 0, rxDataFrames: 0, rxAckFrames: 0, rxNakFrames: 0, rxReDataFrames: 0, // rxN0Frames: 0, rxN1Frames: 0, rxCancelled: 0, rxCrcErrors: 0, rxCommErrors: 0, rxTooShort: 0, rxTooLong: 0, rxBadControl: 0, rxBadLength: 0, rxBadAckNumber: 0, rxNoBuffer: 0, rxDuplicates: 0, rxOutOfSequence: 0, rxAckTimeouts: 0, }; // All transmit buffers are put into txFree, and txQueue and reTxQueue are empty. this.txQueue.tail = undefined; this.reTxQueue.tail = undefined; this.txFree.link = undefined; for (let i = 0; i < consts_1.TX_POOL_BUFFERS; i++) { this.txPool[i] = new queues_1.EzspBuffer(); this.txFree.freeBuffer(this.txPool[i]); } // All receive buffers are put into rxFree, and rxQueue is empty. this.rxQueue.tail = undefined; this.rxFree.link = undefined; for (let i = 0; i < consts_1.EZSP_HOST_RX_POOL_SIZE; i++) { this.rxPool[i] = new queues_1.EzspBuffer(); this.rxFree.freeBuffer(this.rxPool[i]); } } /** * Check if port is valid, open, and not closing. */ get portOpen() { if (this.closing) { return false; } // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` if (socketPortUtils_1.default.isTcpPath(this.portOptions.path)) { return this.socketPort ? !this.socketPort.closed : false; } return this.serialPort ? this.serialPort.isOpen : false; } /** * Get max wait time before response is considered timed out. */ get responseTimeout() { return consts_1.ASH_MAX_TIMEOUTS * CONFIG_ACK_TIME_MAX; } /** * Indicates if the host is in the Connected state. * If not, the host and NCP cannot exchange DATA frames. * Note that this function does not actively confirm that communication with NCP is healthy, but simply returns its last known status. * * @returns * - true - host and NCP can exchange DATA frames * - false - host and NCP cannot now exchange DATA frames */ get connected() { return (this.flags & Flag.CONNECTED) !== 0; } /** * Has nothing to do... */ get idle() { return (!this.decodeInProgress && // don't have a partial frame // && (this.serial.readAvailable() === EzspStatus.NO_RX_DATA) // no rx data this.rxQueue.empty && // no rx frames to process !this.ncpHasCallbacks && // no pending callbacks this.flags === Flag.CONNECTED && // no pending ACKs, NAKs, etc. this.ackTx === this.frmRx && // do not need to send an ACK this.ackRx === this.frmTx && // not waiting to receive an ACK this.sendState === SendState.IDLE && // nothing being transmitted now this.txQueue.empty // nothing waiting to transmit // && this.serial.outputIsIdle() // nothing in OS buffers or UART FIFO ); } /** * Init the serial or socket port and hook parser/writer. * NOTE: This is the only function that throws/rejects in the ASH layer (caught by resetNcp and turned into an EzspStatus). */ async initPort() { await this.closePort(); // will do nothing if nothing's open // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` if (!socketPortUtils_1.default.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, parity: "none", stopBits: 1, xon: false, xoff: false, }; // enable software flow control if RTS/CTS not enabled in config if (!serialOpts.rtscts) { logger_1.logger.info("RTS/CTS config is off, enabling software flow control.", NS); serialOpts.xon = true; serialOpts.xoff = true; } // @ts-expect-error Jest testing if (this.portOptions.binding !== undefined) { // @ts-expect-error Jest testing serialOpts.binding = this.portOptions.binding; } logger_1.logger.debug(() => `Opening serial port with ${JSON.stringify(serialOpts)}`, NS); this.serialPort = new serialPort_1.SerialPort(serialOpts); this.writer.pipe(this.serialPort); this.serialPort.pipe(this.parser); this.parser.on("data", this.onFrame.bind(this)); try { await this.serialPort.asyncOpen(); logger_1.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_1.default.parseTcpPath(this.portOptions.path); logger_1.logger.debug(`Opening TCP socket with ${info.host}:${info.port}`, NS); this.socketPort = new node_net_1.Socket(); this.socketPort.setNoDelay(true); this.socketPort.setKeepAlive(true, 15000); this.writer.pipe(this.socketPort); this.socketPort.pipe(this.parser); this.parser.on("data", this.onFrame.bind(this)); return await new Promise((resolve, reject) => { const openError = async (err) => { await this.stop(); reject(err); }; // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.on("connect", () => { logger_1.logger.debug("Socket connected", NS); }); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.on("ready", () => { logger_1.logger.info("Socket ready", NS); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.removeListener("error", openError); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.once("close", this.onPortClose.bind(this)); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.on("error", this.onPortError.bind(this)); resolve(); }); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.once("error", openError); // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` this.socketPort.connect(info.port, info.host); }); } } /** * Handle port closing * @param err A boolean for Socket, an Error for serialport */ onPortClose(error) { logger_1.logger.info("Port closed.", NS); if (error && this.flags !== 0) { logger_1.logger.info(`Port close ${error}`, NS); this.flags = 0; this.emit("fatalError", enums_1.EzspStatus.ERROR_SERIAL_INIT); } } /** * Handle port error * @param error */ onPortError(error) { logger_1.logger.error(`Port ${error}`, NS); this.flags = 0; this.emit("fatalError", enums_1.EzspStatus.ERROR_SERIAL_INIT); } /** * Handle received frame from AshParser. * @param buf */ onFrame(buffer) { const iCAN = buffer.lastIndexOf(enums_2.AshReservedByte.CANCEL); // should only be one, but just in case... if (iCAN !== -1) { // ignore the cancel before RSTACK if (this.flags & Flag.CONNECTED) { this.counters.rxCancelled += 1; logger_1.logger.warning(`Frame(s) in progress cancelled in [${buffer.toString("hex")}]`, NS); } // get rid of everything up to the CAN flag and start reading frame from there, no need to loop through bytes in vain buffer = buffer.subarray(iCAN + 1); } if (!buffer.length) { // skip any CANCEL that results in empty frame (have yet to see one, but just in case...) // shouldn't happen for any other reason, unless receiving bad stuff from port? logger_1.logger.debug("Received empty frame. Skipping.", NS); return; } const status = this.receiveFrame(buffer); this.sendExec(); // always trigger to cover all cases if (status !== enums_1.EzspStatus.SUCCESS && status !== enums_1.EzspStatus.ASH_IN_PROGRESS && status !== enums_1.EzspStatus.NO_RX_DATA) { logger_1.logger.error(`Error while parsing received frame, status=${enums_1.EzspStatus[status]}.`, NS); this.emit("fatalError", enums_1.EzspStatus.HOST_FATAL_ERROR); return; } } /** * Initializes the ASH protocol, and waits until the NCP finishes rebooting, or a non-recoverable error occurs. * * @returns * - EzspStatus.SUCCESS * - EzspStatus.HOST_FATAL_ERROR * - EzspStatus.ASH_NCP_FATAL_ERROR) */ async start() { if (!this.portOpen || this.flags & Flag.CONNECTED) { return enums_1.EzspStatus.ERROR_INVALID_CALL; } logger_1.logger.info("======== ASH starting ========", NS); try { if (this.serialPort) { await this.serialPort.asyncFlush(); // clear read/write buffers } else { // XXX: Socket equiv? } } catch (err) { logger_1.logger.error(`Error while flushing before start: ${err}`, NS); } // block til RSTACK, fatal error or timeout // NOTE: on average, this seems to take around 1000ms when successful for (let i = 0; i < CONFIG_TIME_RST; i += CONFIG_TIME_RST_CHECK) { this.sendExec(); if (this.flags & Flag.CONNECTED) { logger_1.logger.info("======== ASH started ========", NS); return enums_1.EzspStatus.SUCCESS; } if (this.hostError !== enums_1.EzspStatus.NO_ERROR || this.ncpError !== enums_1.EzspStatus.NO_ERROR) { // don't wait for inevitable fail, bail early, let retry logic in EZSP layer do its thing break; } logger_1.logger.debug(`Waiting for RSTACK... ${i}/${CONFIG_TIME_RST}`, NS); await (0, utils_1.wait)(CONFIG_TIME_RST_CHECK); } return enums_1.EzspStatus.HOST_FATAL_ERROR; } /** * Stops the ASH protocol - flushes and closes the serial port, clears all queues, stops timers, etc. */ async stop() { this.closing = true; this.logCounters(); await this.closePort(); logger_1.logger.info("======== ASH stopped ========", NS); } /** * Close port and remove listeners. * Does nothing if port not defined/open. */ async closePort() { this.flags = 0; if (this.serialPort?.isOpen) { try { await this.serialPort.asyncFlushAndClose(); } catch (err) { logger_1.logger.error(`Failed to close serial port ${err}.`, NS); } this.serialPort.removeAllListeners(); } else if (this.socketPort != null && !this.socketPort.closed) { this.socketPort.destroy(); this.socketPort.removeAllListeners(); } } /** * Initializes the ASH serial port and (if enabled) resets the NCP. * The method used to do the reset is specified by the the host configuration parameter resetMethod. * * When the reset method is sending a RST frame, the caller should retry NCP resets a few times if it fails. * * @returns * - EzspStatus.SUCCESS * - EzspStatus.HOST_FATAL_ERROR */ async resetNcp() { if (this.closing) { return enums_1.EzspStatus.ERROR_INVALID_CALL; } logger_1.logger.info("======== ASH Adapter reset ========", NS); // ask ncp to reset itself using RST frame try { if (!this.portOpen) { await this.initPort(); } this.flags = Flag.RST | Flag.CAN; return enums_1.EzspStatus.SUCCESS; } catch (err) { logger_1.logger.error(`Failed to init port with error ${err}`, NS); this.hostError = enums_1.EzspStatus.HOST_FATAL_ERROR; return this.hostError; } } /** * Adds a DATA frame to the transmit queue to send to the NCP. * Frames that are too long or too short will not be sent, and frames will not be added to the queue * if the host is not in the Connected state, or the NCP is not ready to receive a DATA frame or if there * is no room in the queue; * * @param len length of data field * @param inBuf array containing the data to be sent * * @returns * - EzspStatus.SUCCESS * - EzspStatus.NO_TX_SPACE * - EzspStatus.DATA_FRAME_TOO_SHORT * - EzspStatus.DATA_FRAME_TOO_LONG * - EzspStatus.NOT_CONNECTED */ send(len, inBuf) { // Check for errors that might have been detected if (this.hostError !== enums_1.EzspStatus.NO_ERROR) { return enums_1.EzspStatus.HOST_FATAL_ERROR; } if (this.ncpError !== enums_1.EzspStatus.NO_ERROR) { return enums_1.EzspStatus.ASH_NCP_FATAL_ERROR; } // After verifying that the data field length is within bounds, // copies data frame to a buffer and appends it to the transmit queue. if (len < consts_1.ASH_MIN_DATA_FIELD_LEN) { return enums_1.EzspStatus.DATA_FRAME_TOO_SHORT; } if (len > consts_1.ASH_MAX_DATA_FIELD_LEN) { return enums_1.EzspStatus.DATA_FRAME_TOO_LONG; } if (!(this.flags & Flag.CONNECTED)) { return enums_1.EzspStatus.NOT_CONNECTED; } const buffer = this.txFree.allocBuffer(); if (buffer === undefined) { return enums_1.EzspStatus.NO_TX_SPACE; } inBuf.copy(buffer.data, 0, 0, len); buffer.len = len; this.randomizeBuffer(buffer.data, buffer.len); // IN/OUT data this.txQueue.addTail(buffer); this.sendExec(); return enums_1.EzspStatus.SUCCESS; } /** * Manages outgoing communication to the NCP, including DATA frames as well as the frames used for * initialization and error detection and recovery. */ sendExec() { let outByte = 0x00; let inByte = 0x00; let len = 0; let buffer; // Check for received acknowledgement timer expiry if (this.ackTimerHasExpired()) { if (this.flags & Flag.CONNECTED) { const reTx = this.flags & Flag.RETX; const expectedFrm = reTx ? this.frmReTx : this.frmTx; if (this.ackRx !== expectedFrm) { this.counters.rxAckTimeouts += 1; this.adjustAckPeriod(true); logger_1.logger.debug(`Timer expired waiting for ACK for ${reTx ? "frmReTx" : "frmTx"}=${expectedFrm}, ackRx=${this.ackRx}`, NS); if (++this.timeouts >= consts_1.ASH_MAX_TIMEOUTS) { this.hostDisconnect(enums_1.EzspStatus.ASH_ERROR_TIMEOUTS); return; } this.startRetransmission(); } else { this.stopAckTimer(); } } /* else { this.hostDisconnect(EzspStatus.ASH_ERROR_RESET_FAIL); }*/ // let Ezsp layer retry logic handle timeout } while (this.writer.writeAvailable()) { // Send ASH_CAN character immediately, ahead of any other transmit data if (this.flags & Flag.CAN) { if (this.sendState === SendState.IDLE) { // sending RST or just woke NCP this.writer.writeByte(enums_2.AshReservedByte.CANCEL); } else if (this.sendState === SendState.TX_DATA) { // cancel frame in progress this.counters.txCancelled += 1; this.writer.writeByte(enums_2.AshReservedByte.CANCEL); this.stopAckTimer(); this.sendState = SendState.IDLE; } this.flags &= ~Flag.CAN; continue; } switch (this.sendState) { case SendState.IDLE: { // In between frames - do some housekeeping and decide what to send next // If retransmitting, set the next frame to send to the last ackNum // received, then check to see if retransmission is now complete. if (this.flags & Flag.RETX) { if ((0, math_1.withinRange)(this.frmReTx, this.ackRx, this.frmTx)) { this.frmReTx = this.ackRx; } if (this.frmReTx === this.frmTx) { this.flags &= ~Flag.RETX; this.scrubReTxQueue(); } } // restrain ncp if needed this.dataFrameFlowControl(); // See if a short frame is flagged to be sent // The order of the tests below - RST, NAK and ACK - // sets the relative priority of sending these frame types. if (this.flags & Flag.RST) { this.txSHBuffer[0] = enums_2.AshFrameType.RST; this.setAndStartAckTimer(CONFIG_TIME_RST); len = 1; this.flags &= ~(Flag.RST | Flag.NAK | Flag.ACK); this.sendState = SendState.SHFRAME; logger_1.logger.debug("---> [FRAME type=RST]", NS); } else if (this.flags & (Flag.NAK | Flag.ACK)) { if (this.flags & Flag.NAK) { this.txSHBuffer[0] = enums_2.AshFrameType.NAK + (this.frmRx << consts_1.ASH_ACKNUM_BIT); this.flags &= ~(Flag.NRTX | Flag.NAK | Flag.ACK); logger_1.logger.debug(`---> [FRAME type=NAK frmRx=${this.frmRx}](ackRx=${this.ackRx})`, NS); } else { this.txSHBuffer[0] = enums_2.AshFrameType.ACK + (this.frmRx << consts_1.ASH_ACKNUM_BIT); this.flags &= ~(Flag.NRTX | Flag.ACK); logger_1.logger.debug(`---> [FRAME type=ACK frmRx=${this.frmRx}](ackRx=${this.ackRx})`, NS); } if (this.flags & Flag.NR) { this.txSHBuffer[0] |= consts_1.ASH_NFLAG_MASK; this.flags |= Flag.NRTX; this.startNrTimer(); } this.ackTx = this.frmRx; len = 1; this.sendState = SendState.SHFRAME; } else if (this.flags & Flag.RETX) { // Retransmitting DATA frames for error recovery // buffer assumed valid from loop logic // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` buffer = this.reTxQueue.getNthEntry((0, math_1.mod8)(this.frmTx - this.frmReTx)); len = buffer.len + 1; this.txSHBuffer[0] = enums_2.AshFrameType.DATA | (this.frmReTx << consts_1.ASH_FRMNUM_BIT) | (this.frmRx << consts_1.ASH_ACKNUM_BIT) | consts_1.ASH_RFLAG_MASK; this.sendState = SendState.RETX_DATA; logger_1.logger.debug(`---> [FRAME type=DATA_RETX frmReTx=${this.frmReTx} frmRx=${this.frmRx}](ackRx=${this.ackRx} frmTx=${this.frmTx})`, NS); } else if (this.ackTx !== this.frmRx) { // An ACK should be generated this.flags |= Flag.ACK; break; } else if (!this.txQueue.empty && (0, math_1.withinRange)(this.ackRx, this.frmTx, this.ackRx + exports.CONFIG_TX_K - 1)) { // Send a DATA frame if ready buffer = this.txQueue.head; len = buffer.len + 1; this.counters.txData += len - 1; this.txSHBuffer[0] = enums_2.AshFrameType.DATA | (this.frmTx << consts_1.ASH_FRMNUM_BIT) | (this.frmRx << consts_1.ASH_ACKNUM_BIT); this.sendState = SendState.TX_DATA; logger_1.logger.debug(`---> [FRAME type=DATA frmTx=${this.frmTx} frmRx=${this.frmRx}](ackRx=${this.ackRx})`, NS); } else { // Otherwise there's nothing to send this.writer.writeFlush(); return; } this.countFrame(true); // Start frame - encodeByte() is inited by a non-zero length argument outByte = this.encodeByte(len, this.txSHBuffer[0]); this.writer.writeByte(outByte); break; } case SendState.SHFRAME: { // sending short frame if (this.txOffset !== 0xff) { inByte = this.txSHBuffer[this.txOffset]; outByte = this.encodeByte(0, inByte); this.writer.writeByte(outByte); } else { this.sendState = SendState.IDLE; } break; } case SendState.TX_DATA: case SendState.RETX_DATA: { // sending OR resending data frame if (this.txOffset !== 0xff) { // buffer assumed valid from loop logic // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress` inByte = this.txOffset ? buffer.data[this.txOffset - 1] : this.txSHBuffer[0]; outByte = this.encodeByte(0, inByte); this.writer.writeByte(outByte); } else { if (this.sendState === SendState.TX_DATA) { this.frmTx = (0, math_1.inc8)(this.frmTx); buffer = this.txQueue.removeHead(); this.reTxQueue.addTail(buffer); } else { this.frmReTx = (0, math_1.inc8)(this.frmReTx); } if (this.ackTimerIsNotRunning()) { this.startAckTimer(); } this.ackTx = this.frmRx; this.sendState = SendState.IDLE; } break; } } } this.writer.writeFlush(); } /** * Retrieve a frame and accept, reTx, reject, fail based on type & validity in current state. * @returns * - EzspStatus.SUCCESS On valid RSTACK or valid DATA frame. * - EzspStatus.ASH_IN_PROGRESS * - EzspStatus.NO_RX_DATA * - EzspStatus.NO_RX_SPACE * - EzspStatus.HOST_FATAL_ERROR * - EzspStatus.ASH_NCP_FATAL_ERROR */ receiveFrame(buffer) { // Check for errors that might have been detected if (this.hostError !== enums_1.EzspStatus.NO_ERROR) { return enums_1.EzspStatus.HOST_FATAL_ERROR; } if (this.ncpError !== enums_1.EzspStatus.NO_ERROR) { return enums_1.EzspStatus.ASH_NCP_FATAL_ERROR; } let ackNum = 0; let frmNum = 0; let frameType = enums_2.AshFrameType.INVALID; // Read data from serial port and assemble a frame until complete, aborted // due to an error, cancelled, or there is no more serial data available. const status = this.readFrame(buffer); switch (status) { case enums_1.EzspStatus.SUCCESS: break; case enums_1.EzspStatus.ASH_IN_PROGRESS: // should have a complete frame by now, if not, don't process further return enums_1.EzspStatus.NO_RX_DATA; case enums_1.EzspStatus.ASH_CANCELLED: // should have been taken out in onFrame return this.hostDisconnect(status); case enums_1.EzspStatus.ASH_BAD_CRC: this.counters.rxCrcErrors += 1; this.rejectFrame(); logger_1.logger.error("Received frame with CRC error", NS); return enums_1.EzspStatus.NO_RX_DATA; case enums_1.EzspStatus.ASH_COMM_ERROR: this.counters.rxCommErrors += 1; this.rejectFrame(); logger_1.logger.error("Received frame with comm error", NS); return enums_1.EzspStatus.NO_RX_DATA; case enums_1.EzspStatus.ASH_TOO_SHORT: this.counters.rxTooShort += 1; this.rejectFrame(); logger_1.logger.error("Received frame shorter than minimum", NS); return enums_1.EzspStatus.NO_RX_DATA; case enums_1.EzspStatus.ASH_TOO_LONG: this.counters.rxTooLong += 1; this.rejectFrame(); logger_1.logger.error("Received frame longer than maximum", NS); return enums_1.EzspStatus.NO_RX_DATA; case enums_1.EzspStatus.ASH_ERROR_XON_XOFF: return this.hostDisconnect(status); default: logger_1.logger.error(`Unhandled error while receiving frame, status=${enums_1.EzspStatus[status]}.`, NS); return this.hostDisconnect(enums_1.EzspStatus.HOST_FATAL_ERROR); } // Got a complete frame - validate its control and length. // On an error the type returned will be TYPE_INVALID. frameType = this.getFrameType(this.rxSHBuffer[0], this.rxLen); // Free buffer allocated for a received frame if: // DATA frame, and out of order // DATA frame, and not in the CONNECTED state // not a DATA frame if (frameType === enums_2.AshFrameType.DATA) { if (!(this.flags & Flag.CONNECTED) || ashGetFrmNum(this.rxSHBuffer[0]) !== this.frmRx) { this.freeAllocatedRxBuffer(); } } else { this.freeAllocatedRxBuffer(); } const frameTypeStr = enums_2.AshFrameType[frameType]; logger_1.logger.debug(`<--- [FRAME type=${frameTypeStr}]`, NS); this.countFrame(false); // Process frames received while not in the connected state - // ignore everything except RSTACK and ERROR frames if (!(this.flags & Flag.CONNECTED)) { if (frameType === enums_2.AshFrameType.RSTACK) { // RSTACK frames have the ncp ASH version in the first data field byte, // and the reset reason in the second byte if (this.rxSHBuffer[1] !== consts_1.ASH_VERSION) { return this.hostDisconnect(enums_1.EzspStatus.ASH_ERROR_VERSION); } // Ignore a RSTACK if the reset reason doesn't match our reset method if (this.rxSHBuffer[2] !== enums_2.NcpFailedCode.RESET_SOFTWARE) { return enums_1.EzspStatus.ASH_IN_PROGRESS; } this.ncpError = enums_1.EzspStatus.NO_ERROR; this.stopAckTimer(); this.timeouts = 0; this.setAckPeriod(CONFIG_ACK_TIME_INIT); this.flags = Flag.CONNECTED | Flag.ACK; logger_1.logger.info("======== ASH connected ========", NS); return enums_1.EzspStatus.SUCCESS; } if (frameType === enums_2.AshFrameType.ERROR) { logger_1.logger.error(`Received ERROR from adapter while connecting, with code=${enums_2.NcpFailedCode[this.rxSHBuffer[2]]}.`, NS); // let Ezsp retry logic handle error // return this.ncpDisconnect(EzspStatus.ASH_NCP_FATAL_ERROR); } return enums_1.EzspStatus.ASH_IN_PROGRESS; } // Connected - process the ackNum in ACK, NAK and DATA frames if (frameType === enums_2.AshFrameType.DATA || frameType === enums_2.AshFrameType.ACK || frameType === enums_2.AshFrameType.NAK) { ackNum = ashGetACKNum(this.rxSHBuffer[0]); logger_1.logger.debug(`<--- [FRAME type=${frameTypeStr} ackNum=${ackNum}](ackRx=${this.ackRx} frmTx=${this.frmTx})`, NS); if (!(0, math_1.withinRange)(this.ackRx, ackNum, this.frmTx)) { this.counters.rxBadAckNumber += 1; logger_1.logger.debug(`<-x- [FRAME type=${frameTypeStr} ackNum=${ackNum}] Invalid ACK num; not within <${this.ackRx}-${this.frmTx}>`, NS); frameType = enums_2.AshFrameType.INVALID; } else if (ackNum !== this.ackRx) { // new frame(s) ACK'ed? this.ackRx = ackNum; this.timeouts = 0; if (this.flags & Flag.RETX) { // start timer if unACK'ed frames this.stopAckTimer(); if (ackNum !== this.frmReTx) { this.startAckTimer(); } } else { this.adjustAckPeriod(false); // factor ACK time into period if (ackNum !== this.frmTx) { // if more unACK'ed frames, this.startAckTimer(); // then restart ACK timer } this.scrubReTxQueue(); // free buffer(s) in ReTx queue } } } // Process frames received while connected switch (frameType) { case enums_2.AshFrameType.DATA: { frmNum = ashGetFrmNum(this.rxSHBuffer[0]); const frameStr = `[FRAME type=${frameTypeStr} ackNum=${ackNum} frmNum=${frmNum}](frmRx=${this.frmRx})`; if (frmNum === this.frmRx) { // is frame in sequence? if (this.rxDataBuffer == null) { // valid frame but no memory? this.counters.rxNoBuffer += 1; logger_1.logger.debug(`<-x- ${frameStr} No buffer available`, NS); this.rejectFrame(); return enums_1.EzspStatus.NO_RX_SPACE; } if (this.rxSHBuffer[0] & consts_1.ASH_RFLAG_MASK) { // if retransmitted, force ACK this.flags |= Flag.ACK; } this.flags &= ~(Flag.REJ | Flag.NAK); // clear the REJ condition this.frmRx = (0, math_1.inc8)(this.frmRx); this.randomizeBuffer(this.rxDataBuffer.data, this.rxDataBuffer.len); // IN/OUT data this.rxQueue.addTail(this.rxDataBuffer); // add frame to receive queue logger_1.logger.debug(`<--- ${frameStr} Added to rxQueue`, NS); this.counters.rxData += this.rxDataBuffer.len; setImmediate(() => this.emit("frame")); return enums_1.EzspStatus.SUCCESS; } // frame is out of sequence if (this.rxSHBuffer[0] & consts_1.ASH_RFLAG_MASK) { // if retransmitted, force ACK this.counters.rxDuplicates += 1; this.flags |= Flag.ACK; } else { // 1st OOS? then set REJ, send NAK if ((this.flags & Flag.REJ) === 0) { this.counters.rxOutOfSequence += 1; logger_1.logger.debug(`<-x- ${frameStr} Out of sequence: expected ${this.frmRx}; got ${frmNum}.`, NS); } this.rejectFrame(); } break; } case enums_2.AshFrameType.ACK: // already fully processed break; case enums_2.AshFrameType.NAK: // start retransmission if needed this.startRetransmission(); break; case enums_2.AshFrameType.RSTACK: // unexpected ncp reset logger_1.logger.error(`Received unexpected reset from adapter, with reason=${enums_2.NcpFailedCode[this.rxSHBuffer[2]]}.`, NS); this.ncpError = enums_1.EzspStatus.ASH_NCP_FATAL_ERROR; return this.hostDisconnect(enums_1.EzspStatus.ASH_ERROR_NCP_RESET); case enums_2.AshFrameType.ERROR: // ncp error logger_1.logger.error(`Received ERROR from adapter, with code=${enums_2.NcpFailedCode[this.rxSHBuffer[2]]}.`, NS); return this.ncpDisconnect(enums_1.EzspStatus.ASH_NCP_FATAL_ERROR); case enums_2.AshFrameType.INVALID: // reject invalid frames logger_1.logger.debug(`<-x- [FRAME type=${frameTypeStr}] Rejecting. ${this.rxSHBuffer.toString("hex")}`, NS); this.rejectFrame(); break; } return enums_1.EzspStatus.ASH_IN_PROGRESS; } /** * If the last control byte received was a DATA control, and we are connected and not already in the reject condition, * then send a NAK and set the reject condition. */ rejectFrame() { if ((this.rxSHBuffer[0] & consts_1.ASH_DFRAME_MASK) === enums_2.AshFrameType.DATA && (this.flags & (Flag.REJ | Flag.CONNECTED)) === Flag.CONNECTED) { this.flags |= Flag.REJ | Flag.NAK; } } /** * Retrieve and process serial bytes. * @returns */ readFrame(buffer) { let status = enums_1.EzspStatus.ERROR_INVALID_CALL; // no actual data to read, something's very wrong let index = 0; // let inByte: number = 0x00; let outByte = 0x00; if (!this.decodeInProgress) { this.rxLen = 0; this.rxDataBuffer = undefined; } for (const inByte of buffer) { // 0xFF byte signals a callback is pending when between frames in synchronous (polled) callback mode. if (!this.decodeInProgress && inByte === consts_1.ASH_WAKE) { if (this.ncpSleepEnabled) { this.ncpHasCallbacks = true; } status = enums_1.EzspStatus.ASH_IN_PROGRESS; continue; } // Decode next input byte - note that many input bytes do not produce // an output byte. Return on any error in decoding. index = this.rxLen; [status, outByte, this.rxLen] = this.decodeByte(inByte, outByte, this.rxLen); // discard an invalid frame if (status !== enums_1.EzspStatus.ASH_IN_PROGRESS && status !== enums_1.EzspStatus.SUCCESS) { this.freeAllocatedRxBuffer(); break; } // if input byte produced an output byte if (this.rxLen !== index) { if (this.rxLen <= consts_1.SH_RX_BUFFER_LEN) { // if a short frame, return in rxBuffer this.rxSHBuffer[index] = outByte; } else { // if a longer DATA frame, allocate an EzspBuffer for it. // (Note the control byte is always returned in shRxBuffer[0]. // Even if no buffer can be allocated, the control's ackNum must be processed.) if (this.rxLen === consts_1.SH_RX_BUFFER_LEN + 1) { // alloc buffer, copy prior data this.rxDataBuffer = this.rxFree.allocBuffer(); if (this.rxDataBuffer !== undefined) { // const len = SH_RX_BUFFER_LEN - 1; // (void) memcpy(this.rxDataBuffer.data, this.shRxBuffer + 1, SH_RX_BUFFER_LEN - 1); this.rxSHBuffer.copy(this.rxDataBuffer.data, 0, 1, consts_1.SH_RX_BUFFER_LEN); this.rxDataBuffer.len = consts_1.SH_RX_BUFFER_LEN - 1; } } if (this.rxDataBuffer !== undefined) { // copy next byte to buffer this.rxDataBuffer.data[index - 1] = outByte; // -1 since control is omitted this.rxDataBuffer.len = index; } } } if (status !== enums_1.EzspStatus.ASH_IN_PROGRESS) { break; } } return status; } /** * */ freeAllocatedRxBuffer() { if (this.rxDataBuffer !== undefined) { this.rxFree.freeBuffer(this.rxDataBuffer); this.rxDataBuffer = undefined; } } /** * */ scrubReTxQueue() { let buffer; while (this.ackRx !== this.frmReTxHead) { buffer = this.reTxQueue.removeHead(); this.txFree.freeBuffer(buffer); this.frmReTxHead = (0, math_1.inc8)(this.frmReTxHead); } } /** * If not already retransmitting, and there are unacked frames, start retransmitting after the last frame that was acked. */ startRetransmission() { if (!(this.flags & Flag.RETX) && this.ackRx !== this.frmTx) { this.stopAckTimer(); this.frmReTx = this.ackRx; this.flags |= Flag.RETX | Flag.CAN; } } /** * Check free rx buffers to see whether able to receive DATA frames: set or clear NR flag appropriately. * Inform ncp of our status using the nFlag in ACKs and NAKs. * Note that not ready status must be refreshed if it persists beyond a maximum time limit. */ dataFrameFlowControl() { if (this.flags & Flag.CONNECTED) { // Set/clear NR flag based on the number of buffers free if (this.rxFree.length < CONFIG_NR_LOW_LIMIT) { this.flags |= Flag.NR; logger_1.logger.warning("NOT READY - Signaling adapter", NS); } else if (this.rxFree.length > CONFIG_NR_HIGH_LIMIT) { this.flags &= ~Flag.NR; this.stopNrTimer(); // needed?? } // Force an ACK (or possibly NAK) if we need to send an updated nFlag // due to either a changed NR status or to refresh a set nFlag if (this.flags & Flag.NR) { if (!(this.flags & Flag.NRTX) || this.nrTimerHasExpired()) { this.flags |= Flag.ACK; this.startNrTimer(); } } else { this.nrTimerHasExpired(); // ensure timer checked often if (this.flags & Flag.NRTX) { this.flags |= Flag.ACK; this.stopNrTimer(); // needed??? } } } else { this.stopNrTimer(); this.flags &= ~(Flag.NRTX | Flag.NR); } } /** * Sets a fatal error state at the Host level. * @param error * @returns EzspStatus.HOST_FATAL_ERROR */ hostDisconnect(