protocol/lss.js

/**
 * @file Implements the CANopen Layer Setting Services (LSS) protocol.
 * @author Wilkins White
 * @copyright 2024 Daxbot
 */

const Protocol = require('./protocol');
const { Eds } = require('../eds');
const { deprecate } = require('util');

/**
 * Check an Lss address against the device address.
 *
 * @param {object} identity - identity object.
 * @param {number} identity.vendorId - vendor id.
 * @param {number} identity.productCode - product code.
 * @param {number} identity.revisionNumber - revision number.
 * @param {number} identity.serialNumber - serial number.
 * @param {Array} address - Lss address.
 * @param {number} address.0 - device vendor id.
 * @param {number} address.1 - device product code.
 * @param {number} address.2 - device revision number.
 * @param {number} address.3 - device serial number.
 * @returns {boolean} true if the address matches.
 * @private
 */
function checkLssAddress(identity, address) {
    return address[0] === identity.vendorId
        && address[1] === identity.productCode
        && address[2] === identity.revisionNumber
        && address[3] === identity.serialNumber;
}

/**
 * Mask and compare two unsigned integers.
 *
 * @param {number} a - first number.
 * @param {number} b - second number.
 * @param {number} mask - bit mask.
 * @returns {boolean} true if the masked values are equal.
 * @private
 */
function maskCompare(a, b, mask) {
    a = (a & mask) >>> 0;
    b = (b & mask) >>> 0;

    return a === b;
}

/**
 * CANopen LSS command specifiers.
 *
 * @enum {number}
 * @see CiA305 "LSS Protocol Descriptions" (§3.8.2)
 * @private
 */
const LssCommand = {
    SWITCH_MODE_GLOBAL: 4,
    CONFIGURE_NODE_ID: 17,
    CONFIGURE_BIT_TIMING: 19,
    ACTIVATE_BIT_TIMING: 21,
    STORE_CONFIGURATION: 23,
    SWITCH_MODE_VENDOR_ID: 64,
    SWITCH_MODE_PRODUCT_CODE: 65,
    SWITCH_MODE_REVISION_NUMBER: 66,
    SWITCH_MODE_SERIAL_NUMBER: 67,
    FASTSCAN: 81,
    INQUIRE_VENDOR_ID: 90,
    INQUIRE_PRODUCT_CODE: 91,
    INQUIRE_REVISION_NUMBER: 92,
    INQUIRE_SERIAL_NUMBER: 93,
};

/**
 * CANopen LSS modes.
 *
 * @enum {number}
 * @see CiA305 "Switch Mode Global" (§3.9.1)
 */
const LssMode = {
    /** Only the switch mode service is available. */
    OPERATION: 0,

    /** All LSS services are available. */
    CONFIGURATION: 1,
};

/**
 * Errors generated during LSS services.
 *
 * @param {object} args - arguments.
 * @param {string} args.message - error message.
 * @param {number} args.code - error code.
 * @param {number} args.info - error info code.
 */
class LssError extends Error {
    constructor(...args) {
        if(typeof args[0] === 'object') {
            args = args[0];
        }
        else {
            args = {
                message: args[0],
                code: args[1],
                info: args[2],
            };
        }

        super(args.message);
        this.code = args.code;
        this.info = args.info;

        this.name = this.constructor.name;
        Error.captureStackTrace(this, this.constructor);
    }
}

/**
 * CANopen LSS protocol handler.
 *
 * @param {Eds} eds - Eds object.
 * @see CiA305 "Layer Settings Services and Protocol (LSS)"
 * @implements {Protocol}
 */
class Lss extends Protocol {
    constructor(eds) {
        super(eds);

        this._mode = LssMode.OPERATION;
        this.pending = {};
        this.select = [];
        this.scanState = 0;
        this.identity = {
            vendorId: null,
            productCode: null,
            revisionNumber: null,
            serialNumber: null,
        };
    }

    /**
     * Device LSS Mode.
     *
     * @type {LssMode}
     */
    get mode() {
        return this._mode;
    }

    /**
     * Vendor id.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getIdentity} instead.
     */
    get vendorId() {
        return this.eds.getSubEntry(0x1018, 1).value;
    }

    /**
     * Vendor id.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setIdentity} instead.
     */
    set vendorId(value) {
        this.eds.getSubEntry(0x1018, 1).value = value;
    }

    /**
     * Product code.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getIdentity} instead.
     */
    get productCode() {
        return this.eds.getSubEntry(0x1018, 2).value;
    }

    /**
     * Product code.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setIdentity} instead.
     */
    set productCode(value) {
        this.eds.getSubEntry(0x1018, 2).value = value;
    }

    /**
     * Revision number.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getIdentity} instead.
     */
    get revisionNumber() {
        return this.eds.getSubEntry(0x1018, 3).value;
    }

    /**
     * Revision number.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setIdentity} instead.
     */
    set revisionNumber(value) {
        this.eds.getSubEntry(0x1018, 3).value = value;
    }

    /**
     * Serial number.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getIdentity} instead.
     */
    get serialNumber() {
        return this.eds.getSubEntry(0x1018, 4).value;
    }

    /**
     * Serial number.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setIdentity} instead.
     */
    set serialNumber(value) {
        this.eds.getSubEntry(0x1018, 4).value = value;
    }

    /**
     * Set the LSS mode.
     *
     * @param {LssMode} mode - new mode.
     * @fires Lss#changeMode
     */
    setMode(mode) {
        if(mode !== this._mode) {
            this._mode = mode;

            /**
             * The LSS mode changed.
             *
             * @event Lss#changeMode
             * @type {LssMode}
             */
            this.emit('changeMode', mode);
        }
    }

    /**
     * LSS Fastscan protocol.
     *
     * Identifies exactly one LSS consumer device and switches it to
     * configuration mode.
     *
     * @param {object} [args] - arguments.
     * @param {number} [args.vendorId] - vendor-id hint.
     * @param {number} [args.productCode] - product-code hint.
     * @param {number} [args.revisionNumber] - revision-number hint.
     * @param {number} [args.serialNumber] - serial-number hint.
     * @param {number} [args.timeout] - how long to wait for nodes to respond.
     * @returns {Promise<null | object>} resolves to the discovered device's id (or null).
     * @see https://www.can-cia.org/fileadmin/resources/documents/proceedings/2008_pfeiffer.pdf
     */
    async fastscan(args={}) {
        let vendorId = args.vendorId;
        let productCode = args.productCode;
        let revisionNumber = args.revisionNumber;
        let serialNumber = args.serialNumber;
        let timeout = args.timeout || 20;
        let timeoutFlag = false;

        // Initiate fastscan
        await new Promise((resolve) => {
            const timer = setTimeout(() => {
                timeoutFlag = true;
                resolve();
            }, timeout);

            this._sendLssRequest(
                LssCommand.FASTSCAN, Buffer.from([0, 0, 0, 0, 0x80]));

            this.pending[0x4f] = { resolve, timer };
        });

        if (timeoutFlag)
            return null; // No devices

        // Find vendor-id
        if (vendorId === undefined) {
            vendorId = 0;
            for (let i = 31; i >= 0; --i) {
                await new Promise((resolve) => {
                    const timer = setTimeout(() => {
                        const bit = (1 << i) >>> 0;
                        vendorId = (vendorId | bit) >>> 0;
                        resolve();
                    }, timeout);

                    const data = Buffer.alloc(7);
                    data.writeUInt32LE(vendorId);
                    data[4] = i; // Bit checked
                    data[5] = 0; // LSS sub
                    data[6] = 0; // LSS next
                    this._sendLssRequest(LssCommand.FASTSCAN, data);

                    this.pending[0x4f] = { resolve, timer };
                });
            }
        }

        // Verify vendor-id
        await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('unverified vendorId', 255, 0));
            }, timeout);

            const data = Buffer.alloc(7);
            data.writeUInt32LE(vendorId);
            data[4] = 0; // Bit checked
            data[5] = 0; // LSS sub
            data[6] = 1; // LSS next
            this._sendLssRequest(LssCommand.FASTSCAN, data);

            this.pending[0x4f] = { resolve, timer };
        });

        // Find product-code
        if (productCode === undefined) {
            productCode = 0;
            for (let i = 31; i >= 0; --i) {
                await new Promise((resolve) => {
                    const timer = setTimeout(() => {
                        const bit = (1 << i) >>> 0;
                        productCode = (productCode | bit) >>> 0;
                        resolve();
                    }, timeout);

                    const data = Buffer.alloc(7);
                    data.writeUInt32LE(productCode);
                    data[4] = i; // Bit checked
                    data[5] = 1; // LSS sub
                    data[6] = 1; // LSS next
                    this._sendLssRequest(LssCommand.FASTSCAN, data);

                    this.pending[0x4f] = { resolve, timer };
                });
            }
        }

        // Verify product-code
        await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('unverified productCode', 255, 0));
            }, timeout);

            const data = Buffer.alloc(7);
            data.writeUInt32LE(productCode);
            data[4] = 0; // Bit checked
            data[5] = 1; // LSS sub
            data[6] = 2; // LSS next
            this._sendLssRequest(LssCommand.FASTSCAN, data);

            this.pending[0x4f] = { resolve, timer };
        });

        // Find revision-number
        if (revisionNumber === undefined) {
            revisionNumber = 0;
            for (let i = 31; i >= 0; --i) {
                await new Promise((resolve) => {
                    const timer = setTimeout(() => {
                        const bit = (1 << i) >>> 0;
                        revisionNumber = (revisionNumber | bit) >>> 0;
                        resolve();
                    }, timeout);

                    const data = Buffer.alloc(7);
                    data.writeUInt32LE(revisionNumber);
                    data[4] = i; // Bit checked
                    data[5] = 2; // LSS sub
                    data[6] = 2; // LSS next
                    this._sendLssRequest(LssCommand.FASTSCAN, data);

                    this.pending[0x4f] = { resolve, timer };
                });
            }
        }

        // Verify revision-number
        await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('unverified revisionNumber', 255, 0));
            }, timeout);

            const data = Buffer.alloc(7);
            data.writeUInt32LE(revisionNumber);
            data[4] = 0; // Bit checked
            data[5] = 2; // LSS sub
            data[6] = 3; // LSS next
            this._sendLssRequest(LssCommand.FASTSCAN, data);

            this.pending[0x4f] = { resolve, timer };
        });

        // Find serial-number
        if (serialNumber === undefined) {
            serialNumber = 0;
            for (let i = 31; i >= 0; --i) {
                await new Promise((resolve) => {
                    const timer = setTimeout(() => {
                        const bit = (1 << i) >>> 0;
                        serialNumber = (serialNumber | bit) >>> 0;
                        resolve();
                    }, timeout);

                    const data = Buffer.alloc(7);
                    data.writeUInt32LE(serialNumber);
                    data[4] = i; // Bit checked
                    data[5] = 3; // LSS sub
                    data[6] = 3; // LSS next
                    this._sendLssRequest(LssCommand.FASTSCAN, data);

                    this.pending[0x4f] = { resolve, timer };
                });
            }
        }

        // Verify serial-number
        await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('unverified serialNumber', 255, 0));
            }, timeout);

            const data = Buffer.alloc(7);
            data.writeUInt32LE(serialNumber);
            data[4] = 0; // Bit checked
            data[5] = 3; // LSS sub
            data[6] = 0; // LSS next
            this._sendLssRequest(LssCommand.FASTSCAN, data);

            this.pending[0x4f] = { resolve, timer };
        });

        return { vendorId, productCode, revisionNumber, serialNumber };
    }

    /**
     * Service: switch mode global.
     *
     * @param {LssMode} mode - LSS mode to switch to.
     * @see CiA305 "Switch Mode Global" (§3.9.1)
     */
    switchModeGlobal(mode) {
        if (mode === undefined)
            throw ReferenceError('mode not defined');

        this._sendLssRequest(
            LssCommand.SWITCH_MODE_GLOBAL, Buffer.from([mode]));
    }

    /**
     * Service: switch mode selective.
     *
     * @param {object} args - arguments.
     * @param {number} args.vendorId - LSS consumer vendor-id.
     * @param {number} args.productCode - LSS consumer product-code.
     * @param {number} args.revisionNumber - LSS consumer revision-number.
     * @param {number} args.serialNumber - LSS consumer serial-number.
     * @param {number} [args.timeout] - time until promise is rejected.
     * @returns {Promise<LssMode>} - the actual mode of the LSS consumer.
     * @see CiA305 "Switch Mode Selective" (§3.9.2)
     */
    switchModeSelective(...args) {
        if(typeof args[0] === 'object') {
            args = args[0];
        }
        else {
            args = {
                vendorId: args[0],
                productCode: args[1],
                revisionNumber: args[2],
                serialNumber: args[3],
                timeout: args[4],
            };
        }

        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('timeout'));
            }, args.timeout);

            const data = Buffer.alloc(4);

            // Send vendor-id
            data.writeUInt32LE(args.vendorId);
            this._sendLssRequest(LssCommand.SWITCH_MODE_VENDOR_ID, data);

            // Send product-code
            data.writeUInt32LE(args.productCode);
            this._sendLssRequest(LssCommand.SWITCH_MODE_PRODUCT_CODE, data);

            // Send revision-number
            data.writeUInt32LE(args.revisionNumber);
            this._sendLssRequest(LssCommand.SWITCH_MODE_REVISION_NUMBER, data);

            // Send serial-number
            data.writeUInt32LE(args.serialNumber);
            this._sendLssRequest(LssCommand.SWITCH_MODE_SERIAL_NUMBER, data);

            this.pending[68] = { resolve, timer };
        });
    }

    /**
     * Service: configure node-id.
     *
     * @param {number} nodeId - new node-id
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise} resolves when the service is finished.
     * @see CiA305 "Configure Node-ID Protocol" (§3.10.1)
     */
    async configureNodeId(nodeId, timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('timeout'));
            }, timeout);

            this._sendLssRequest(
                LssCommand.CONFIGURE_NODE_ID, Buffer.from([nodeId]));

            this.pending[LssCommand.CONFIGURE_NODE_ID] = {
                resolve,
                timer
            };
        });

        let message = '';
        switch (result[0]) {
            case 0:
                return; // Success
            case 1:
                message = 'Node-ID out of range';
                break;
            case 255:
                message = 'Implementation specific error';
                break;
            default:
                message = 'Unsupported error code';
                break;
        }

        throw new LssError(message, result[0], result[1]);
    }

    /**
     * Service: configure bit timing parameters.
     *
     * @param {number} tableSelect - which bit timing parameters table to use.
     * @param {number} tableIndex - the entry in the selected table to use.
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise} resolves when the service is finished.
     * @see CiA305 "Configure Bit Timing Parameters Protocol" (§3.10.2)
     */
    async configureBitTiming(tableSelect, tableIndex, timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('timeout'));
            }, timeout);

            this._sendLssRequest(
                LssCommand.CONFIGURE_BIT_TIMING,
                Buffer.from([tableSelect, tableIndex]));

            this.pending[LssCommand.CONFIGURE_BIT_TIMING] = {
                resolve,
                timer
            };
        });

        let message = '';
        switch (result[0]) {
            case 0:
                return; // Success
            case 1:
                message = 'Bit timing not supported';
                break;
            case 255:
                message = 'Implementation specific error';
                break;
            default:
                message = 'Unsupported error code';
                break;
        }

        throw new LssError(message, result[0], result[1]);
    }

    /**
     * Service: activate bit timing parameters.
     *
     * @param {number} delay - switch delay in ms.
     * @see CiA305 "Activate Bit Timing Parameters Protocol" (§3.10.3)
     */
    activateBitTiming(delay) {
        const switchDelay = Buffer.alloc(2);
        switchDelay.writeUInt16LE(delay);
        this._sendLssRequest(LssCommand.ACTIVATE_BIT_TIMING, switchDelay);
    }

    /**
     * Service: store configuration.
     *
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise} resolves when the service is finished.
     * @see CiA305 "Store Configuration Protocol" (§3.10.4)
     */
    async storeConfiguration(timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new LssError('timeout'));
            }, timeout);

            this._sendLssRequest(LssCommand.STORE_CONFIGURATION);

            this.pending[LssCommand.STORE_CONFIGURATION] = {
                resolve,
                timer
            };
        });

        let message = '';
        switch (result[0]) {
            case 0:
                return; // Success
            case 1:
                message = 'Store configuration not supported';
                break;
            case 2:
                message = 'Storage media access error';
                break;
            case 255:
                message = 'Implementation specific error';
                break;
            default:
                message = 'Unsupported error code';
                break;
        }

        throw new LssError(message, result[0], result[1]);
    }

    /**
     * Service: inquire identity vendor-id.
     *
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise<number>} - LSS consumer vendor-id.
     * @see CiA305 "Inquire Identity Vendor-ID Protocol" (§3.11.1.1)
     */
    async inquireVendorId(timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(
                () => reject(new LssError('timeout')), timeout);

            this._sendLssRequest(LssCommand.INQUIRE_VENDOR_ID);

            this.pending[LssCommand.INQUIRE_VENDOR_ID] = {
                resolve,
                timer
            };
        });

        return result.readUInt32LE();
    }

    /**
     * Service: inquire identity product-code.
     *
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise<number>} - LSS consumer product-code.
     * @see CiA305 "Inquire Identity Product-Code Protocol" (§3.11.1.2)
     */
    async inquireProductCode(timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(
                () => reject(new LssError('timeout')), timeout);

            this._sendLssRequest(LssCommand.INQUIRE_PRODUCT_CODE);

            this.pending[LssCommand.INQUIRE_PRODUCT_CODE] = {
                resolve,
                timer
            };
        });

        return result.readUInt32LE();
    }

    /**
     * Service: inquire identity revision-number.
     *
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise<number>} - LSS consumer revision-number.
     * @see CiA305 "Inquire Identity Revision-Number Protocol" (§3.11.1.3)
     */
    async inquireRevisionNumber(timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(
                () => reject(new LssError('timeout')), timeout);

            this._sendLssRequest(LssCommand.INQUIRE_REVISION_NUMBER);

            this.pending[LssCommand.INQUIRE_REVISION_NUMBER] = {
                resolve,
                timer
            };
        });

        return result.readUInt32LE();
    }

    /**
     * Service: inquire identity serial-number.
     *
     * @param {number} timeout - time until promise is rejected.
     * @returns {Promise<number>} - LSS consumer serial-number.
     * @see CiA305 "Inquire Identity Serial-Number Protocol" (§3.11.1.4)
     */
    async inquireSerialNumber(timeout = 20) {
        const result = await new Promise((resolve, reject) => {
            const timer = setTimeout(
                () => reject(new LssError('timeout')), timeout);

            this._sendLssRequest(LssCommand.INQUIRE_SERIAL_NUMBER);

            this.pending[LssCommand.INQUIRE_SERIAL_NUMBER] = {
                resolve,
                timer
            };
        });

        return result.readUInt32LE();
    }

    /**
     * Start the module.
     *
     * @override
     */
    start() {
        if(!this.started) {
            const obj1018 = this.eds.getEntry(0x1018);
            if(obj1018)
                this._addEntry(obj1018);

            this.addEdsCallback('newEntry', (obj) => this._addEntry(obj));
            this.addEdsCallback('removeEntry', (obj) => this._removeEntry(obj));

            super.start();
        }
    }

    /**
     * Stop the module.
     *
     * @override
     */
    stop() {
        if(this.started) {
            this.removeEdsCallback('newEntry');
            this.removeEdsCallback('removeEntry');

            const obj1018 = this.eds.getEntry(0x1018);
            if(obj1018)
                this._removeEntry(obj1018);

            super.stop();
        }
    }

    /**
     * Call when a new CAN message is received.
     *
     * @param {object} message - CAN frame.
     * @param {number} message.id - CAN message identifier.
     * @param {Buffer} message.data - CAN message data;
     * @fires Lss#changeMode
     * @fires Lss#changeDeviceId
     * @override
     */
    receive({ id, data }) {
        if (id === 0x7e4) {
            const cs = data[0];
            if (this.pending[cs] !== undefined) {
                clearTimeout(this.pending[cs].timer);
                this.pending[cs].resolve(data.slice(1));
            }
        }
        else if (id === 0x7e5) {
            const cs = data[0];

            if (cs === LssCommand.FASTSCAN) {
                // Fastscan is only available for unconfigured nodes.
                const bitCheck = data[5];
                const lssSub = data[6];
                const lssNext = data[7];

                if (bitCheck === 0x80) {
                    // Reset state
                    this.scanState = 0;
                    this._sendLssResponse(0x4f);
                }
                else if (this.scanState === lssSub) {
                    if (bitCheck > 0x1f || lssSub > 3 || lssNext > 3)
                        return; // Invalid request

                    const value = data.readUInt32LE(1);
                    const mask = (0xffffffff << bitCheck) >>> 0;
                    let match = false;

                    switch (lssSub) {
                        case 0:
                            // Test vendor id
                            match = maskCompare(
                                this.identity.vendorId, value, mask);
                            break;

                        case 1:
                            // Test product code
                            match = maskCompare(
                                this.identity.productCode, value, mask);
                            break;

                        case 2:
                            // Test revision number
                            match = maskCompare(
                                this.identity.revisionNumber, value, mask);
                            break;

                        case 3:
                            match = maskCompare(
                                this.identity.serialNumber, value, mask);
                            break;
                    }

                    if (match) {
                        this.scanState = lssNext;
                        if (bitCheck === 0 && lssNext < lssSub)
                            this.setMode(LssMode.CONFIGURATION);

                        this._sendLssResponse(0x4f);
                    }
                }
                return;
            }

            // Switch mode commands
            switch (cs) {
                case LssCommand.SWITCH_MODE_GLOBAL:
                    this.setMode(data[1]);
                    this.select = [];
                    return;

                case LssCommand.SWITCH_MODE_VENDOR_ID:
                    this.select[0] = data.readUInt32LE(1);
                    return;

                case LssCommand.SWITCH_MODE_PRODUCT_CODE:
                    this.select[1] = data.readUInt32LE(1);
                    return;

                case LssCommand.SWITCH_MODE_REVISION_NUMBER:
                    this.select[2] = data.readUInt32LE(1);
                    return;

                case LssCommand.SWITCH_MODE_SERIAL_NUMBER:
                    this.select[3] = data.readUInt32LE(1);
                    if (checkLssAddress(this.identity, this.select))
                        this.setMode(LssMode.CONFIGURATION);
                    return;
            }

            // Configuration commands
            if (this.mode !== LssMode.CONFIGURATION)
                return;

            switch (cs) {
                case LssCommand.CONFIGURE_NODE_ID:
                    try {
                        /**
                         * LssCommand.CONFIGURE_NODE_ID was received.
                         *
                         * @event Lss#changeDeviceId
                         * @type {number}
                         */
                        this.emit('changeDeviceId', data[1]);
                        this._sendLssResponse(cs, 0);
                    }
                    catch {
                        // Error: Node-ID out of range
                        this._sendLssResponse(cs, 1);
                    }
                    return;

                case LssCommand.CONFIGURE_BIT_TIMING:
                    // Error: Bit timing not supported
                    this._sendLssResponse(cs, 1);
                    return;

                case LssCommand.STORE_CONFIGURATION:
                    // Error: Store configuration not supported
                    this._sendLssResponse(cs, 1);
                    return;

                case LssCommand.INQUIRE_VENDOR_ID:
                    this._sendLssResponse(cs, this.identity.vendorId);
                    return;

                case LssCommand.INQUIRE_PRODUCT_CODE:
                    this._sendLssResponse(cs, this.identity.productCode);
                    return;

                case LssCommand.INQUIRE_REVISION_NUMBER:
                    this._sendLssResponse(cs, this.identity.revisionNumber);
                    return;

                case LssCommand.INQUIRE_SERIAL_NUMBER:
                    this._sendLssResponse(cs, this.identity.serialNumber);
                    return;
            }
        }
    }

    /**
     * Listens for new Eds entries.
     *
     * @param {DataObject} entry - new entry.
     * @private
     */
    _addEntry(entry) {
        if(entry.index === 0x1018) {
            this.addUpdateCallback(entry, (obj) => this._parse1018(obj));
            this._parse1018(entry);
        }
    }

    /**
     * Listens for removed Eds entries.
     *
     * @param {DataObject} entry - removed entry.
     * @private
     */
    _removeEntry(entry) {
        if(entry.index === 0x1018) {
            this.removeUpdateCallback(entry);
            this._clear1018();
        }
    }

    /**
     * Called when 0x1018 (Identity object) is updated.
     *
     * @param {DataObject} entry - updated DataObject.
     * @listens DataObject#update
     * @private
     */
    _parse1018(entry) {
        if(!entry)
            return;

        const subIndex = entry.subIndex;
        if(subIndex === null) {
            const maxSubIndex = entry[0].value;
            for(let i = 1; i <= maxSubIndex; ++i)
                this._parse1018(entry.at(i));
        }
        else {
            switch(subIndex) {
                case 1:
                    this.identity.vendorId = entry.value;
                    break;
                case 2:
                    this.identity.productCode = entry.value;
                    break;
                case 3:
                    this.identity.revisionNumber = entry.value;
                    break;
                case 4:
                    this.identity.serialNumber = entry.value;
                    break;
            }
        }
    }

    /**
     * Called when 0x1018 (Identity object) is removed.
     *
     * @private
     */
    _clear1018() {
        this.identity = {
            vendorId: null,
            productCode: null,
            revisionNumber: null,
            serialNumber: null,
        };
    }

    /**
     * Send an LSS request object.
     *
     * @param {LssCommand} command - LSS command specifier.
     * @param {Buffer} data - command data.
     * @private
     */
    _sendLssRequest(command, data) {
        const sendBuffer = Buffer.alloc(8);
        sendBuffer[0] = command;

        if (data !== undefined)
            data.copy(sendBuffer, 1);

        this.send(0x7e5, sendBuffer);
    }

    /**
     * Send an LSS response object.
     *
     * @param {LssCommand} command - LSS command specifier.
     * @param {number} code - response code.
     * @param {number} info - response info.
     * @private
     */
    _sendLssResponse(command, code, info = 0) {
        const sendBuffer = Buffer.alloc(8);
        sendBuffer[0] = command;
        sendBuffer[1] = code;
        sendBuffer[2] = info;

        this.send(0x7e4, sendBuffer);
    }
}

////////////////////////////////// Deprecated //////////////////////////////////

/**
 * Initialize the device and audit the object dictionary.
 *
 * @deprecated Use {@link Lss#start} instead.
 * @function
 */
Lss.prototype.init = deprecate(
    function() {
        this.start();
    }, 'Lss.init() is deprecated. Use Lss.start() instead.');

module.exports = exports = { LssMode, LssError, Lss };