UNPKG

@yinyinfurong_zmr/dbc-can

Version:

A general purpose CAN (Controller Area Network) toolbox with support for .dbc file parsing, CAN message decoding, and more

605 lines 23.1 kB
import DbcParser from '../parser/DbcParser'; import Writer from './Writer'; import { MessageDoesNotExist, SignalDoesNotExist } from './Errors'; import { computeDataType } from '../shared/DataTypes'; /** * Creates a DBC instance that allows for parsing/loading of an existing DBC file * or write data to a new DBC file. * * If loading data from an existing file, simply call: * const dbc = new Dbc(); * dbc.load(fileContent) * * By default, when a new Dbc() instance is created, the encapsulated data will be empty. * If you are wanting to create fresh data you can call createMessage or createSignal to * create messages and signals, respectively. * Calls to createMessage and createSignal do not by default add the messages to the data, * you will need to make subsequent calls to addMessage or addSignal to add the data * to the class. * */ class Dbc { constructor() { this.errors = new Map(); this.data = this.initDbcDataObj(); } /** * Adds a version number to dbc data */ set version(version) { this.data.version = version; } /** * Adds a short description for the DBC data */ set description(description) { this.data.description = description; } createNode(name, options) { let description; let attributes; options && options.description ? (description = options.description) : (description = null); options && options.attributes ? (attributes = options.attributes) : (attributes = new Map()); const node = { name, description, attributes, add: () => { this.data.nodes.set(node.name, node); return node; }, updateDescription: (content) => { node.description = content; return node; }, addAttribute: (attrName, type, attrProps, attrOptions) => { if (attrProps) { attrProps.type = 'Node'; } else { attrProps = { type: 'Node' }; } const attr = this.createAttribute(attrName, type, attrProps, attrOptions); this.addAttribute(attr, { node: node.name }); return node; }, }; return node; } /** * * Creates a Message instance that can later be added using addMessage() * or using the attached method .add() * * Ex. * let msg = dbc.createMessage('MessageName',100,8); * msg.add(); or dbc.addMessage(msg); * * @param name Name of CAN message * @param id ID of CAN message * @param dlc Data Length Code (data length) of CAN message * @param options Optional attributes that can be used when creating a message: signals, attributes, signalGroups, * sendingNode, and description * @returns Message */ createMessage(name, id, dlc, extended, options) { // TODO: Check that ID does not exceed max range let signals; let baseSignals; let multiplexSignals; let attributes; let signalGroups; let sendingNode; let description; options && options.signals ? (signals = options.signals) : (signals = new Map()); options && options.baseSignals ? (baseSignals = options.baseSignals) : (baseSignals = new Map()); options && options.multiplexSignals ? (multiplexSignals = options.multiplexSignals) : (multiplexSignals = new Map()); options && options.attributes ? (attributes = options.attributes) : (attributes = new Map()); options && options.signalGroups ? (signalGroups = options.signalGroups) : (signalGroups = new Map()); options && options.description ? (description = options.description) : (description = null); options && options.sendingNode ? (sendingNode = options.sendingNode) : (sendingNode = null); if (sendingNode) { this.createNode(sendingNode).add(); } const message = { name, id, extended, dlc, sendingNode, signals, baseSignals, multiplexSignals, description, attributes, signalGroups, add: () => { this.addMessage(message); return message; }, addSignal: (signalName, startBit, length, additionalOptions) => { const signal = this.createSignal(signalName, startBit, length, additionalOptions); this.addSignal(message.name, signal); return message; }, updateDescription: (content) => { message.description = content; return message; }, updateNode: (node) => { message.sendingNode = node; this.createNode(node).add(); return message; }, addAttribute: (attrName, type, attrProps, attrOptions) => { if (attrProps) { attrProps.type = 'Message'; } else { attrProps = { type: 'Message' }; } const attr = this.createAttribute(attrName, type, attrProps, attrOptions); this.addAttribute(attr, { id: message.id }); return message; }, }; return message; } /** * * Adds/appends message to existing message list * * @param message Message object to be added */ addMessage(message) { const errorOnDuplicate = (name) => { if (this.data.messages.has(name)) { throw new Error(`Can not add message ${name} as ${name} already exists. Unique message names are required.`); } }; if (Array.isArray(message)) { message.forEach((msg) => { errorOnDuplicate(msg.name); this.data.messages.set(msg.name, msg); }); } else { errorOnDuplicate(message.name); this.data.messages.set(message.name, message); } } /** * Removes an existing message from the DBC data * @param messageName Name of the message to remove */ removeMessage(messageName) { const ret = this.data.messages.delete(messageName); if (!ret) throw new Error(`${messageName} does not exist in the database`); } /** * Creates a signal based on characteristics such as start bit and length * @param name Name of the signal * @param startBit Bit position of where the signal is to start in the Message bitfield * @param length Length of the signal * @param options Additional options, such as signed, endian, etc. */ createSignal(name, startBit, length, options) { let min; let max; let factor; let offset; let isFloat; let signed; let endian; let dataType; let unit; let description; let multiplex; let multiplexer; let receivingNodes; let valueTable; let attributes; options && options.signed ? (signed = options.signed) : (signed = false); options && options.endian ? (endian = options.endian) : (endian = 'Intel'); options && options.min ? (min = options.min) : (min = 0); options && options.max ? (max = options.max) : (max = 0); options && options.offset ? (offset = options.offset) : (offset = 0); options && options.factor ? (factor = options.factor) : (factor = 1); options && options.isFloat ? (isFloat = options.isFloat) : (isFloat = false); options && options.unit ? (unit = options.unit) : (unit = ''); options && options.description ? (description = options.description) : (description = null); options && options.multiplex ? (multiplex = options.multiplex) : (multiplex = null); options && options.multiplexer ? (multiplexer = options.multiplexer) : (multiplexer = false); options && options.receivingNodes ? (receivingNodes = options.receivingNodes) : (receivingNodes = []); options && options.valueTable ? (valueTable = options.valueTable) : (valueTable = null); options && options.attributes ? (attributes = options.attributes) : (attributes = new Map()); dataType = computeDataType(length, signed, isFloat); if (receivingNodes.length) { receivingNodes.forEach((node) => { this.createNode(node).add(); }); } const signal = { name, multiplex, multiplexer, startBit, length, endian, signed, factor, offset, min, max, unit, receivingNodes, description, valueTable, attributes, dataType, add: (messageName) => { this.addSignal(messageName, signal); return signal; }, updateDescription: (content) => { signal.description = content; return signal; }, addAttribute: (attrName, messageId, type, attrProps, attrOptions) => { if (attrProps) { attrProps.type = 'Signal'; } else { attrProps = { type: 'Signal' }; } const attr = this.createAttribute(attrName, type, attrProps, attrOptions); this.addAttribute(attr, { id: messageId, signalName: signal.name }); return signal; }, isMultiplex: () => { var _a; return signal.multiplexer || ((_a = signal.multiplex) !== null && _a !== void 0 ? _a : '').length > 0; }, }; return signal; } /** * Removes a signal from an existing message * @param signalName Name of the signal * @param messageName Name of the message containing the signal */ removeSignal(signalName, messageName) { const msg = this.getMessageByName(messageName); if (msg) { const ret = msg.signals.delete(signalName); if (!ret) throw new Error(`${signalName} does not exist in message ${messageName}`); } } /** * * Adds a Signal object to a specified Message * * @param messageName Name of the message the signal will be added to * @param signal Signal object to be added to the specified message */ addSignal(messageName, signal) { const message = this.data.messages.get(messageName); if (message) { if (Array.isArray(signal)) { signal.forEach((sig) => { message.signals.set(sig.name, sig); }); } else { message.signals.set(signal.name, signal); } } else { throw new MessageDoesNotExist(`No message with name ${messageName} exists in the database.`); } } /** * * Returns a message with the corresponding CAN ID. If message does not exist * a MessageDoesNotExist error will be thrown. * * @param id The CAN ID of the message wanting to be found * @returns Message * @throws MessageDoesNotExist */ getMessageById(id) { const messages = this.data.messages; for (const [name, message] of messages) { if (message.id === id) { return message; } } throw new MessageDoesNotExist(`No message with id ${id} exists in the database.`); } /** * * Finds a specific message within the DBC file data by name * * @param name string * @returns Message * @error MessageDoesNotExist */ getMessageByName(name) { try { return this.data.messages.get(name); } catch (e) { throw new MessageDoesNotExist(`No message with name ${name} exists in the database.`); } } /** * * Returns a signal object located in a specific CAN message by name * * @param name string * @param messageName string * @returns Signal * @error SignalDoesNotExist */ getSignalByName(name, messageName) { const msg = this.getMessageByName(messageName); const signals = msg === null || msg === void 0 ? void 0 : msg.signals; if (signals) { for (const [signal, signalObj] of signals) { if (signal === name) { return signalObj; } } } else { throw new SignalDoesNotExist(`Signal could not be found in ${messageName}, because the signal list for that message is empty.`); } throw new SignalDoesNotExist(`Could not find ${name} in signal list.`); } /** * Convenience method that will create an Attribute object that can be appended to DBC data * @param name Name to be assigned to the attribute * @param type Attribute type: FLOAT, HEX, ENUM, INT, STRING * @param props Required properties of the attribute based on the type provided * @param options Additional attribute options that can be added */ createAttribute(name, type, props, options) { let attrType = 'Global'; let enumMembers = null; let min = null; let max = null; let value = null; let defaultValue = null; if (props) { if (props.type) attrType = props.type; if (type === 'ENUM' && !props.enumMembers) { throw new Error('enumMembers is a required property when defining an attribute with type ENUM'); } else if (type === 'ENUM') { if (props.enumMembers) { enumMembers = props.enumMembers; } } if (type !== 'ENUM' && type !== 'STRING' && !props.min && !props.max) { throw new Error('min and max are required properties when defining anything other than type ENUM and STRING'); } else { if (props.min !== undefined) min = props.min; if (props.max !== undefined) max = props.max; } if (options) { if (options.defaultValue !== undefined) { defaultValue = options.defaultValue; } else if (options.value !== undefined && options.defaultValue === undefined) { value = options.value; defaultValue = value; } } } else if (!props && type !== 'STRING') { throw new Error('Additional attribute properties are required for any type other than STRING'); } const attribute = { name, type: attrType, dataType: type, min, max, options: enumMembers, value, defaultValue, }; return attribute; } /** * Adds an existing attribute to the DBC data based on the supplied type * @param attribute Attribute * @param options node, id, signalName, or evName */ addAttribute(attribute, options) { switch (attribute.type) { case 'Message': if (options && !options.id) { throw new Error('ID is a required option for adding a Message attribute'); } if (options === null || options === void 0 ? void 0 : options.id) { const msg = this.getMessageById(options.id); msg.attributes.set(attribute.name, attribute); } break; case 'Signal': if ((options && !options.id) || !(options === null || options === void 0 ? void 0 : options.signalName)) { throw new Error('Signal name/and message ID are required options for adding a Signal attribute'); } if ((options === null || options === void 0 ? void 0 : options.id) && (options === null || options === void 0 ? void 0 : options.signalName)) { const signal = this.getSignalByName(options.signalName, this.messageIdToName(options.id)); signal.attributes.set(attribute.name, attribute); } break; case 'Node': if (options && !options.node) { throw new Error('Node name is a required option for adding a Node attribute'); } if (options === null || options === void 0 ? void 0 : options.node) { const node = this.getNode(options.node); node.attributes.set(attribute.name, attribute); } break; case 'EnvironmentVariable': if (options && !options.evName) { throw new Error('Environmental Variable name is a required option for adding EV Attribute'); } if (options === null || options === void 0 ? void 0 : options.evName) { const ev = this.getEnvironmentalVariable(options.evName); ev.attributes.set(attribute.name, attribute); } break; case 'Global': this.data.attributes.set(attribute.name, attribute); break; } } /** * Returns an environmental variable by name * @param name Name of environmental variable * @throws Error if environmental variable does not exist in database */ getEnvironmentalVariable(name) { const ev = this.data.environmentVariables.get(name); if (!ev) { throw new Error('${name} is not an existing environmental variable in the database'); } return ev; } /** * Returns a node if it exists in the database * @param name Name of the node (string) * @throws Error if node does not exist */ getNode(name) { const node = this.data.nodes.get(name); if (!node) { throw new Error(`${name} is not an existing node in the database`); } return node; } /** * Returns the mapped name in the database based on the supplied CAN ID * @param id Message ID (number) * @throws Error if no message with the corresponding ID exists */ messageIdToName(id) { const name = this.getMessageById(id).name; if (!name) { throw new Error(`Could not find ${id} in the database`); } return name; } /** * * Loads a DBC file, as opposed to the default method 'load', which is * a non-blocking/async call whose promise must be caught for the return data to be used. * * @param fileContent Full file path to the dbc file, including extension * @param throwOnError * @returns DbcData Data contained in the dbc file */ load(fileContent, throwOnError = false) { let data = this.initDbcDataObj(); let lineNum = 1; const errMap = new Map(); const lines = fileContent.split('\n'); lines.forEach((line) => { const parser = new DbcParser(line); const parseErrors = parser.parseResult.errs; if (parseErrors.length === 0) { data = parser.updateData(data); } else { if (throwOnError) { throw new Error(`A syntax error occurred on line ${lineNum} - Reason: ${parseErrors}`); } errMap.set(lineNum, parseErrors); } lineNum++; }); // Clean up attributes that are not global/scoped Array.from(data.attributes.entries()).forEach(([key, attribute]) => { if (attribute.type !== 'Global') data.attributes.delete(key); }); // Set parsing errors this.errors = errMap; // Add table data to class instance for future referencing this.data = data; return data; } /** * * Writes the encapsulated data of a Dbc class instance to a dbc file */ write() { const writer = new Writer(); writer.constructFile(this.data); return writer.dbcString; } /** * * Transforms the internal DBC data from class instance into a JSON object/string * * @param options Additional formatting options, such as pretty print. * @returns JSON representation of loaded DBC data */ toJson(options) { const replacer = (key, value) => { if (value instanceof Map) { if (key === 'valueTable' || key === 'valueTables') { return Object.fromEntries(value.entries()); } return Array.from(value.values()); // or with spread: value: [...value] } else { return value; } }; let indent = 0; let pretty; options && options.pretty ? (pretty = options.pretty) : (pretty = true); if (pretty) { indent = 2; } let json; if (options && options.preserveFormat) { json = JSON.stringify(this.data, undefined, indent); } else { json = JSON.stringify(this.data, replacer, indent); } return json; } initDbcDataObj() { return { version: null, messages: new Map(), description: null, busSpeed: null, nodes: new Map(), valueTables: new Map(), attributes: new Map(), newSymbols: [], environmentVariables: new Map(), networkBridges: new Map(), }; } } export default Dbc; //# sourceMappingURL=Dbc.js.map