"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UartAsh = exports.AshEvents = void 0;
/* istanbul ignore file */
const debug_1 = __importDefault(require("debug"));
const stream_1 = require("stream");
const net_1 = require("net");
const socketPortUtils_1 = __importDefault(require("../../socketPortUtils"));
const serialPort_1 = require("../../serialPort");
const consts_1 = require("./consts");
const math_1 = require("../utils/math");
const enums_1 = require("../enums");
const enums_2 = require("./enums");
const queues_1 = require("./queues");
const writer_1 = require("./writer");
const parser_1 = require("./parser");
const utils_1 = require("../../../utils");
const debug = (0, debug_1.default)('zigbee-herdsman:adapter:ember:uart:ash');
/** ASH get rflag in control byte */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ashGetRFlag = (ctrl) => ((ctrl & consts_1.ASH_RFLAG_MASK) >> consts_1.ASH_RFLAG_BIT);
/** ASH get nflag in control byte */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ashGetNFlag = (ctrl) => ((ctrl & consts_1.ASH_NFLAG_MASK) >> consts_1.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 AshEvents;
(function (AshEvents) {
    /** When the ASH protocol detects a fatal error (bubbles up to restart adapter). */
    AshEvents["fatalError"] = "fatalError";
    /** When a frame has been parsed and queued in the rxQueue. */
    AshEvents["frame"] = "frame";
})(AshEvents || (exports.AshEvents = AshEvents = {}));
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) */
const 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 stream_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 = null;
        this.socketPort = null;
        this.writer = null;
        this.parser = null;
        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();
    }
    /**
     * Check if port is valid, open, and not closing.
     */
    get portOpen() {
        if (this.closing) {
            return false;
        }
        if (socketPortUtils_1.default.isTcpPath(this.portOptions.path)) {
            return this.socketPort && !this.socketPort.closed;
        }
        else {
            return this.serialPort && this.serialPort.isOpen;
        }
    }
    /**
     * 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
        );
    }
    /**
     * Initialize ASH variables, timers and queues, but not the serial port
     */
    initVariables() {
        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 = null;
        this.rxLen = 0;
        // init to "start of frame" default
        this.encodeCount = 0;
        this.encodeState = 0;
        this.encodeEscFlag = false;
        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.initQueues();
        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,
        };
    }
    /**
     * Initializes all queues and free lists.
     * All receive buffers are put into rxFree, and rxQueue is empty.
     * All transmit buffers are put into txFree, and txQueue and reTxQueue are empty.
     */
    initQueues() {
        this.txQueue.tail = null;
        this.reTxQueue.tail = null;
        this.txFree.link = null;
        for (let i = 0; i < consts_1.TX_POOL_BUFFERS; i++) {
            this.txFree.freeBuffer(this.txPool[i] = new queues_1.EzspBuffer());
        }
        this.rxQueue.tail = null;
        this.rxFree.link = null;
        for (let i = 0; i < consts_1.EZSP_HOST_RX_POOL_SIZE; i++) {
            this.rxFree.freeBuffer(this.rxPool[i] = new queues_1.EzspBuffer());
        }
    }
    /**
     * 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
        if (!socketPortUtils_1.default.isTcpPath(this.portOptions.path)) {
            const serialOpts = {
                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) {
                debug(`RTS/CTS config is off, enabling software flow control.`);
                serialOpts.xon = true;
                serialOpts.xoff = true;
            }
            //@ts-expect-error Jest testing
            if (this.portOptions.binding != null) {
                //@ts-expect-error Jest testing
                serialOpts.binding = this.portOptions.binding;
            }
            debug(`Opening serial port with ${JSON.stringify(serialOpts)}`);
            this.serialPort = new serialPort_1.SerialPort(serialOpts);
            this.writer = new writer_1.AshWriter({ highWaterMark: CONFIG_HIGHWATER_MARK });
            this.writer.pipe(this.serialPort);
            this.parser = new parser_1.AshParser({ readableHighWaterMark: CONFIG_HIGHWATER_MARK });
            this.serialPort.pipe(this.parser);
            this.parser.on('data', this.onFrame.bind(this));
            try {
                await this.serialPort.asyncOpen();
                debug(`Serial port opened: ${JSON.stringify(await this.serialPort.asyncGet())}`);
                this.serialPort.once('close', this.onPortClose.bind(this));
                this.serialPort.on('error', this.onPortError.bind(this));
            }
            catch (error) {
                await this.stop();
                throw error;
            }
        }
        else {
            const info = socketPortUtils_1.default.parseTcpPath(this.portOptions.path);
            debug(`Opening TCP socket with ${info.host}:${info.port}`);
            this.socketPort = new net_1.Socket();
            this.socketPort.setNoDelay(true);
            this.socketPort.setKeepAlive(true, 15000);
            this.writer = new writer_1.AshWriter({ highWaterMark: CONFIG_HIGHWATER_MARK });
            this.writer.pipe(this.socketPort);
            this.parser = new parser_1.AshParser({ readableHighWaterMark: CONFIG_HIGHWATER_MARK });
            this.socketPort.pipe(this.parser);
            this.parser.on('data', this.onFrame.bind(this));
            return new Promise((resolve, reject) => {
                const openError = async (err) => {
                    await this.stop();
                    reject(err);
                };
                this.socketPort.on('connect', () => {
                    debug('Socket connected');
                });
                this.socketPort.on('ready', async () => {
                    debug('Socket ready');
                    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);
            });
        }
    }
    /**
     * Handle port closing
     * @param err A boolean for Socket, an Error for serialport
     */
    async onPortClose(err) {
        console.log(`Port closed. Error? ${err ?? 'no'}`);
    }
    /**
     * Handle port error
     * @param error
     */
    async onPortError(error) {
        console.log(`Port error: ${error}`);
        this.emit(AshEvents.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;
                console.warn(`Frame(s) in progress cancelled in [${buffer.toString('hex')}]`);
            }
            // 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?
            debug(`Received empty frame. Skipping.`);
            return;
        }
        const status = this.receiveFrame(buffer);
        if ((status !== enums_1.EzspStatus.SUCCESS) && (status !== enums_1.EzspStatus.ASH_IN_PROGRESS) && (status !== enums_1.EzspStatus.NO_RX_DATA)) {
            if (this.flags & Flag.CONNECTED) {
                console.error(`Error while parsing received frame, status=${enums_1.EzspStatus[status]}.`);
                // if we're connected (not in reset) and get here, we need to reset
                this.emit(AshEvents.fatalError, enums_1.EzspStatus.HOST_FATAL_ERROR);
                return;
            }
            else {
                debug(`Error while parsing received frame in NOT_CONNECTED state (flags=${this.flags}), status=${enums_1.EzspStatus[status]}.`);
            }
        }
    }
    /**
     * 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;
        }
        console.log(`======== ASH starting ========`);
        try {
            if (this.serialPort != null) {
                await this.serialPort.asyncFlush(); // clear read/write buffers
            }
            else {
                // XXX: Socket equiv?
            }
        }
        catch (err) {
            console.error(`Error while flushing before start: ${err}`);
        }
        this.sendExec();
        // 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) {
            if ((this.flags & Flag.CONNECTED)) {
                console.log(`======== ASH started ========`);
                return enums_1.EzspStatus.SUCCESS;
            }
            else 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;
            }
            debug(`Waiting for RSTACK... ${i}/${CONFIG_TIME_RST}`);
            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.printCounters();
        await this.closePort();
        this.initVariables();
        console.log(`======== ASH stopped ========`);
    }
    /**
     * Close port and remove listeners.
     * Does nothing if port not defined/open.
     */
    async closePort() {
        if (this.serialPort?.isOpen) {
            try {
                await this.serialPort.asyncFlushAndClose();
                debug(`Serial port closed.`);
            }
            catch (err) {
                console.error(`Failed to close serial port ${err}.`);
            }
            this.serialPort.removeAllListeners();
        }
        else if (this.socketPort != null && !this.socketPort.closed) {
            this.socketPort.destroy();
            this.socketPort.removeAllListeners();
            debug(`Socket port closed.`);
        }
    }
    /**
     * 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;
        }
        console.log(`======== ASH NCP reset ========`);
        this.initVariables();
        let status;
        // 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) {
            console.error(`Failed to init port with error ${err}`);
            this.hostError = status;
            return enums_1.EzspStatus.HOST_FATAL_ERROR;
        }
    }
    /**
     * 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;
        }
        else 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 == null) {
            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 = null;
        // Check for received acknowledgement timer expiry
        if (this.ackTimerHasExpired()) {
            if (this.flags & Flag.CONNECTED) {
                const expectedFrm = ((this.flags & Flag.RETX) ? this.frmReTx : this.frmTx);
                if (this.ackRx !== expectedFrm) {
                    this.counters.rxAckTimeouts += 1;
                    this.adjustAckPeriod(true);
                    debug(`Timer expired waiting for ACK for ${expectedFrm}, current=${this.ackRx}`);
                    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(enums_1.EzspStatus.ASH_ERROR_RESET_FAIL);
            }
        }
        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;
                        debug(`---> [FRAME type=RST]`);
                    }
                    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);
                            debug(`---> [FRAME type=NAK frmRx=${this.frmRx}]`);
                        }
                        else {
                            this.txSHBuffer[0] = enums_2.AshFrameType.ACK + (this.frmRx << consts_1.ASH_ACKNUM_BIT);
                            this.flags &= ~(Flag.NRTX | Flag.ACK);
                            debug(`---> [FRAME type=ACK frmRx=${this.frmRx}]`);
                        }
                        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 = 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;
                        debug(`---> [FRAME type=DATA_RETX frmReTx=${this.frmReTx} frmRx=${this.frmRx}]`);
                    }
                    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 + 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;
                        debug(`---> [FRAME type=DATA frmTx=${this.frmTx} frmRx=${this.frmRx}]`);
                    }
                    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) {
                        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();
                console.error(`Received frame with CRC error`);
                return enums_1.EzspStatus.NO_RX_DATA;
            case enums_1.EzspStatus.ASH_COMM_ERROR:
                this.counters.rxCommErrors += 1;
                this.rejectFrame();
                console.error(`Received frame with comm error`);
                return enums_1.EzspStatus.NO_RX_DATA;
            case enums_1.EzspStatus.ASH_TOO_SHORT:
                this.counters.rxTooShort += 1;
                this.rejectFrame();
                console.error(`Received frame shorter than minimum`);
                return enums_1.EzspStatus.NO_RX_DATA;
            case enums_1.EzspStatus.ASH_TOO_LONG:
                this.counters.rxTooLong += 1;
                this.rejectFrame();
                console.error(`Received frame longer than maximum`);
                return enums_1.EzspStatus.NO_RX_DATA;
            case enums_1.EzspStatus.ASH_ERROR_XON_XOFF:
                return this.hostDisconnect(status);
            default:
                console.error(`Unhandled error while receiving frame, status=${enums_1.EzspStatus[status]}.`);
                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.freeNonNullRxBuffer();
            }
        }
        else {
            this.freeNonNullRxBuffer();
        }
        const frameTypeStr = enums_2.AshFrameType[frameType];
        debug(`<--- [FRAME type=${frameTypeStr}]`);
        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;
                console.log(`======== ASH connected ========`);
                return enums_1.EzspStatus.SUCCESS;
            }
            else if (frameType === enums_2.AshFrameType.ERROR) {
                console.error(`Received ERROR from NCP while connecting, with code=${enums_2.NcpFailedCode[this.rxSHBuffer[2]]}.`);
                return this.ncpDisconnect(enums_1.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]);
            debug(`<--- [FRAME type=${frameTypeStr} ackNum=${ackNum}]`);
            if (!(0, math_1.withinRange)(this.ackRx, ackNum, this.frmTx)) {
                this.counters.rxBadAckNumber += 1;
                debug(`<-x- [FRAME type=${frameTypeStr} ackNum=${ackNum}] Invalid ACK num; not within <${this.ackRx}-${this.frmTx}>`);
                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}]`;
                if (frmNum === this.frmRx) {
                    // is frame in sequence?
                    if (this.rxDataBuffer == null) {
                        // valid frame but no memory?
                        this.counters.rxNoBuffer += 1;
                        debug(`<-x- ${frameStr} No buffer available`);
                        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
                    debug(`<--- ${frameStr} Added to rxQueue`);
                    this.counters.rxData += this.rxDataBuffer.len;
                    setImmediate(() => {
                        this.emit(AshEvents.frame);
                    });
                    return enums_1.EzspStatus.SUCCESS;
                }
                else {
                    // 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;
                            debug(`<-x- ${frameStr} Out of sequence: expected ${this.frmRx}; got ${frmNum}.`);
                        }
                        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
                console.error(`Received unexpected reset from NCP, with reason=${enums_2.NcpFailedCode[this.rxSHBuffer[2]]}.`);
                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
                console.error(`Received ERROR from NCP, with code=${enums_2.NcpFailedCode[this.rxSHBuffer[2]]}.`);
                return this.ncpDisconnect(enums_1.EzspStatus.ASH_NCP_FATAL_ERROR);
            case enums_2.AshFrameType.INVALID:
                // reject invalid frames
                debug(`<-x- [FRAME type=${frameTypeStr}] Rejecting. ${this.rxSHBuffer.toString('hex')}`);
                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 = null;
        }
        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.freeNonNullRxBuffer();
                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 !== null) {
                            // 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 !== null) {
                        // 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;
    }
    /**
     *
     */
    freeNonNullRxBuffer() {
        if (this.rxDataBuffer !== null) {
            this.rxFree.freeBuffer(this.rxDataBuffer);
            this.rxDataBuffer = null;
        }
    }
    /**
     *
     */
    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;
                debug(`NOT READY - Signaling NCP`);
            }
            else if (this.rxFree.length > CONFIG_NR_HIGH_LIMIT) {
                this.flags &= ~Flag.NR;
                this.stopNrTimer(); // needed??
                // debug(`READY - Signaling NCP`);// spams-a-lot
            }
            // 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(error) {
        this.flags = 0;
        this.hostError = error;
        console.error(`ASH disconnected: ${enums_1.EzspStatus[error]} | NCP status: ${enums_1.EzspStatus[this.ncpError]}`);
        return enums_1.EzspStatus.HOST_FATAL_ERROR;
    }
    /**
     * Sets a fatal error state at the NCP level. Will require a reset.
     * @param error
     * @returns EzspStatus.ASH_NCP_FATAL_ERROR
     */
    ncpDisconnect(error) {
        this.flags = 0;
        this.ncpError = error;
        console.error(`ASH disconnected | NCP status: ${enums_1.EzspStatus[this.ncpError]}`);
        return enums_1.EzspStatus.ASH_NCP_FATAL_ERROR;
    }
    /**
     * Same as randomizeArray(0, buffer, len).
     * Returns buffer as-is if randomize is OFF.
     * @param buffer IN/OUT
     * @param len
     */
    randomizeBuffer(buffer, len) {
        // If enabled, exclusive-OR buffer data with a pseudo-random sequence
        if (CONFIG_RANDOMIZE) {
            this.randomizeArray(0, buffer, len); // zero inits the random sequence
        }
    }
    /**
     * Randomizes array contents by XORing with an 8-bit pseudo random sequence.
     * This reduces the likelihood that byte-stuffing will greatly increase the size of the payload.
     * (This could happen if a DATA frame contained repeated instances of the same reserved byte value.)
     *
     * @param seed  zero initializes the random sequence a non-zero value continues from a previous invocation
     * @param buf IN/OUT pointer to the array whose contents will be randomized
     * @param len  number of bytes in the array to modify
     * @returns  last value of the sequence.
     *           If a buffer is processed in two or more chunks, as with linked buffers,
     *           this value should be passed back as the value of the seed argument
     */
    randomizeArray(seed, buf, len) {
        let outIdx = 0;
        if (seed === 0) {
            seed = consts_1.LFSR_SEED;
        }
        while (len--) {
            // *buf++ ^= seed;
            buf[outIdx++] ^= seed;
            seed = (seed & 1) ? ((seed >> 1) ^ consts_1.LFSR_POLY) : (seed >> 1);
        }
        return seed;
    }
    /**
     * Get the frame type from the control byte and validate it against the frame length.
     * @param control
     * @param len Frame length
     * @returns AshFrameType.INVALID if bad control/length otherwise the frame type.
     */
    getFrameType(control, len) {
        if (control === enums_2.AshFrameType.RSTACK) {
            if (len === consts_1.ASH_FRAME_LEN_RSTACK) {
                return enums_2.AshFrameType.RSTACK;
            }
        }
        else if (control === enums_2.AshFrameType.ERROR) {
            if (len === consts_1.ASH_FRAME_LEN_ERROR) {
                return enums_2.AshFrameType.ERROR;
            }
        }
        else if ((control & consts_1.ASH_DFRAME_MASK) === enums_2.AshFrameType.DATA) {
            if (len >= consts_1.ASH_FRAME_LEN_DATA_MIN) {
                return enums_2.AshFrameType.DATA;
            }
        }
        else if ((control & consts_1.ASH_SHFRAME_MASK) === enums_2.AshFrameType.ACK) {
            if (len === consts_1.ASH_FRAME_LEN_ACK) {
                return enums_2.AshFrameType.ACK;
            }
        }
        else if ((control & consts_1.ASH_SHFRAME_MASK) === enums_2.AshFrameType.NAK) {
            if (len === consts_1.ASH_FRAME_LEN_NAK) {
                return enums_2.AshFrameType.NAK;
            }
        }
        else {
            this.counters.rxBadControl += 1;
            debug(`Frame illegal control ${control}.`); // EzspStatus.ASH_BAD_CONTROL
            return enums_2.AshFrameType.INVALID;
        }
        this.counters.rxBadLength += 1;
        debug(`Frame illegal length ${len} for control ${control}.`); // EzspStatus.ASH_BAD_LENGTH
        return enums_2.AshFrameType.INVALID;
    }
    /**
     * Encode byte for sending.
     * @param len Start a new frame if non-zero
     * @param byte
     * @returns outByte
     */
    encodeByte(len, byte) {
        // start a new frame if len is non-zero
        if (len) {
            this.encodeCount = len;
            this.txOffset = 0;
            this.encodeState = 0;
            this.encodeEscFlag = false;
            this.encodeCrc = 0xFFFF;
        }
        // was an escape last time?
        if (this.encodeEscFlag) {
            this.encodeEscFlag = false;
            // send data byte with bit flipped
            return this.encodeFlip;
        }
        // control and data field bytes
        if (this.encodeState === 0) {
            this.encodeCrc = (0, math_1.halCommonCrc16)(byte, this.encodeCrc);
            if (--this.encodeCount === 0) {
                this.encodeState = 1;
            }
            else {
                ++this.txOffset;
            }
            return this.encodeStuffByte(byte);
        }
        else if (this.encodeState === 1) {
            // CRC high byte
            this.encodeState = 2;
            return this.encodeStuffByte(this.encodeCrc >> 8);
        }
        else if (this.encodeState === 2) {
            // CRC low byte
            this.encodeState = 3;
            return this.encodeStuffByte(this.encodeCrc & 0xFF);
        }
        this.txOffset = 0xFF;
        return enums_2.AshReservedByte.FLAG;
    }
    /**
     * Stuff byte as defined by ASH protocol.
     * @param byte
     * @returns
     */
    encodeStuffByte(byte) {
        if (enums_2.AshReservedByte[byte] != null) {
            // is special byte
            this.encodeEscFlag = true;
            this.encodeFlip = byte ^ consts_1.ASH_FLIP;
            return enums_2.AshReservedByte.ESCAPE;
        }
        else {
            return byte;
        }
    }
    /**
     * Decode received byte.
     * @param byte
     * @param inByte IN/OUT
     * @param inLen IN/OUT
     * @returns  [EzspStatus, outByte, outLen]
     * - EzspStatus.ASH_IN_PROGRESS
     * - EzspStatus.ASH_COMM_ERROR
     * - EzspStatus.ASH_BAD_CRC
     * - EzspStatus.ASH_TOO_SHORT
     * - EzspStatus.ASH_TOO_LONG
     * - EzspStatus.SUCCESS
     * - EzspStatus.ASH_CANCELLED
     * - EzspStatus.ASH_ERROR_XON_XOFF
     */
    decodeByte(byte, inByte, inLen) {
        let status = enums_1.EzspStatus.ASH_IN_PROGRESS;
        if (!this.decodeInProgress) {
            this.decodeLen = 0;
            this.decodeByte1 = 0;
            this.decodeByte2 = 0;
            this.decodeFlip = 0;
            this.decodeCrc = 0xFFFF;
        }
        switch (byte) {
            case enums_2.AshReservedByte.FLAG:
                // flag byte (frame delimiter)
                if (this.decodeLen === 0) {
                    // if no frame data, not end flag, so ignore it
                    this.decodeFlip = 0; // ignore isolated data escape between flags
                    break;
                }
                else if (this.decodeLen === 0xFF) {
                    status = enums_1.EzspStatus.ASH_COMM_ERROR;
                }
                else if (this.decodeCrc !== ((this.decodeByte2 << 8) + this.decodeByte1)) {
                    status = enums_1.EzspStatus.ASH_BAD_CRC;
                }
                else if (this.decodeLen < consts_1.ASH_MIN_FRAME_WITH_CRC_LEN) {
                    status = enums_1.EzspStatus.ASH_TOO_SHORT;
                }
                else if (this.decodeLen > consts_1.ASH_MAX_FRAME_WITH_CRC_LEN) {
                    status = enums_1.EzspStatus.ASH_TOO_LONG;
                }
                else {
                    status = enums_1.EzspStatus.SUCCESS;
                }
                break;
            case enums_2.AshReservedByte.ESCAPE:
                // byte stuffing escape byte
                this.decodeFlip = consts_1.ASH_FLIP;
                break;
            case enums_2.AshReservedByte.CANCEL:
                // cancel frame without an error
                status = enums_1.EzspStatus.ASH_CANCELLED;
                break;
            case enums_2.AshReservedByte.SUBSTITUTE:
                // discard remainder of frame
                this.decodeLen = 0xFF; // special value flags low level comm error
                break;
            case enums_2.AshReservedByte.XON:
            case enums_2.AshReservedByte.XOFF:
                // If host is using RTS/CTS, ignore any XON/XOFFs received from the NCP.
                // If using XON/XOFF, the host driver must remove them from the input stream.
                // If it doesn't, it probably means the driver isn't setup for XON/XOFF,
                // so issue an error to flag the serial port driver problem.
                if (this.serialPort != null && !this.serialPort.settings.rtscts) {
                    status = enums_1.EzspStatus.ASH_ERROR_XON_XOFF;
                }
                break;
            default:
                // a normal byte
                byte ^= this.decodeFlip;
                this.decodeFlip = 0;
                if (this.decodeLen <= consts_1.ASH_MAX_FRAME_WITH_CRC_LEN) {
                    // limit length to max + 1
                    ++this.decodeLen;
                }
                if (this.decodeLen > consts_1.ASH_CRC_LEN) {
                    // compute frame CRC even if too long
                    this.decodeCrc = (0, math_1.halCommonCrc16)(this.decodeByte2, this.decodeCrc);
                    if (this.decodeLen <= consts_1.ASH_MAX_FRAME_WITH_CRC_LEN) {
                        // store to only max len
                        inByte = this.decodeByte2;
                        inLen = this.decodeLen - consts_1.ASH_CRC_LEN; // CRC is not output, reduce length
                    }
                }
                this.decodeByte2 = this.decodeByte1;
                this.decodeByte1 = byte;
                break;
        }
        this.decodeInProgress = (status === enums_1.EzspStatus.ASH_IN_PROGRESS);
        return [status, inByte, inLen];
    }
    /**
     * Starts the Not Ready timer
     *
     * On the host, this times nFlag refreshing when the host doesn't have room for callbacks for a prolonged period.
     *
     * On the NCP, if this times out the NCP resumes sending callbacks.
     */
    startNrTimer() {
        this.nrTimer = (Date.now() + CONFIG_NR_TIME);
    }
    /**
     * Stop Not Ready timer (set to 0).
     */
    stopNrTimer() {
        this.nrTimer = 0;
    }
    /**
     * Tests whether the Not Ready timer has expired or has stopped. If expired, it is stopped.
     *
     * @returns  true if the Not Ready timer has expired or stopped
     */
    nrTimerHasExpired() {
        if (this.nrTimer) {
            if ((Date.now() - this.nrTimer) >= 0) {
                this.nrTimer = 0;
            }
        }
        return (!this.nrTimer);
    }
    /**
     * Indicates whether or not Not Ready timer is currently running.
     *
     * @return True if nrTime == 0
     */
    nrTimerIsNotRunning() {
        return this.nrTimer === 0;
    }
    /**
     * Sets the acknowledgement timer period (in msec) and stops the timer.
     */
    setAckPeriod(msec) {
        this.ackPeriod = msec;
        this.ackTimer = 0;
    }
    /**
     * Sets the acknowledgement timer period (in msec), and starts the timer running.
     */
    setAndStartAckTimer(msec) {
        this.setAckPeriod(msec);
        this.startAckTimer();
    }
    /**
     * Adapts the acknowledgement timer period to the observed ACK delay.
     * If the timer is not running, it does nothing.
     * If the timer has expired, the timeout period is doubled.
     * If the timer has not expired, the elapsed time is fed into simple
     *
     * IIR filter:
     *          T[n+1] = (7*T[n] + elapsedTime) / 8
     *
     * The timeout period, ackPeriod, is limited such that:
     * config.ackTimeMin <= ackPeriod <= config.ackTimeMax.
     *
     * The acknowledgement timer is always stopped by this function.
     *
     * @param expired true if timer has expired
     */
    adjustAckPeriod(expired) {
        if (expired) {
            // if expired, double the period
            this.ackPeriod += this.ackPeriod;
        }
        else if (this.ackTimer) {
            // adjust period only if running
            // time elapsed since timer was started
            let temp = this.ackPeriod;
            // compute time to receive acknowledgement, then stop timer
            const lastAckTime = Date.now() - this.ackTimer;
            temp = (temp << 3) - temp;
            temp += lastAckTime << 2;
            temp >>= 3;
            this.ackPeriod = (temp & 0xFFFF);
        }
        // keep ackPeriod within limits
        if (this.ackPeriod > CONFIG_ACK_TIME_MAX) {
            this.ackPeriod = CONFIG_ACK_TIME_MAX;
        }
        else if (this.ackPeriod < CONFIG_ACK_TIME_MIN) {
            this.ackPeriod = CONFIG_ACK_TIME_MIN;
        }
        this.ackTimer = 0; // always stop the timer
    }
    /**
     * Sets ACK Timer to the specified period and starts it running.
     */
    startAckTimer() {
        this.ackTimer = Date.now();
    }
    /**
     * Stops and clears ACK Timer.
     */
    stopAckTimer() {
        this.ackTimer = 0;
    }
    /**
     * Indicates whether or not ACK Timer has expired.
     * If the timer is stopped (0) then it is not expired.
     *
     * @returns
     */
    ackTimerHasExpired() {
        if (this.ackTimer === 0) {
            // if timer is not running, return false
            return false;
        }
        // return ((halCommonGetInt16uMillisecondTick() - this.ackTimer) >= this.ackPeriod);
        return ((Date.now() - this.ackTimer) >= this.ackPeriod);
    }
    /**
     * Indicates whether or not ACK Timer is currently running (!= 0).
     * The timer may be running even if expired.
     */
    ackTimerIsNotRunning() {
        return this.ackTimer === 0;
    }
    /**
     * Increase counters based on frame type and direction.
     * @param sent True if frame being sent, false if being received.
     */
    countFrame(sent) {
        let control;
        if (sent) {
            control = this.txSHBuffer[0];
            this.counters.txAllFrames += 1;
        }
        else {
            control = this.rxSHBuffer[0];
            this.counters.rxAllFrames += 1;
        }
        if ((control & consts_1.ASH_DFRAME_MASK) === enums_2.AshFrameType.DATA) {
            if (sent) {
                if (control & consts_1.ASH_RFLAG_MASK) {
                    this.counters.txReDataFrames += 1;
                }
                else {
                    this.counters.txDataFrames += 1;
                }
            }
            else {
                if (control & consts_1.ASH_RFLAG_MASK) {
                    this.counters.rxReDataFrames += 1;
                }
                else {
                    this.counters.rxDataFrames += 1;
                }
            }
        }
        else if ((control & consts_1.ASH_SHFRAME_MASK) === enums_2.AshFrameType.ACK) {
            if (sent) {
                this.counters.txAckFrames += 1;
                if (control & consts_1.ASH_NFLAG_MASK) {
                    this.counters.txN1Frames += 1;
                } /* else {
                    this.counters.txN0Frames += 1;
                }*/
            }
            else {
                this.counters.rxAckFrames += 1;
                if (control & consts_1.ASH_NFLAG_MASK) {
                    this.counters.rxN1Frames += 1;
                } /* else {
                    this.counters.rxN0Frames += 1;
                }*/
            }
        }
        else if ((control & consts_1.ASH_SHFRAME_MASK) === enums_2.AshFrameType.NAK) {
            if (sent) {
                this.counters.txNakFrames += 1;
                if (control & consts_1.ASH_NFLAG_MASK) {
                    this.counters.txN1Frames += 1;
                } /* else {
                    this.counters.txN0Frames += 1;
                }*/
            }
            else {
                this.counters.rxNakFrames += 1;
                if (control & consts_1.ASH_NFLAG_MASK) {
                    this.counters.rxN1Frames += 1;
                } /* else {
                    this.counters.rxN0Frames += 1;
                }*/
            }
        }
    }
    /**
     * Prints counters in a nicely formatted table.
     */
    printCounters() {
        console.table({
            "Total frames": { "Received": this.counters.rxAllFrames, "Transmitted": this.counters.txAllFrames },
            "Cancelled": { "Received": this.counters.rxCancelled, "Transmitted": this.counters.txCancelled },
            "DATA frames": { "Received": this.counters.rxDataFrames, "Transmitted": this.counters.txDataFrames },
            "DATA bytes": { "Received": this.counters.rxData, "Transmitted": this.counters.txData },
            "Retry frames": { "Received": this.counters.rxReDataFrames, "Transmitted": this.counters.txReDataFrames },
            "ACK frames": { "Received": this.counters.rxAckFrames, "Transmitted": this.counters.txAckFrames },
            "NAK frames": { "Received": this.counters.rxNakFrames, "Transmitted": this.counters.txNakFrames },
            "nRdy frames": { "Received": this.counters.rxN1Frames, "Transmitted": this.counters.txN1Frames },
        });
        console.table({
            "CRC errors": { "Received": this.counters.rxCrcErrors },
            "Comm errors": { "Received": this.counters.rxCommErrors },
            "Length < minimum": { "Received": this.counters.rxTooShort },
            "Length > maximum": { "Received": this.counters.rxTooLong },
            "Bad controls": { "Received": this.counters.rxBadControl },
            "Bad lengths": { "Received": this.counters.rxBadLength },
            "Bad ACK numbers": { "Received": this.counters.rxBadAckNumber },
            "Out of buffers": { "Received": this.counters.rxNoBuffer },
            "Retry dupes": { "Received": this.counters.rxDuplicates },
            "Out of sequence": { "Received": this.counters.rxOutOfSequence },
            "ACK timeouts": { "Received": this.counters.rxAckTimeouts },
        });
    }
}
exports.UartAsh = UartAsh;
//# sourceMappingURL=ash.js.map