UNPKG

canopen

Version:

CANopen implementation for Javascript

1,723 lines (1,473 loc) 109 kB
/** * @file Implements a CANopen Electronic Data Sheet (EDS) * @author Wilkins White * @copyright 2024 Daxbot */ // External modules const { EOL } = require('os'); const EventEmitter = require('events'); const fs = require('fs'); const ini = require('ini'); // Local modules const { ObjectType, AccessType, DataType, } = require('./types'); const rawToType = require('./functions/raw_to_type'); const typeToRaw = require('./functions/type_to_raw'); /** * Parse EDS date and time. * * @param {string} time - time string (hh:mm[AM|PM]). * @param {string} date - date string (mm-dd-yyyy). * @returns {Date} parsed Date. * @private */ function parseDate(time, date) { const postMeridiem = time.includes('PM'); time = time .replace('AM', '') .replace('PM', ''); let [hours, minutes] = time.split(':'); let [month, day, year] = date.split('-'); hours = parseInt(hours); minutes = parseInt(minutes); month = parseInt(month); day = parseInt(day); year = parseInt(year); if (postMeridiem) hours += 12; return new Date(year, month - 1, day, hours, minutes); } /** * Helper method to turn EDS file data into {@link DataObject} data. * * @param {object} data - EDS style data to convert. * @returns {object} DataObject style data. * @private */ function edsToEntry(data) { return { parameterName: data['ParameterName'], subNumber: parseInt(data['SubNumber']) || undefined, objectType: parseInt(data['ObjectType']) || undefined, dataType: parseInt(data['DataType']) || undefined, lowLimit: parseInt(data['LowLimit']) || undefined, highLimit: parseInt(data['HighLimit']) || undefined, accessType: data['AccessType'], defaultValue: data['DefaultValue'], pdoMapping: data['PDOMapping'], objFlags: parseInt(data['ObjFlags']) || undefined, compactSubObj: parseInt(data['CompactSubObj']) || undefined }; } /** * Formats a {@link DataObject} for writing to an EDS file. * * @param {DataObject} entry - DataObject style data to convert. * @returns {object} EDS style data. * @private */ function entryToEds(entry) { if(!DataObject.isDataObject(entry)) throw new TypeError('entry is not a DataObject'); let data = {}; data['ParameterName'] = entry.parameterName; data['ObjectType'] = `0x${entry.objectType.toString(16)}`; if (entry.subNumber !== undefined) data['SubNumber'] = `0x${entry.subNumber.toString(16)}`; if (entry.dataType !== undefined) data['DataType'] = `0x${entry.dataType.toString(16)}`; if (entry.lowLimit !== undefined) data['LowLimit'] = entry.lowLimit.toString(); if (entry.highLimit !== undefined) data['HighLimit'] = entry.highLimit.toString(); if (entry.accessType !== undefined) data['AccessType'] = entry.accessType; if (entry.defaultValue !== undefined) data['DefaultValue'] = entry.defaultValue.toString(); if (entry.pdoMapping !== undefined) data['PDOMapping'] = (entry.pdoMapping) ? '1' : '0'; if (entry.objFlags !== undefined) data['ObjFlags'] = entry.objFlags.toString(); if (entry.compactSubObj !== undefined) data['CompactSubObj'] = (entry.compactSubObj) ? '1' : '0'; return data; } /** * Errors generated due to an improper EDS configuration. * * @param {string} message - error message. */ class EdsError extends Error { constructor(message) { super(message); this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); } } /** * A CANopen Data Object. * * DataObjects should not be created directly, use {@link Eds#addEntry} or * {@link Eds#addSubEntry} instead. * * @param {string} key - object index key (e.g., 1018sub3) * @param {object} data - creation parameters. * @param {string} data.parameterName - name of the data object. * @param {ObjectType} data.objectType - object type. * @param {DataType} data.dataType - data type. * @param {AccessType} data.accessType - access restrictions. * @param {number} data.lowLimit - minimum value. * @param {number} data.highLimit - maximum value. * @param {boolean} data.pdoMapping - enable PDO mapping. * @param {boolean} data.compactSubObj - use the compact sub-object format. * @param {number | string | Date} data.defaultValue - default value. * @param {number} data.scaleFactor - optional multiplier for numeric types. * @fires DataObject#update * @see CiA306 "Object descriptions" (§4.6.3) */ class DataObject extends EventEmitter { constructor(key, data) { super(); Object.assign(this, data); this.parent = null; this.key = key; if (this.key === undefined) throw new ReferenceError('key must be defined'); if (this.parameterName === undefined) throw new EdsError('parameterName is mandatory for DataObject'); if (this.objectType === undefined) this.objectType = ObjectType.VAR; switch (this.objectType) { case ObjectType.DEFTYPE: case ObjectType.VAR: // Mandatory data if (this.dataType === undefined) { throw new EdsError('dataType is mandatory for type ' + this.objectTypeString); } if (this.compactSubObj !== undefined) { throw new EdsError('compactSubObj is not supported for type ' + this.objectTypeString); } // Optional data if (this.accessType === undefined) this.accessType = AccessType.READ_WRITE; if (this.pdoMapping === undefined) this.pdoMapping = false; // Check limits if (this.highLimit !== undefined && this.lowLimit !== undefined) { if (this.highLimit < this.lowLimit) throw new EdsError('highLimit may not be less lowLimit'); } // Create raw data buffer this._raw = typeToRaw(this.defaultValue, this.dataType); break; case ObjectType.DEFSTRUCT: case ObjectType.ARRAY: case ObjectType.RECORD: if (this.compactSubObj) { // Mandatory data if (this.dataType === undefined) { throw new EdsError('dataType is mandatory for compact type ' + this.objectTypeString); } // Optional data if (this.accessType === undefined) this.accessType = AccessType.READ_WRITE; if (this.pdoMapping === undefined) this.pdoMapping = false; } else { // Not supported data if (this.dataType !== undefined) { throw new EdsError('dataType is not supported for type ' + this.objectTypeString); } if (this.accessType !== undefined) { throw new EdsError('accessType is not supported for type ' + this.objectTypeString); } if (this.defaultValue !== undefined) { throw new EdsError('defaultValue is not supported for type ' + this.objectTypeString); } if (this.pdoMapping !== undefined) { throw new EdsError('pdoMapping is not supported for type ' + this.objectTypeString); } if (this.lowLimit !== undefined) { throw new EdsError('lowLimit is not supported for type ' + this.objectTypeString); } if (this.highLimit !== undefined) { throw new EdsError('highLimit is not supported for type ' + this.objectTypeString); } } // Create sub-objects array this._subObjects = []; Object.defineProperty(this, '_subObjects', { enumerable: false }); // Store max sub index at index 0 this.addSubObject(0, { parameterName: 'Max sub-index', objectType: ObjectType.VAR, dataType: DataType.UNSIGNED8, accessType: AccessType.READ_WRITE, }); break; case ObjectType.DOMAIN: // Not supported data if (this.pdoMapping !== undefined) { throw new EdsError('pdoMapping is not supported for type ' + this.objectTypeString); } if (this.lowLimit !== undefined) { throw new EdsError('lowLimit is not supported for type ' + this.objectTypeString); } if (this.highLimit !== undefined) { throw new EdsError('highLimit is not supported for type ' + this.objectTypeString); } if (this.compactSubObj !== undefined) { throw new EdsError('compactSubObj is not supported for type ' + this.objectTypeString); } // Optional data if (this.dataType === undefined) this.dataType = DataType.DOMAIN; if (this.accessType === undefined) this.accessType = AccessType.READ_WRITE; break; default: throw new EdsError( `objectType not supported (${this.objectType})`); } } /** * The Eds index. * * @type {number} */ get index() { return parseInt(this.key.split('sub')[0], 16); } /** * The Eds subIndex. * * @type {number | null} */ get subIndex() { const key = this.key.split('sub'); if (key.length < 2) return null; return parseInt(key[1], 16); } /** * The object type as a string. * * @type {string} */ get objectTypeString() { switch (this.objectType) { case ObjectType.NULL: return 'NULL'; case ObjectType.DOMAIN: return 'DOMAIN'; case ObjectType.DEFTYPE: return 'DEFTYPE'; case ObjectType.DEFSTRUCT: return 'DEFSTRUCT'; case ObjectType.VAR: return 'VAR'; case ObjectType.ARRAY: return 'ARRAY'; case ObjectType.RECORD: return 'RECORD'; default: return 'UNKNOWN'; } } /** * The data type as a string. * * @type {string} */ get dataTypeString() { switch (this.dataType) { case DataType.BOOLEAN: return 'BOOLEAN'; case DataType.INTEGER8: return 'INTEGER8'; case DataType.INTEGER16: return 'INTEGER16'; case DataType.INTEGER32: return 'INTEGER32'; case DataType.UNSIGNED8: return 'UNSIGNED8'; case DataType.UNSIGNED16: return 'UNSIGNED16'; case DataType.UNSIGNED32: return 'UNSIGNED32'; case DataType.REAL32: return 'REAL32'; case DataType.VISIBLE_STRING: return 'VISIBLE_STRING'; case DataType.OCTET_STRING: return 'OCTET_STRING'; case DataType.UNICODE_STRING: return 'UNICODE_STRING'; case DataType.TIME_OF_DAY: return 'TIME_OF_DAY'; case DataType.TIME_DIFFERENCE: return 'TIME_DIFFERENCE'; case DataType.DOMAIN: return 'DOMAIN'; case DataType.REAL64: return 'REAL64'; case DataType.INTEGER24: return 'INTEGER24'; case DataType.INTEGER40: return 'INTEGER40'; case DataType.INTEGER48: return 'INTEGER48'; case DataType.INTEGER56: return 'INTEGER56'; case DataType.INTEGER64: return 'INTEGER64'; case DataType.UNSIGNED24: return 'UNSIGNED24'; case DataType.UNSIGNED40: return 'UNSIGNED40'; case DataType.UNSIGNED48: return 'UNSIGNED48'; case DataType.UNSIGNED56: return 'UNSIGNED56'; case DataType.UNSIGNED64: return 'UNSIGNED64'; case DataType.PDO_PARAMETER: return 'PDO_PARAMETER'; case DataType.PDO_MAPPING: return 'PDO_MAPPING'; case DataType.SDO_PARAMETER: return 'SDO_PARAMETER'; case DataType.IDENTITY: return 'IDENTITY'; default: return 'UNKNOWN'; } } /** * Size of the raw data in bytes including sub-entries. * * @type {number} */ get size() { if (!this.subNumber) return this.raw.length; let size = 0; for (let i = 1; i <= this._subObjects[0].value; ++i) { if (this._subObjects[i] === undefined) continue; size += this._subObjects[i].size; } return size; } /** * The raw data Buffer. * * @type {Buffer} */ get raw() { if (!this.subNumber) return this._raw; const data = []; for (let i = 1; i <= this._subObjects[0].value; ++i) { if (this._subObjects[i] === undefined) continue; data.push(this._subObjects[i].raw); } return data; } set raw(raw) { if (this.subNumber) { throw new EdsError('not supported for type ' + this.objectTypeString); } if (raw === undefined || raw === null) raw = typeToRaw(0, this.dataType); if(this.raw && Buffer.compare(raw, this.raw) == 0) return; this._raw = raw; this._emitUpdate(); } /** * The cooked value. * * @type {number | bigint | string | Date} * @see {@link Eds.typeToRaw} */ get value() { if (!this.subNumber) return rawToType(this.raw, this.dataType, this.scaleFactor); const data = []; for (let i = 1; i <= this._subObjects[0].value; ++i) { if (this._subObjects[i] === undefined) continue; data.push(this._subObjects[i].value); } return data; } set value(value) { if (this.subNumber) { throw new EdsError('not supported for type ' + this.objectTypeString); } this.raw = typeToRaw(value, this.dataType, this.scaleFactor); } /** * Returns true if the object is an instance of DataObject; * * @param {*} obj - object to test. * @returns {boolean} true if obj is DataObject. * @since 6.0.0 */ static isDataObject(obj) { return obj instanceof DataObject; } /** * Primitive value conversion. * * @returns {number | bigint | string | Date} DataObject value. * @since 6.0.0 */ valueOf() { return this.value; } /** * Primitive string conversion. * * @returns {string} DataObject string representation. * @since 6.0.0 */ toString() { return '[' + this.key + ']'; } /** * Get a sub-entry. * * @param {number} index - sub-entry index to get. * @returns {DataObject} new DataObject. * @since 6.0.0 */ at(index) { if (!this._subObjects) throw new TypeError('not an Array type'); return this._subObjects[index]; } /** * Create or add a new sub-entry. * * @param {number} subIndex - sub-entry index to add. * @param {DataObject | object} data - An existing {@link DataObject} or * the data to create one. * @returns {DataObject} new DataObject. * @see {@link Eds#addSubEntry} * @private */ addSubObject(subIndex, data) { if (!this._subObjects) throw new TypeError('not an Array type'); const key = this.key + 'sub' + subIndex; const entry = new DataObject(key, data); entry.parent = this; this._subObjects[subIndex] = entry; // Allow access to the sub-object using bracket notation if (!Object.prototype.hasOwnProperty.call(this, subIndex)) { Object.defineProperty(this, subIndex, { get: () => this.at(subIndex) }); } // Update max sub-index if (this._subObjects[0].value < subIndex) this._subObjects[0]._raw.writeUInt8(subIndex); // Update subNumber this.subNumber = 1; for (let i = 1; i <= this._subObjects[0].value; ++i) { if (this._subObjects[i] !== undefined) this.subNumber += 1; } return entry; } /** * Remove a sub-entry and return it. * * @param {number} subIndex - sub-entry index to remove. * @returns {DataObject} removed DataObject. * @see {@link Eds#removeSubEntry} * @private */ removeSubObject(subIndex) { if (!this._subObjects) throw new TypeError('not an Array type'); const obj = this._subObjects[subIndex]; delete this._subObjects[subIndex]; // Update max sub-index if (subIndex >= this._subObjects[0].value) { // Find the next highest sub-index for (let i = subIndex; i >= 0; --i) { if (this._subObjects[i] !== undefined) { this._subObjects[0]._raw.writeUInt8(i); break; } } } // Update subNumber this.subNumber = 1; for (let i = 1; i <= this._subObjects[0].value; ++i) { if (this._subObjects[i] !== undefined) this.subNumber += 1; } return obj; } /** * Emit the update event. * * @param {DataObject} [obj] - updated object. * @fires DataObject#update * @private */ _emitUpdate(obj) { if(this.parent) { this.parent._emitUpdate(this); } else { /** * The DataObject value was changed. * * @event DataObject#update */ this.emit('update', obj || this); } } } /** * A CANopen Electronic Data Sheet. * * This class provides methods for loading and saving CANopen EDS v4.0 files. * * @param {object} info - file info. * @param {string} info.fileName - file name. * @param {string} info.fileVersion - file version. * @param {string} info.fileRevision - file revision. * @param {string} info.description - What the file is for. * @param {Date} info.creationDate - When the file was created. * @param {string} info.createdBy - Who created the file. * @param {string} info.vendorName - The device vendor name. * @param {number} info.vendorNumber - the device vendor number. * @param {string} info.productName - the device product name. * @param {number} info.productNumber - the device product number. * @param {number} info.revisionNumber - the device revision number. * @param {string} info.orderCode - the device order code. * @param {Array<number>} info.baudRates - supported buadrates * @param {boolean} info.lssSupported - true if LSS is supported. * @see CiA306 "Electronic data sheet specification for CANopen" */ class Eds extends EventEmitter { constructor(info = {}) { super(); this.fileInfo = { EDSVersion: '4.0' }; this.deviceInfo = { SimpleBootUpMaster: 0, SimpleBootUpSlave: 0, Granularity: 8, DynamicChannelsSupported: 0, CompactPDO: 0, GroupMessaging: 0, }; this.dummyUsage = {}; this._dataObjects = {}; this.comments = []; this.nameLookup = {}; if(typeof info === 'object') { // fileInfo this.fileName = info.fileName || ''; this.fileVersion = info.fileVersion || 1; this.fileRevision = info.fileRevision || 1; this.description = info.description || ''; this.creationDate = info.creationDate || new Date(); this.createdBy = info.createdBy || 'node-canopen'; // deviceInfo this.vendorName = info.vendorName || ''; this.vendorNumber = info.vendorNumber || 0; this.productName = info.productName || ''; this.productNumber = info.productNumber || 0; this.revisionNumber = info.revisionNumber || 0; this.orderCode = info.orderCode || ''; this.baudRates = info.baudRates || []; this.lssSupported = info.lssSupported || false; // Add default data types for (const [name, index] of Object.entries(DataType)) { this.addEntry(index, { parameterName: name, objectType: ObjectType.DEFTYPE, dataType: DataType[name], accessType: AccessType.READ_WRITE, }); } // Add mandatory objects (0x1000, 0x1001, 0x1018) this.addEntry(0x1000, { parameterName: 'Device type', objectType: ObjectType.VAR, dataType: DataType.UNSIGNED32, accessType: AccessType.READ_ONLY, }); this.setErrorRegister(0); this.setIdentity({ vendorId: info.vendorNumber, productCode: info.productNumber, revisionNumber: info.revisionNumber, serialNumber: 0, }); } else if(typeof info === 'string') { this.load(info); } } /** * Constructs and returns the Eds DataObjects keyed by decimal string. This * is provided to support old tools. For new code use the new Eds iterator * methods (keyed by hex string) instead. * * @type {object} * @deprecated Use {@link Eds#entries} instead. */ get dataObjects() { const entries = {}; for(const entry of this.values()) entries[entry.index] = entry; return entries; } [Symbol.iterator]() { return this.values(); } /** * File name. * * @type {string} */ get fileName() { return this.fileInfo['FileName']; } set fileName(value) { this.fileInfo['FileName'] = String(value); } /** * File version (8-bit unsigned integer). * * @type {number} */ get fileVersion() { return this.fileInfo['FileVersion']; } set fileVersion(value) { this.fileInfo['FileVersion'] = Number(value); } /** * File revision (8-bit unsigned integer). * * @type {number} */ get fileRevision() { return this.fileInfo['FileRevision']; } set fileRevision(value) { this.fileInfo['FileRevision'] = Number(value); } /** * File description. * * @type {string} */ get description() { return this.fileInfo['Description']; } set description(value) { this.fileInfo['Description'] = String(value); } /** * File creation time. * * @type {Date} */ get creationDate() { const time = this.fileInfo['CreationTime']; const date = this.fileInfo['CreationDate']; return parseDate(time, date); } set creationDate(value) { const hours = value.getHours().toString().padStart(2, '0'); const minutes = value.getMinutes().toString().padStart(2, '0'); const time = hours + ':' + minutes; const month = (value.getMonth() + 1).toString().padStart(2, '0'); const day = value.getDate().toString().padStart(2, '0'); const year = value.getFullYear().toString(); const date = month + '-' + day + '-' + year; this.fileInfo['CreationTime'] = time; this.fileInfo['CreationDate'] = date; } /** * Name or description of the file creator (max 245 characters). * * @type {string} */ get createdBy() { return this.fileInfo['CreatedBy']; } set createdBy(value) { this.fileInfo['CreatedBy'] = String(value); } /** * Time of the last modification. * * @type {Date} */ get modificationDate() { const time = this.fileInfo['ModificationTime']; const date = this.fileInfo['ModificationDate']; return parseDate(time, date); } set modificationDate(value) { const hours = value.getHours().toString().padStart(2, '0'); const minutes = value.getMinutes().toString().padStart(2, '0'); const time = hours + ':' + minutes; const month = (value.getMonth() + 1).toString().padStart(2, '0'); const day = value.getDate().toString().padStart(2, '0'); const year = value.getFullYear().toString(); const date = month + '-' + day + '-' + year; this.fileInfo['ModificationTime'] = time; this.fileInfo['ModificationDate'] = date; } /** * Name or description of the last modifier (max 244 characters). * * @type {string} */ get modifiedBy() { return this.fileInfo['ModifiedBy']; } set modifiedBy(value) { this.fileInfo['ModifiedBy'] = String(value); } /** * Vendor name (max 244 characters). * * @type {string} */ get vendorName() { return this.deviceInfo['VendorName']; } set vendorName(value) { this.deviceInfo['VendorName'] = String(value); } /** * Unique vendor ID (32-bit unsigned integer). * * @type {number} */ get vendorNumber() { return this.deviceInfo['VendorNumber']; } set vendorNumber(value) { this.deviceInfo['VendorNumber'] = Number(value); } /** * Product name (max 243 characters). * * @type {string} */ get productName() { return this.deviceInfo['ProductName']; } set productName(value) { this.deviceInfo['ProductName'] = String(value); } /** * Product code (32-bit unsigned integer). * * @type {number} */ get productNumber() { return this.deviceInfo['ProductNumber']; } set productNumber(value) { this.deviceInfo['ProductNumber'] = Number(value); } /** * Revision number (32-bit unsigned integer). * * @type {number} */ get revisionNumber() { return this.deviceInfo['RevisionNumber']; } set revisionNumber(value) { this.deviceInfo['RevisionNumber'] = Number(value); } /** * Product order code (max 245 characters). * * @type {string} */ get orderCode() { return this.deviceInfo['OrderCode']; } set orderCode(value) { this.deviceInfo['OrderCode'] = String(value); } /** * Supported baud rates. * * @type {Array<number>} */ get baudRates() { let rates = []; if (parseInt(this.deviceInfo['BaudRate_10'])) rates.push(10000); if (parseInt(this.deviceInfo['BaudRate_20'])) rates.push(20000); if (parseInt(this.deviceInfo['BaudRate_50'])) rates.push(50000); if (parseInt(this.deviceInfo['BaudRate_125'])) rates.push(125000); if (parseInt(this.deviceInfo['BaudRate_250'])) rates.push(250000); if (parseInt(this.deviceInfo['BaudRate_500'])) rates.push(500000); if (parseInt(this.deviceInfo['BaudRate_800'])) rates.push(800000); if (parseInt(this.deviceInfo['BaudRate_1000'])) rates.push(1000000); return rates; } set baudRates(rates) { this.deviceInfo['BaudRate_10'] = rates.includes(10000) ? '1' : '0'; this.deviceInfo['BaudRate_20'] = rates.includes(20000) ? '1' : '0'; this.deviceInfo['BaudRate_50'] = rates.includes(50000) ? '1' : '0'; this.deviceInfo['BaudRate_125'] = rates.includes(125000) ? '1' : '0'; this.deviceInfo['BaudRate_250'] = rates.includes(250000) ? '1' : '0'; this.deviceInfo['BaudRate_500'] = rates.includes(500000) ? '1' : '0'; this.deviceInfo['BaudRate_800'] = rates.includes(800000) ? '1' : '0'; this.deviceInfo['BaudRate_1000'] = rates.includes(1e6) ? '1' : '0'; } /** * Indicates simple boot-up master functionality (not supported). * * @type {boolean} */ get simpleBootUpMaster() { return !!parseInt(this.deviceInfo['SimpleBootUpMaster']); } set simpleBootUpMaster(value) { this.deviceInfo['SimpleBootUpMaster'] = (value) ? 1 : 0; } /** * Indicates simple boot-up slave functionality (not supported). * * @type {boolean} */ get simpleBootUpSlave() { return !!parseInt(this.deviceInfo['SimpleBootUpSlave']); } set simpleBootUpSlave(value) { this.deviceInfo['SimpleBootUpSlave'] = (value) ? 1 : 0; } /** * Provides the granularity allowed for the mapping on this device - most * devices support a granularity of 8. (8-bit integer, max 64). * * @type {number} */ get granularity() { return parseInt(this.deviceInfo['Granularity']); } set granularity(value) { this.deviceInfo['Granularity'] = value; } /** * Indicates the facility of dynamic variable generation (not supported). * * @type {boolean} * @see CiA302 */ get dynamicChannelsSupported() { return !!parseInt(this.deviceInfo['DynamicChannelsSupported']); } set dynamicChannelsSupported(value) { this.deviceInfo['DynamicChannelsSupported'] = (value) ? 1 : 0; } /** * Indicates the facility of multiplexed PDOs (not supported). * * @type {boolean} * @see CiA301 */ get groupMessaging() { return !!parseInt(this.deviceInfo['GroupMessaging']); } set groupMessaging(value) { this.deviceInfo['GroupMessaging'] = (value) ? 1 : 0; } /** * The number of supported receive PDOs (16-bit unsigned integer). * * @type {number} */ get nrOfRXPDO() { let count = 0; for (let index of Object.keys(this._dataObjects)) { index = parseInt(index, 16); if (index >= 0x1400 && index <= 0x15FF) count++; } return count; } /** * The number of supported transmit PDOs (16-bit unsigned integer). * * @type {number} */ get nrOfTXPDO() { let count = 0; for (let index of Object.keys(this._dataObjects)) { index = parseInt(index, 16); if (index >= 0x1800 && index <= 0x19FF) count++; } return count; } /** * Indicates if LSS functionality is supported. * * @type {boolean} */ get lssSupported() { return !!(this.deviceInfo['LSS_Supported']); } set lssSupported(value) { this.deviceInfo['LSS_Supported'] = (value) ? 1 : 0; } /** * Returns true if the object is an instance of Eds. * * @param {object} obj - object to test. * @returns {boolean} true if obj is Eds. * @since 6.0.0 */ static isEds(obj) { return obj instanceof Eds; } /** * Create a new Eds from a file path. * * @param {string} path - path to file. * @returns {Eds} new Eds object. * @since 6.0.0 */ static fromFile(path) { const eds = new Eds(); eds.load(path); return eds; } /** * Read and parse an EDS file. * * @param {string} path - path to file. */ load(path) { // Parse EDS file const file = ini.parse(fs.readFileSync(path, 'utf-8')); // Clear existing entries this._dataObjects = {}; this.nameLookup = {}; // Extract header fields this.fileInfo = file['FileInfo']; this.deviceInfo = file['DeviceInfo']; this.dummyUsage = file['DummyUsage']; this.comments = file['Comments']; // Construct data objects. const entries = Object.entries(file); const indexMatch = RegExp('^[0-9A-Fa-f]{4}$'); const subIndexMatch = RegExp('^([0-9A-Fa-f]{4})sub([0-9A-Fa-f]+)$'); entries .filter(([key]) => { return indexMatch.test(key); }) .forEach(([key, data]) => { const index = parseInt(key, 16); this.addEntry(index, edsToEntry(data)); }); entries .filter(([key]) => { return subIndexMatch.test(key); }) .forEach(([key, data]) => { let [index, subIndex] = key.split('sub'); index = parseInt(index, 16); subIndex = parseInt(subIndex, 16); this.addSubEntry(index, subIndex, edsToEntry(data)); }); } /** * Write an EDS file. * * @param {string} path - path to file, defaults to fileName. * @param {object} [options] - optional inputs. * @param {Date} [options.modificationDate] - file modification date to file. * @param {Date} [options.modifiedBy] - file modification date to file. */ save(path, options = {}) { if (!path) path = this.fileName; this.modificationDate = options.modificationDate || new Date(); this.modifiedBy = options.modifiedBy || ''; this.deviceInfo['NrOfTXPDO'] = this.nrOfTXPDO; this.deviceInfo['NrOfRXPDO'] = this.nrOfRXPDO; const fd = fs.openSync(path, 'w'); // Write header fields this._write(fd, ini.encode(this.fileInfo, { section: 'FileInfo' })); this._write(fd, ini.encode(this.deviceInfo, { section: 'DeviceInfo' })); this._write(fd, ini.encode(this.dummyUsage, { section: 'DummyUsage' })); this._write(fd, ini.encode(this.comments, { section: 'Comments' })); // Sort data objects let mandObjects = {}; let mandCount = 0; let optObjects = {}; let optCount = 0; let mfrObjects = {}; let mfrCount = 0; for (const key of this.keys()) { let index = parseInt(key, 16); if ([0x1000, 0x1001, 0x1018].includes(index)) { mandCount += 1; mandObjects[mandCount] = '0x' + key; } else if (index >= 0x1000 && index < 0x1FFF) { optCount += 1; optObjects[optCount] = '0x' + key; } else if (index >= 0x2000 && index < 0x5FFF) { mfrCount += 1; mfrObjects[mfrCount] = '0x' + key; } else if (index >= 0x6000 && index < 0xFFFF) { optCount += 1; optObjects[optCount] = '0x' + key; } } // Write data objects mandObjects['SupportedObjects'] = mandCount; this._write(fd, ini.encode(mandObjects, { section: 'MandatoryObjects' })); this._writeObjects(fd, mandObjects); optObjects['SupportedObjects'] = optCount; this._write(fd, ini.encode(optObjects, { section: 'OptionalObjects' })); this._writeObjects(fd, optObjects); mfrObjects['SupportedObjects'] = mfrCount; this._write(fd, ini.encode( mfrObjects, { section: 'ManufacturerObjects' })); this._writeObjects(fd, mfrObjects); fs.closeSync(fd); } /** * Returns a new iterator object that iterates the keys for each entry. * * @returns {Iterable.<string>} Iterable keys. * @since 6.0.0 */ keys() { return Object.keys(this._dataObjects).values(); } /** * Returns a new iterator object that iterates DataObjects. * * @returns {Iterable.<DataObject>} Iterable DataObjects. * @since 6.0.0 */ values() { return Object.values(this._dataObjects).values(); } /** * Returns a new iterator object that iterates key/DataObjects pairs. * * @returns {Iterable.<Array>} Iterable [key, DataObjects]. * @since 6.0.0 */ entries() { return Object.entries(this._dataObjects).values(); } /** * Reset objects to their default values. * * @since 6.0.0 */ reset() { for (const entry of this.values()) { if(entry.objectType === ObjectType.VAR) entry.value = entry.defaultValue; } } /** * Get a data object by name. * * @param {string} name - name of the data object. * @returns {Array<DataObject>} - all entries matching name. * @since 6.0.0 */ findEntry(name) { let result = this.nameLookup[name]; if (result !== undefined) return result; return []; } /** * Get a data object by index. * * @param {number} index - index of the data object. * @returns {DataObject | null} - entry matching index. */ getEntry(index) { let entry = null; if (typeof index === 'string') { // Name lookup entry = this.findEntry(index); if (entry.length > 1) throw new EdsError('duplicate entry'); entry = entry[0]; } else { // Index lookup. const key = index.toString(16).padStart(4, '0'); entry = this._dataObjects[key]; } return entry; } /** * Create a new entry. * * @param {number} index - index of the data object. * @param {object} data - data passed to the {@link DataObject} constructor. * @returns {DataObject} - the newly created entry. * @fires Eds#newEntry */ addEntry(index, data) { if(typeof index !== 'number') throw new TypeError('index must be a number'); const key = index.toString(16).padStart(4, '0'); if (this._dataObjects[key] !== undefined) throw new EdsError(`${key} already exists`); const entry = new DataObject(key, data); /** * A DataObject was added to the Eds. * * @event Eds#newEntry * @type {DataObject} */ this.emit('newEntry', entry); this._dataObjects[key] = entry; if (this.nameLookup[entry.parameterName] === undefined) this.nameLookup[entry.parameterName] = []; this.nameLookup[entry.parameterName].push(entry); return entry; } /** * Delete an entry. * * @param {number} index - index of the data object. * @returns {DataObject} the deleted entry. * @fires Eds#removeEntry */ removeEntry(index) { const entry = this.getEntry(index); if (entry === undefined) throw new EdsError(`${index.toString(16)} does not exist`); this.nameLookup[entry.parameterName].splice( this.nameLookup[entry.parameterName].indexOf(entry), 1); if (this.nameLookup[entry.parameterName].length == 0) delete this.nameLookup[entry.parameterName]; delete this._dataObjects[entry.key]; /** * A DataObject was removed from the Eds. * * @event Eds#removeEntry * @type {DataObject} */ this.emit('removeEntry', entry); return entry; } /** * Get a sub-entry. * * @param {number | string} index - index or name of the data object. * @param {number} subIndex - subIndex of the data object. * @returns {DataObject | null} - the sub-entry or null. */ getSubEntry(index, subIndex) { const entry = this.getEntry(index); if (entry === undefined) throw new EdsError(`${index.toString(16)} does not exist`); if (entry.subNumber === undefined) { throw new EdsError( `${index.toString(16)} does not support sub objects`); } return entry[subIndex] || null; } /** * Create a new sub-entry. * * @param {number} index - index of the data object. * @param {number} subIndex - subIndex of the data object. * @param {object} data - data passed to the {@link DataObject} constructor. * @returns {DataObject} - the newly created sub-entry. */ addSubEntry(index, subIndex, data) { const entry = this.getEntry(index); if (entry === undefined) throw new EdsError(`${index.toString(16)} does not exist`); if (entry.subNumber === undefined) { throw new EdsError( `${index.toString(16)} does not support sub objects`); } // Add the new entry return entry.addSubObject(subIndex, data); } /** * Delete a sub-entry. * * @param {number} index - index of the data object. * @param {number} subIndex - subIndex of the data object. */ removeSubEntry(index, subIndex) { const entry = this.getEntry(index); if (subIndex < 1) throw new EdsError('subIndex must be >= 1'); if (entry === undefined) throw new EdsError(`${index.toString(16)} does not exist`); if (entry.subNumber === undefined) { throw new EdsError( `${index.toString(16)} does not support sub objects`); } if (entry[subIndex] === undefined) return; // Delete the entry entry.removeSubObject(subIndex); } /** * Get object 0x1001 - Error register. * * @returns {number} error register value. * @since 6.0.0 */ getErrorRegister() { const obj1001 = this.getEntry(0x1001); if (obj1001) return obj1001.value; return null; } /** * Set object 0x1001 - Error register. * - bit 0 - Generic error. * - bit 1 - Current. * - bit 2 - Voltage. * - bit 3 - Temperature. * - bit 4 - Communication error. * - bit 5 - Device profile specific. * - bit 6 - Reserved (always 0). * - bit 7 - Manufacturer specific. * * @param {number | object} flags - error flags. * @param {boolean} flags.generic - generic error. * @param {boolean} flags.current - current error. * @param {boolean} flags.voltage - voltage error. * @param {boolean} flags.temperature - temperature error. * @param {boolean} flags.communication - communication error. * @param {boolean} flags.device - device profile specific error. * @param {boolean} flags.manufacturer - manufacturer specific error. * @since 6.0.0 */ setErrorRegister(flags) { let obj1001 = this.getEntry(0x1001); if (obj1001 === undefined) { obj1001 = this.addEntry(0x1001, { parameterName: 'Error register', objectType: ObjectType.VAR, dataType: DataType.UNSIGNED8, accessType: AccessType.READ_ONLY, }); } if (typeof flags !== 'object') { obj1001.value = flags; } else { let value = obj1001.value; if (flags.generic !== undefined) { if (flags.generic) value |= (1 << 0); else value &= ~(1 << 0); } if (flags.current !== undefined) { if (flags.current) value |= (1 << 1); else value &= ~(1 << 1); } if (flags.voltage !== undefined) { if (flags.voltage) value |= (1 << 2); else value &= ~(1 << 2); } if (flags.temperature !== undefined) { if (flags.temperature) value |= (1 << 3); else value &= ~(1 << 3); } if (flags.communication !== undefined) { if (flags.communication) value |= (1 << 4); else value &= ~(1 << 4); } if (flags.device !== undefined) { if (flags.device) value |= (1 << 5); else value &= ~(1 << 5); } if (flags.manufacturer !== undefined) { if (flags.manufacturer) value |= (1 << 7); else value &= ~(1 << 7); } obj1001.value = value; } } /** * Get object 0x1002 - Manufacturer status register. * * @returns {number} status register value. * @since 6.0.0 */ getStatusRegister() { const obj1002 = this.getEntry(0x1002); if (obj1002) return obj1002.value; return null; } /** * Set object 0x1002 - Manufacturer status register. * * @param {number} status - status register. * @param {object} [options] - DataObject creation options. * @param {boolean} [options.saveDefault] - save value as default. * @since 6.0.0 */ setStatusRegister(status, options = {}) { let obj1002 = this.getEntry(0x1002); if (obj1002 === undefined) { obj1002 = this.addEntry(0x1002, { parameterName: 'Manufacturer status register', objectType: ObjectType.VAR, dataType: DataType.UNSIGNED32, accessType: AccessType.READ_ONLY, }); } obj1002.value = status; if (options.saveDefault) obj1002.defaultValue = obj1002.value; } /** * Get object 0x1003 - Pre-defined error field. * * @returns {Array<object>} [{ code, info } ... ] * @since 6.0.0 */ getErrorHistory() { const history = []; const obj1003 = this.getEntry(0x1003); if (obj1003) { const maxSubIndex = obj1003[0].value; for (let i = 1; i <= maxSubIndex; ++i) { const subObj = obj1003.at(i); const code = subObj.raw.readUInt16LE(0); const info = subObj.raw.readUInt16LE(2); if (code) history.push({ code, info }); } } return history; } /** * Push an entry to object 0x1003 - Pre-defined error field. * - bit 0..15 - Error code. * - bit 16..31 - Additional info. * * @param {number} code - error code. * @param {Buffer | number} info - error info (2 bytes). * @since 6.0.0 */ pushErrorHistory(code, info) { const obj1003 = this.getEntry(0x1003); if (!obj1003) throw new EdsError(); const maxSubIndex = obj1003[0].value; if (maxSubIndex > 1) { // Shift buffers for (let i = maxSubIndex; i > 1; --i) obj1003.at(i).raw = obj1003.at(i - 1).raw; } // Write new value to sub-index 1 const raw = Buffer.alloc(4); raw.writeUInt16LE(code, 0); if (info) { if (typeof info === 'number') { raw.writeUInt16LE(info, 2); } else { if (!Buffer.isBuffer(info)) info = Buffer.from(info); info.copy(raw, 2); } } obj1003.at(1).raw = raw; } /** * Configures the length of 0x1003 - Pre-defined error field. * * @param {number} length - how many historical error events should be kept. * @param {object} [options] - DataObject creation options. * @param {AccessType} [options.accessType] - DataObject access type. * @since 6.0.0 */ setErrorHistoryLength(length, options = {}) { if (length === undefined || length < 0) throw new EdsError('error field size must >= 0'); let obj1003 = this.getEntry(0x1003); if (obj1003 === undefined) { obj1003 = this.addEntry(0x1003, { parameterName: 'Pre-defined error field', objectType: ObjectType.ARRAY, }); } while (length < obj1003.subNumber - 1) { // Remove extra entries this.removeSubEntry(0x1003, obj1003.subNumber - 1); } while (length > obj1003.subNumber - 1) { // Add new entries