canopen
Version:
CANopen implementation for Javascript
1,723 lines (1,473 loc) • 109 kB
JavaScript
/**
* @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