zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
1,121 lines • 69.8 kB
JavaScript
"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(