protocol/sync.js

/**
 * @file Implements the CANopen Synchronization (SYNC) protocol.
 * @author Wilkins White
 * @copyright 2024 Daxbot
 */

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

/**
 * CANopen SYNC protocol handler.
 *
 * The synchronization (SYNC) protocol follows a producer-consumer structure
 * that provides a basic network synchronization mechanism. There should be
 * at most one sync producer on the network at a time.
 *
 * @param {Eds} eds - Eds object.
 * @see CiA301 "Synchronization object (SYNC)" (ยง7.2.5)
 * @implements {Protocol}
 */
class Sync extends Protocol {
    constructor(eds) {
        super(eds);

        this.syncCounter = 0;
        this.syncTimer = null;
        this._overflow = 0;
        this._cobId = null;
        this._generate = false;
    }

    /**
     * Get object 0x1005 [bit 30] - Sync generation enable.
     *
     * @type {boolean}
     * @deprecated Use {@link Eds#getSyncGenerationEnable} instead.
     */
    get generate() {
        return this.eds.getSyncGenerationEnable();
    }

    /**
     * Set object 0x1005 [bit 30] - Sync generation enable.
     *
     * @type {boolean}
     * @deprecated Use {@link Eds#setSyncGenerationEnable} instead.
     */
    set generate(enable) {
        this.eds.setSyncGenerationEnable(enable);
    }

    /**
     * Get object 0x1005 - COB-ID SYNC.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getSyncCobId} instead.
     */
    get cobId() {
        return this.eds.getSyncCobId();
    }

    /**
     * Set object 0x1005 - COB-ID SYNC.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setSyncCobId} instead.
     */
    set cobId(cobId) {
        this.eds.setSyncCobId(cobId);
    }

    /**
     * Get object 0x1006 - Communication cycle period.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getSyncCyclePeriod} instead.
     */
    get cyclePeriod() {
        return this.eds.getSyncCyclePeriod();
    }

    /**
     * Set object 0x1006 - Communication cycle period.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setSyncCyclePeriod} instead.
     */
    set cyclePeriod(period) {
        this.eds.setSyncCyclePeriod(period);
    }

    /**
     * Get object 0x1019 - Synchronous counter overflow value.
     *
     * @type {number}
     * @deprecated Use {@link Eds#getSyncOverflow} instead.
     */
    get overflow() {
        return this.eds.getSyncOverflow();
    }

    /**
     * Set object 0x1019 - Synchronous counter overflow value.
     *
     * @type {number}
     * @deprecated Use {@link Eds#setSyncOverflow} instead.
     */
    set overflow(overflow) {
        this.eds.setSyncOverflow(overflow);
    }

    /**
     * Service: SYNC write.
     *
     * @param {number | null} counter - sync counter;
     * @fires Protocol#message
     */
    write(counter = null) {
        if(!this._generate)
            throw new EdsError('SYNC generation is disabled');

        if (!this._cobId)
            throw new EdsError('COB-ID SYNC may not be 0');

        if(counter !== null)
            this.send(this._cobId, Buffer.from([counter]));
        else
            this.send(this._cobId);
    }

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

            const obj1006 = this.eds.getEntry(0x1006);
            if(obj1006)
                this._addEntry(obj1006);

            const obj1019 = this.eds.getEntry(0x1019);
            if(obj1019)
                this._addEntry(obj1019);

            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 obj1005 = this.eds.getEntry(0x1005);
            if(obj1005)
                this._removeEntry(obj1005);

            const obj1006 = this.eds.getEntry(0x1006);
            if(obj1006)
                this._removeEntry(obj1006);

            const obj1019 = this.eds.getEntry(0x1019);
            if(obj1019)
                this._removeEntry(obj1019);

            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 Sync#sync
     * @override
     */
    receive({ id, data }) {
        if (this._cobId === id) {
            if (data)
                data = data[0];

            /**
             * A Sync object was received.
             *
             * @event Sync#sync
             * @type {number}
             */
            this.emit('sync', data);
        }
    }

    /**
     * Listens for new Eds entries.
     *
     * @param {DataObject} entry - new entry.
     * @listens Eds#newEntry
     * @private
     */
    _addEntry(entry) {
        switch(entry.index) {
            case 0x1005:
                this.addUpdateCallback(entry, (obj) => this._parse1005(obj));
                this._parse1005(entry);
                break;
            case 0x1006:
                this.addUpdateCallback(entry, (obj) => this._parse1006(obj));
                this._parse1006(entry);
                break;
            case 0x1019:
                this.addUpdateCallback(entry, (obj) => this._parse1019(obj));
                this._parse1019(entry);
                break;
        }
    }

    /**
     * Listens for removed Eds entries.
     *
     * @param {DataObject} entry - removed entry.
     * @listens Eds#newEntry
     * @private
     */
    _removeEntry(entry) {
        switch(entry.index) {
            case 0x1005:
                this.removeUpdateCallback(entry);
                this._clear1005();
                break;
            case 0x1006:
                this.removeUpdateCallback(entry);
                this._clear1006();
                break;
            case 0x1019:
                this.removeUpdateCallback(entry);
                this._clear1019();
                break;
        }
    }

    /**
     * Called when 0x1005 (COB-ID SYNC) is updated.
     *
     * @param {DataObject} entry - updated DataObject.
     * @private
     */
    _parse1005(entry) {
        const value = entry.value;
        const gen = (value >> 30) & 0x1;
        const rtr = (value >> 29) & 0x1;
        const cobId = value & 0x7FF;

        if(rtr != 0x1) {
            this._generate = !!gen;
            this._cobId = cobId;
        }
        else {
            this._clear1005();
        }
    }

    /**
     * Called when 0x1005 (COB-ID SYNC) is removed.
     *
     * @private
     */
    _clear1005() {
        this._generate = false;
        this._cobId = null;
    }

    /**
     * Called when 0x1006 (Communication cycle period) is updated.
     *
     * @param {DataObject} entry - updated DataObject.
     * @private
     */
    _parse1006(entry) {
        // Clear the old timer
        this._clear1006();

        const cyclePeriod = entry.value;
        if(cyclePeriod > 0) {
            this.syncTimer = setInterval(() => {
                if(!this._generate || !this._cobId)
                    return;

                if(this._overflow > 0) {
                    this.syncCounter += 1;
                    if (this.syncCounter > this._overflow)
                        this.syncCounter = 1;

                    this.send(this._cobId, Buffer.from([this.syncCounter]));
                }
                else {
                    this.send(this._cobId, Buffer.alloc(0));
                }
            }, this._cyclePeriod / 1000);
        }
    }

    /**
     * Called when 0x1006 (Communication cycle period) is removed.
     *
     * @private
     */
    _clear1006() {
        clearInterval(this.syncTimer);
        this.syncTimer = null;
    }

    /**
     * Called when 0x1019 (Synchronous counter overflow value) is updated.
     *
     * @param {DataObject} entry - updated DataObject.
     * @private
     */
    _parse1019(entry) {
        this._overflow = entry.value;
    }

    /**
     * Called when 0x1019 (Synchronous counter overflow value) is removed.
     *
     * @private
     */
    _clear1019() {
        this._overflow = 0;
    }
}

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

/**
 * Initialize the device and audit the object dictionary.
 *
 * @deprecated Use {@link Sync#start} instead.
 * @function
 */
Sync.prototype.init = deprecate(
    function () {
        const { ObjectType, DataType } = require('../types');

        let obj1005 = this.eds.getEntry(0x1005);
        if(obj1005 === undefined) {
            obj1005 = this.eds.addEntry(0x1005, {
                parameterName:  'COB-ID SYNC',
                objectType:     ObjectType.VAR,
                dataType:       DataType.UNSIGNED32,
            });
        }

        let obj1006 = this.eds.getEntry(0x1006);
        if(obj1006 === undefined) {
            obj1006 = this.eds.addEntry(0x1006, {
                parameterName:  'Communication cycle period',
                objectType:     ObjectType.VAR,
                dataType:       DataType.UNSIGNED32,
            });
        }

        let obj1019 = this.eds.getEntry(0x1019);
        if(obj1019 === undefined) {
            obj1019 = this.eds.addEntry(0x1019, {
                parameterName:  'Synchronous counter overflow value',
                objectType:     ObjectType.VAR,
                dataType:       DataType.UNSIGNED8,
            });
        }

        this.start();
    }, 'Sync.init() is deprecated. Use Sync.start() instead.');


module.exports = exports = { Sync };