cabana/src/models/can/dbc.js

650 lines
19 KiB
JavaScript

import CloudLog from '../../logging/CloudLog';
import Signal from './signal';
import Frame from './frame';
import BoardUnit from './BoardUnit';
import DbcUtils from '../../utils/dbc';
const DBC_COMMENT_RE = /^CM_ *"(.*)";/;
const DBC_COMMENT_MULTI_LINE_RE = /^CM_ *"(.*)/;
const MSG_RE = /^BO_ (\w+) (\w+) *: (\w+) (\w+)/;
const SIGNAL_RE = /^SG_ (\w+) : (\d+)\|(\d+)@(\d+)([+|-]) \(([0-9.+-eE]+),([0-9.+-eE]+)\) \[([0-9.+-eE]+)\|([0-9.+-eE]+)\] "(.*)" (.*)/;
// Multiplexed signal
const MP_SIGNAL_RE = /^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([+|-]) \(([0-9.+-eE]+),([0-9.+-eE]+)\) \[([0-9.+-eE]+)\|([0-9.+-eE]+)\] "(.*)" (.*)/;
const VAL_RE = /^VAL_ (\w+) (\w+) (.*);/;
const VAL_TABLE_RE = /^VAL_TABLE_ (\w+) (.*);/;
const MSG_TRANSMITTER_RE = /^BO_TX_BU_ ([0-9]+) *: *(.+);/;
const SIGNAL_COMMENT_RE = /^CM_ SG_ *(\w+) *(\w+) *"(.*)";/;
const SIGNAL_COMMENT_MULTI_LINE_RE = /^CM_ SG_ *(\w+) *(\w+) *"(.*)/;
// Message Comments (CM_ BO_ )
const MESSAGE_COMMENT_RE = /^CM_ BO_ *(\w+) *"(.*)";/;
const MESSAGE_COMMENT_MULTI_LINE_RE = /^CM_ BO_ *(\w+) *"(.*)/;
const BOARD_UNIT_RE = /^BU_:(.*)/;
const BOARD_UNIT_COMMENT_RE = /^CM_ BU_ *(\w+) *"(.*)";/;
const BOARD_UNIT_COMMENT_MULTI_LINE_RE = /^CM_ BU_ *(\w+) *"(.*)/;
// Follow ups are used to parse multi-line comment definitions
const FOLLOW_UP_DBC_COMMENT = 'FollowUpDbcComment';
const FOLLOW_UP_SIGNAL_COMMENT = 'FollowUpSignalComment';
const FOLLOW_UP_MSG_COMMENT = 'FollowUpMsgComment';
const FOLLOW_UP_BOARD_UNIT_COMMENT = 'FollowUpBoardUnitComment';
/* global BigInt */
function floatOrInt(numericStr) {
if (Number.isInteger(numericStr)) {
return parseInt(numericStr, 10);
}
return parseFloat(numericStr);
}
export function swapOrder(arr, wordSize, gSize) {
const swappedWords = [];
for (let i = 0; i < arr.length; i += wordSize) {
const word = arr.slice(i, i + wordSize);
for (let j = wordSize - gSize; j > -gSize; j -= gSize) {
swappedWords.push(word.slice(j, j + gSize));
}
}
return swappedWords.join('');
}
export default class DBC {
constructor(dbcString) {
this.boardUnits = [];
this.comments = [];
this.messages = new Map();
if (dbcString !== undefined) {
this.dbcText = dbcString;
this.importDbcString(dbcString);
}
}
getMessageFrame(address) {
return this.messages.get(address);
}
nextNewFrameName() {
const messageNames = [];
for (const msg of this.messages.values()) {
messageNames.push(msg.name);
}
let msgNum = 1;
let msgName;
do {
msgName = `NEW_MSG_${msgNum}`;
msgNum++;
} while (messageNames.indexOf(msgName) !== -1);
return msgName;
}
updateBoardUnits() {
const boardUnitNames = this.boardUnits.map((bu) => bu.name);
const missingBoardUnits = Array.from(this.messages.entries())
.map(([msgId, frame]) => Object.values(frame.signals))
.reduce((arr, signals) => arr.concat(signals), [])
.map((signal) => signal.receiver)
.reduce((arr, receivers) => arr.concat(receivers), [])
.filter((recv, idx, array) => array.indexOf(recv) === idx)
.filter((recv) => boardUnitNames.indexOf(recv) === -1)
.map((recv) => new BoardUnit(recv));
this.boardUnits = this.boardUnits.concat(missingBoardUnits);
}
text() {
this.updateBoardUnits();
let txt = 'VERSION ""\n\n\n';
txt += `NS_ :${this._newSymbols()}`;
txt += '\n\nBS_:\n';
const boardUnitsText = this.boardUnits.map((bu) => bu.text()).join(' ');
txt += `\nBU_: ${boardUnitsText}\n\n\n`;
const frames = [];
for (const frame of this.messages.values()) {
frames.push(frame);
}
txt += `${frames.map((f) => f.text()).join('\n\n')}\n\n`;
const messageTxs = frames
.map((f) => [f.id, f.transmitters.slice(1)])
.filter(([addr, txs]) => txs.length > 0);
txt += `${messageTxs
.map(([addr, txs]) => `BO_TX_BU_ ${addr} : ${txs.join(',')};`)
.join('\n')}\n\n\n`;
txt += this.boardUnits
.filter((bu) => bu.comment !== null)
.map((bu) => `CM_ BU_ ${bu.name} "${bu.comment}";`)
.join('\n');
txt += frames
.filter((f) => f.comment !== null)
.map((msg) => `CM_ BO_ ${msg.address} "${msg.comment}";`)
.join('\n');
const signalsByMsgId = frames
.map((f) => Object.values(f.signals).map((sig) => [f.id, sig]))
.reduce((s1, s2) => s1.concat(s2), []);
txt += `${signalsByMsgId
.filter(([msgAddr, sig]) => sig.comment !== null)
.map(
([msgAddr, sig]) => `CM_ SG_ ${msgAddr} ${sig.name} "${sig.comment}";`
)
.join('\n')}\n`;
txt += `${signalsByMsgId
.filter(([msgAddr, sig]) => sig.valueDescriptions.size > 0)
.map(([msgAddr, sig]) => sig.valueDescriptionText(msgAddr))
.join('\n')}\n`;
txt += this.comments.map((comment) => `CM_ "${comment}";`).join('\n');
return `${txt.trim()}\n`;
}
getMessageName(msgId) {
const msg = this.getMessageFrame(msgId);
if (msg && msg.frame) return msg.frame.name;
return null;
}
getSignals(msgId) {
const msg = this.getMessageFrame(msgId);
if (msg) return msg.signals;
return {};
}
createFrame(msgId, size=64) {
const msg = new Frame({
name: this.nextNewFrameName(),
id: msgId,
size: size,
});
this.messages.set(msgId, msg);
return msg;
}
setSignals(msgId, signals, frameSize) {
const msg = this.getMessageFrame(msgId); // TODO conform frameSize
if (msg) {
const newMsg = Object.assign(Object.create(msg), msg);
newMsg.signals = signals;
this.messages.set(msgId, newMsg);
} else {
const msg = this.createFrame(msgId, frameSize);
msg.signals = signals;
this.messages.set(msgId, msg);
this.updateBoardUnits();
}
}
addSignal(msgId, signal) {
const msg = this.getMessageFrame(msgId);
if (msg) {
msg.signals[signal.name] = signal;
this.updateBoardUnits();
}
}
importDbcString(dbcString) {
const warnings = [];
const messages = new Map();
let boardUnits = [];
const valueTables = new Map();
let id = 0;
let followUp = null;
const lines = dbcString.split('\n');
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (line.length === 0) continue;
if (followUp != null) {
const { type, data } = followUp;
line = line.replace(/" *;/, '');
let followUpLine = `\n${line.substr(0, line.length)}`;
if (line.indexOf('"') !== -1) {
followUp = null;
followUpLine = followUpLine.substr(0, followUpLine.length - 1);
}
if (type === FOLLOW_UP_SIGNAL_COMMENT) {
const signal = data;
signal.comment += followUpLine;
} else if (type === FOLLOW_UP_MSG_COMMENT) {
const msg = data;
msg.comment += followUpLine;
} else if (type === FOLLOW_UP_BOARD_UNIT_COMMENT) {
const boardUnit = data;
boardUnit.comment += followUpLine;
} else if (type === FOLLOW_UP_DBC_COMMENT) {
// const comment = data;
const partialComment = this.comments[this.comments.length - 1];
this.comments[this.comments.length - 1] = partialComment + followUpLine;
}
}
if (line.indexOf('BO_ ') === 0) {
const matches = line.match(MSG_RE);
if (matches === null) {
warnings.push(
`failed to parse message definition on line ${i + 1} -- ${line}`
);
continue;
}
let [idString, name, size, transmitter] = matches.slice(1);
id = parseInt(idString, 0); // 0 radix parses hex or dec
size = parseInt(size, 10);
const frame = new Frame({
name,
id,
size,
transmitters: [transmitter]
});
messages.set(id, frame);
} else if (line.indexOf('SG_') === 0) {
let matches = line.match(SIGNAL_RE);
if (matches === null) {
matches = line.match(MP_SIGNAL_RE);
if (matches === null) {
warnings.push(
`failed to parse signal definition on line ${i + 1} -- ${line}`
);
continue;
}
// for now, ignore multiplex which is matches[1]
matches = matches[1] + matches.slice(3);
} else {
matches = matches.slice(1);
}
let [
name,
startBit,
size,
isLittleEndian,
isSigned,
factor,
offset,
min,
max,
unit,
receiverStr
] = matches;
startBit = parseInt(startBit, 10);
size = parseInt(size, 10);
isLittleEndian = parseInt(isLittleEndian, 10) === 1;
isSigned = isSigned === '-';
factor = floatOrInt(factor);
offset = floatOrInt(offset);
min = floatOrInt(min);
max = floatOrInt(max);
const receiver = receiverStr.split(',').map((s) => s.trim());
const signalProperties = {
name,
startBit,
size,
isLittleEndian,
isSigned,
factor,
offset,
unit,
min,
max,
receiver
};
const signal = new Signal(signalProperties);
if (messages.get(id) !== undefined) {
messages.get(id).signals[name] = signal;
} else {
CloudLog.warn(
`importDbcString: could not add signal: ${name} due to missing message: ${id}`
);
}
} else if (line.indexOf('VAL_ ') === 0) {
const matches = line.match(VAL_RE);
if (matches !== null) {
let [messageId, signalName, vals] = matches.slice(1);
vals = vals
.split('"')
.map((s) => s.trim())
.filter((s) => s.length > 0);
messageId = parseInt(messageId, 10);
const msg = messages.get(messageId);
const signal = msg.signals[signalName];
if (signal === undefined) {
warnings.push(
`could not find signal for value description on line ${i
+ 1} -- ${line}`
);
continue;
}
for (let i = 0; i < vals.length; i += 2) {
const value = vals[i].trim();
const description = vals[i + 1].trim();
signal.valueDescriptions.set(value, description);
}
} else {
warnings.push(
`failed to parse value description on line ${i + 1} -- ${line}`
);
}
} else if (line.indexOf('VAL_TABLE_ ') === 0) {
const matches = line.match(VAL_TABLE_RE);
if (matches !== null) {
const table = new Map();
let [tableName, items] = matches.slice(1);
items = items
.split('"')
.map((s) => s.trim())
.filter((s) => s.length > 0);
for (let i = 0; i < items.length; i += 2) {
const key = items[i];
const value = items[i + 1];
table.set(key, value);
}
valueTables.set(tableName, table);
} else {
warnings.push(
`failed to parse value table on line ${i + 1} -- ${line}`
);
}
} else if (line.indexOf('BO_TX_BU_ ') === 0) {
const matches = line.match(MSG_TRANSMITTER_RE);
if (matches !== null) {
let [messageId, transmitter] = matches.slice(1);
messageId = parseInt(messageId, 10);
const msg = messages.get(messageId);
msg.transmitters.push(transmitter);
messages.set(messageId, msg);
} else {
warnings.push(
`failed to parse message transmitter definition on line ${i
+ 1} -- ${line}`
);
}
} else if (line.indexOf('CM_ SG_ ') === 0) {
let matches = line.match(SIGNAL_COMMENT_RE);
let hasFollowUp = false;
if (matches === null) {
matches = line.match(SIGNAL_COMMENT_MULTI_LINE_RE);
hasFollowUp = true;
}
if (matches === null) {
warnings.push(
`failed to parse signal comment on line ${i + 1} -- ${line}`
);
continue;
}
let [messageId, signalName, comment] = matches.slice(1);
messageId = parseInt(messageId, 10);
const msg = messages.get(messageId);
if (msg === undefined) {
warnings.push(`failed to parse signal comment on line ${i
+ 1} -- ${line}:
message id ${messageId} does not exist prior to this line`);
continue;
}
const signal = msg.signals[signalName];
if (signal === undefined) {
warnings.push(
`failed to parse signal comment on line ${i + 1} -- ${line}`
);
continue;
} else {
signal.comment = comment;
messages.set(messageId, msg);
}
if (hasFollowUp) {
followUp = { type: FOLLOW_UP_SIGNAL_COMMENT, data: signal };
}
} else if (line.indexOf('CM_ BO_ ') === 0) {
let matches = line.match(MESSAGE_COMMENT_RE);
let hasFollowUp = false;
if (matches === null) {
matches = line.match(MESSAGE_COMMENT_MULTI_LINE_RE);
hasFollowUp = true;
if (matches === null) {
warnings.push(
`failed to message comment on line ${i + 1} -- ${line}`
);
continue;
}
}
let [messageId, comment] = matches.slice(1);
messageId = parseInt(messageId, 10);
const msg = messages.get(messageId);
if (msg === undefined) {
warnings.push(
`failed to find message to add comment to, msg id: ${messageId}`
);
continue;
}
msg.comment = comment;
if (hasFollowUp) {
followUp = { type: FOLLOW_UP_MSG_COMMENT, data: msg };
}
} else if (line.indexOf('BU_: ') === 0) {
const matches = line.match(BOARD_UNIT_RE);
if (matches !== null) {
const [boardUnitNameStr] = matches.slice(1);
const newBoardUnits = boardUnitNameStr
.split(' ')
.map((s) => s.trim())
.filter((s) => s.length > 0)
.map((name) => new BoardUnit(name));
boardUnits = boardUnits.concat(newBoardUnits);
} else {
warnings.push(
`failed to parse board unit definition on line ${i + 1} -- ${line}`
);
continue;
}
} else if (line.indexOf('CM_ BU_ ') === 0) {
let matches = line.match(BOARD_UNIT_COMMENT_RE);
let hasFollowUp = false;
if (matches === null) {
matches = line.match(BOARD_UNIT_COMMENT_MULTI_LINE_RE);
hasFollowUp = true;
if (matches === null) {
warnings.push(
`failed to parse board unit comment on line ${i + 1} -- ${line}`
);
continue;
}
}
const [boardUnitName, comment] = matches.slice(1);
const boardUnit = boardUnits.find((bu) => bu.name === boardUnitName);
if (boardUnit) {
boardUnit.comment = comment;
}
if (hasFollowUp) {
followUp = { type: FOLLOW_UP_BOARD_UNIT_COMMENT, data: boardUnit };
}
} else if (line.indexOf('CM_ ') === 0) {
let matches = line.match(DBC_COMMENT_RE);
let hasFollowUp = false;
if (matches === null) {
matches = line.match(DBC_COMMENT_MULTI_LINE_RE);
if (matches === null) {
warnings.push(
`failed to parse dbc comment on line ${i + 1} -- ${line}`
);
continue;
} else {
hasFollowUp = true;
}
}
const [comment] = matches.slice(1);
this.comments.push(comment);
if (hasFollowUp) {
followUp = { type: FOLLOW_UP_DBC_COMMENT, data: comment };
}
}
}
// Disabled b/c live mode frequently calls this function
// and executes way too many network requests
if (warnings.length > 0) {
// warnings.forEach((warning) => CloudLog.warn('importDbcString: ' + warning));
// warnings.forEach((warning) => console.log('importDbcString: ' + warning));
}
this.messages = messages;
this.boardUnits = boardUnits;
this.valueTables = valueTables;
}
valueForIntSignal(signalSpec, view) {
let sig_lsb, sig_msb;
if (signalSpec.isLittleEndian) {
sig_lsb = signalSpec.startBit;
sig_msb = signalSpec.startBit + signalSpec.size - 1;
} else {
sig_lsb = DbcUtils.matrixBitNumber(DbcUtils.bigEndianBitIndex(signalSpec.startBit) + signalSpec.size - 1);
sig_msb = signalSpec.startBit;
}
let ret = signalSpec.size > 32 ? 0n : 0;
let i = Math.floor(sig_msb / 8);
let bits = signalSpec.size;
while (i >= 0 && i < view.byteLength && bits > 0) {
let lsb = Math.floor(sig_lsb / 8) === i ? sig_lsb : i*8;
let msb = Math.floor(sig_msb / 8) === i ? sig_msb : (i+1)*8 - 1;
let size = msb - lsb + 1;
let d = (view.getUint8(i) >>> (lsb - (i*8))) & ((1 << size) - 1);
if (signalSpec.size > 32) {
ret |= BigInt(d) << BigInt(bits - size);
} else {
ret |= d << (bits - size);
}
bits -= size;
i = signalSpec.isLittleEndian ? i-1 : i+1;
}
if (signalSpec.size > 32) {
ret = signalSpec.isSigned ? BigInt.asIntN(64, ret) : ret;
return ret * BigInt(signalSpec.factor) + BigInt(signalSpec.offset);
} else {
if (signalSpec.isSigned) {
ret -= ((ret >> (signalSpec.size-1)) & 1) ? (1 << signalSpec.size) : 0;
}
return ret * signalSpec.factor + signalSpec.offset;
}
}
getSignalValues(messageId, data) {
if (!this.messages.has(messageId)) {
return {};
}
const frame = this.getMessageFrame(messageId);
const view = new DataView(data.buffer);
const signalValuesByName = {};
Object.values(frame.signals).forEach((signalSpec) => {
if (isNaN(signalSpec.startBit)) {
return;
}
signalValuesByName[signalSpec.name] = this.valueForIntSignal(signalSpec, view);
});
return signalValuesByName;
}
getChffrMetricMappings() {
const metricComment = this.comments.find(
(comment) => comment.indexOf('CHFFR_METRIC') === 0
);
if (!metricComment) {
return null;
}
return metricComment
.split(';')
.map((metric) => metric.trim().split(' '))
.reduce(
(metrics, [_, messageId, signalName, metricName, factor, offset]) => {
metrics[metricName] = {
messageId: parseInt(messageId, 10),
signalName,
factor: parseFloat(factor),
offset: parseFloat(offset)
};
return metrics;
},
{}
);
}
_newSymbols() {
return `
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_`;
}
}