@canboat/canboatjs
Version:
Native javascript version of canboat
340 lines (303 loc) • 11.4 kB
JavaScript
const {
compact, cond, constant, curry, flow, isEmpty,
isString, negate, overSome, startsWith, stubTrue, toNumber, zipObject
} = require('lodash/fp')
const {
arrBuff, byteString, getPlainPGNs, rmChecksum, trimWrap, compute0183Checksum, hexByte
} = require('./utilities')
const {
buildCanId, encodeCanIdString, parseCanId, parseCanIdStr,
} = require('./canId')
const moment = require('moment')
/**
* Helper function that helps merge canId fields with format, data, and others.
* The idea here is to reflect what was in the source and not remove or add.
* If the source has a len or timestamp attribute it should be added but not created.
* @param {Object} canIdInfo The result of parseCanId, parseCanIdStr, or buildCanId.
* @param {string} format String that defines the source format.
* @param {Buffer} data Buffer array that contains the fields data.
* @param {Object} [rest={}] Anything else to be added like len, timestamp, direction.
* @return {Object} All canId fields with format and data props added.
*/
function buildMsg(_canIdInfo, format, data, rest = {}) {
const canIdInfo = Object.assign({}, _canIdInfo, {
format,
data
})
for (const property in rest) {
if (canIdInfo[property] === undefined) {
canIdInfo[property] = rest[property]
}
}
return canIdInfo
}
function buildErrMsg(msg, input) {
if (input && isString(input)) return `${msg} - ${input}`
return msg
}
const buildErr = curry((format, msg, input) => ({
error: new Error(buildErrMsg(msg, input)), format, input,
}))
function toPaddedHexString(num, len) {
str = num.toString(16).toUpperCase();
return "0".repeat(len - str.length) + str;
}
// 2016-02-28T19:57:02.364Z,2,127250,7,255,8,ff,10,3b,ff,7f,ce,f5,fc
exports.isActisense = input => (input.charAt(10) === 'T' && input.charAt(23) === 'Z') || (input.charAt(10) === '-' && input.charAt(23) === ',')
exports.parseActisense = (input) => {
const [ timestamp, prio, pgn, src, dst, len, ...data ] = input.split(',')
return buildMsg(
buildCanId(prio, pgn, dst, src),
'Actisense',
Buffer.from(data.join(''), 'hex'),
{ len: Number(len), timestamp },
)
}
exports.encodeActisense = ({
pgn, data, timestamp, prio = 2, dst = 255, src = 0 }) => ([
timestamp || new Date().toISOString(),
prio, pgn, src, dst, data.length,
byteString(data)
].join(','))
exports.toActisenseSerialFormat = (pgn, data, dst=255, src=0, prio=2) => exports.encodeActisense({
pgn, data, dst, src, prio
})
// A764027.880 05FF7 1EF00 E59861060202FFFFFFFF03030000FFFFFFFFFFFFFFFFFFFF0000FFFFFF7FFFFFFF7FFFFFFF7F0000FF7F
exports.isActisenseN2KASCII = input => input.charAt(0) === 'A' && input.charAt(7) === '.' && input.charAt(11) === ' '
exports.parseActisenseN2KASCII = (input) => {
const [ timestamp, srcdstp, pgn, data ] = input.split(' ')
const src = parseInt(srcdstp.substring(0, 2), 16)
const dst = parseInt(srcdstp.substring(2,4), 16)
const prio = parseInt(srcdstp.substring(4))
return buildMsg(
buildCanId(prio, parseInt(pgn, 16), dst, src),
'Actisense N2K ASCII',
Buffer.from(data, 'hex'),
{ len: data.length, time: timestamp.substring(1) },
)
}
exports.encodeActisenseN2KACSII = ({
pgn, data, timestamp, prio = 2, dst = 255, src = 0 }) => {
timestamp = 'A000000.000'
const srcdstp = hexByte(src) + hexByte(dst) + prio
return ([
timestamp,
srcdstp.toUpperCase(),
toPaddedHexString(pgn, 5).toUpperCase(),
byteString(data, '').toUpperCase()
].join(' '))
}
// 16:29:27.082 R 09F8017F 50 C3 B8 13 47 D8 2B C6
exports.isYDRAW = (input) => {
if (input.charAt(2) !== ':') return false
const direction = input.substr(12, 3)
return direction === ' R ' || direction === ' T '
}
exports.parseYDRAW = (input) => {
const parts = input.split(' ')
if ( parts.length < 4 ) return buildErr('YDRAW', 'Invalid parts.', input)
const [ time, direction, canId, ...data ] = parts // time format HH:mm:ss.SSS
return buildMsg(
parseCanIdStr(canId), 'YDRAW', arrBuff(data),
{ direction, time }
)
}
//19F51323 01 02<CR><LF>
exports.encodeYDRAW = ({ data, ...canIdInfo }) => {
const canId = encodeCanIdString(canIdInfo)
const pgns = data.length > 8 || canIdInfo.pgn == 126720 ? getPlainPGNs(data) : [ data ]
return pgns.map(buffer => canId + ' ' + byteString(buffer, ' '))
}
//16:29:27.082 R 19F51323 01 02<CR><LF>
exports.encodeYDRAWFull = ({ data, ...canIdInfo }) => {
const canId = encodeCanIdString(canIdInfo)
const pgns = data.length > 8 || canIdInfo.pgn == 126720 ? getPlainPGNs(data) : [ data ]
return pgns.map(buffer =>
moment().utc().format('hh:mm:ss.SSS')
+ ' R '
+ canId + ' '
+ byteString(buffer, ' '))
}
const get0183Sentence = (msg) => {
let sentence = msg
if (sentence.charAt(0) === '\\') {
split = sentence.split('\\')
if ( split.length < 3 ) {
return false
}
sentence = split[2]
}
return sentence
}
// $PCDIN,01F119,00000000,0F,2AAF00D1067414FF*59
exports.isPCDIN = (msg) => {
const sentence = get0183Sentence(msg)
return sentence.startsWith('$PCDIN,')
}
exports.parsePCDIN = (input) => {
const sentence = get0183Sentence(input)
const [ prefix, pgn, timeHex, src, data ] = sentence.split(',')
let timer = parseInt(timeHex, 32)
timer = timer / 1024
timer = timer + 1262304000 // starts epoch time from 1/1/2010
timer = timer * 1000
return buildMsg(
buildCanId(0, parseInt(pgn, 16), 255, parseInt(src, 16)),
'PCDIN',
Buffer.from(rmChecksum(data), 'hex'),
{ coalesced: true, prefix, timer, timestamp: new Date(timer) },
)
}
exports.encodePCDIN = ({ prefix = '$PCDIN', pgn, data, dst = 255}) => {
const sentence = [ prefix, toPaddedHexString(pgn, 6), '0000180C', hexByte(dst).toUpperCase(), byteString(data, '').toUpperCase()].join(',')
return sentence + compute0183Checksum(sentence)
}
const changeEndianness = (string) => {
const result = [];
let len = string.length - 2;
while (len >= 0) {
result.push(string.substr(len, 2));
len -= 2;
}
return result.join('');
}
// $MXPGN,01F801,2801,C1308AC40C5DE343*19
exports.isMXPGN = (msg) => {
const sentence = get0183Sentence(msg)
return sentence.startsWith('$MXPGN,')
}
exports.parseMXPGN = (input, options) => {
const sentence = get0183Sentence(input)
const [ prefix, pgn, attr_word, data ] = sentence.split(',')
const send_prio_len = (parseInt(attr_word.substr(0,2), 16).toString(2)).padStart(8, '0');
const addr = (parseInt(attr_word.substr(2,2), 16));
const send = parseInt(send_prio_len.substr(0,1), 2);
const prio = parseInt(send_prio_len.substr(1,3), 2);
const len = parseInt(send_prio_len.substr(4,4), 2);
let src, dst;
send ? dst = addr: src = addr;
let reversed
if ( options && options.littleEndianMXPGN )
reversed = changeEndianness(rmChecksum(data))
else
reversed = data
return buildMsg(
buildCanId(0, parseInt(pgn, 16), 255, parseInt(src, 16)),
'MXPGN',
Buffer.from(reversed, 'hex'),
{ coalesced: true, prefix },
)
}
exports.encodeMXPGN = ({ prefix = '$MXPGN', pgn, prio, src, data }) => {
if (src > 255) src = 255;
if (!prio) prio = 3;
if (!src) src = 255;
const dataLength = hexByte(128 + (prio * 16) + (byteString(data, '').toUpperCase().length / 2)).toUpperCase()
const attribWord = dataLength + hexByte(src).toUpperCase()
var buff = Buffer.from(byteString(data, ''), 'Hex');
for (var i = 0, j = buff.length - 1; i < j; ++i, --j) {
var t = buff[j]
buff[j] = buff[i]
buff[i] = t
}
const sentence = [prefix, toPaddedHexString(pgn, 6), attribWord, buff.toString('Hex').toUpperCase()].join(',')
return sentence + compute0183Checksum(sentence)
}
// iKonvert
// !PDGY,126992,3,2,255,0.563,d2009e45b3b8821d
exports.isPDGY = startsWith('!PDGY,')
exports.parsePDGY = (input) => {
const parts = input.split(',')
if ( parts.length === 7 ) {
const [ prefix, pgn, prio, src, dst, timer, data ] = parts
return buildMsg(
buildCanId(prio, pgn, dst, src), 'PDGY', Buffer.from(data, 'base64'),
{ timer: Number(timer), prefix, coalesced: true },
)
} else if ( parts.length === 4 ) {
const [ prefix, pgn, dst, data ] = parts
return buildMsg(
buildCanId(0, pgn, dst, 0), 'PDGY', Buffer.from(data, 'base64'),
{ coalesced: true }
)
} else {
return buildErr('iKonvert', 'Invalid parts.', input)
}
}
exports.encodePDGY = ({ prefix = '!PDGY', pgn, data, dst = 255}) => (
[ prefix, pgn, dst, data.toString('base64')].join(',')
)
exports.isPDGYdebug = startsWith('$PDGY,')
exports.parsePDGYdebug = (input) => {
const [ prefix, pgn, ...fieldParts ] = input.split(',')
const fieldVals = fieldParts.map(toNumber)
const fields = zipObject([
'busLoad', 'errors', 'deviceCount', 'timer', 'gatewaySrc', 'rejectedTX',
], fieldVals)
const src = fields.gatewaySrc
return buildMsg(
buildCanId(3, pgn, src, src), 'PDGYdebug', Buffer.from(fieldVals),
{ fields, prefix },
)
}
// candump1 Angstrom
// <0x18eeff01> [8] 05 a0 be 1c 00 a0 a0 c0
exports.isCandump1 = startsWith('<0x')
exports.parseCandump1 = (input) => {
const [ canId, len, ...data ] = input.split(' ')
return buildMsg(
parseCanIdStr(trimWrap(canId)), 'candump1', arrBuff(data),
{ len: Number(trimWrap(len)) },
)
}
// candump2 Debian
// can0 09F8027F [8] 00 FC FF FF 00 00 FF FF
exports.isCandump2 = startsWith('can')
exports.parseCandump2 = (input) => {
const [ bus, canId, len, ...data ] = compact(input.split(' '))
return buildMsg(
parseCanIdStr(canId), 'candump2', arrBuff(data),
{ bus, len: Number(trimWrap(len)) },
)
}
// candump3 log
// (1502979132.106111) slcan0 09F50374#000A00FFFF00FFFF
exports.isCandump3 = startsWith('(')
exports.parseCandump3 = (input) => {
const [ timestamp, bus, canFrame ] = input.split(' ')
const [ canId, data ] = canFrame.split('#')
return buildMsg(
parseCanIdStr(canId), 'candump3', Buffer.from(data, 'hex'), { timestamp, bus }
)
}
const hasErr = overSome([negate(isString), isEmpty])
exports.parseN2kString = cond([
[hasErr, buildErr('INVALID', 'Input not string or empty.')],
[exports.isActisense, exports.parseActisense],
[exports.isYDRAW, exports.parseYDRAW],
[exports.isPCDIN, exports.parsePCDIN],
[exports.isMXPGN, exports.parseMXPGN],
[exports.isPDGY, exports.parsePDGY],
[exports.isCandump1, exports.parseCandump1],
[exports.isCandump2, exports.parseCandump2],
[exports.isCandump3, exports.parseCandump3],
[exports.isPDGYdebug, exports.parsePDGYdebug],
[exports.isActisenseN2KASCII, exports.parseActisenseN2KASCII],
[stubTrue, buildErr('MISSING_PARSER', 'Parser not found for input.')],
])
exports.isN2KString = cond([
[hasErr, () => false],
[exports.isActisense, () => true],
[exports.isYDRAW, () => true],
[exports.isPCDIN, () => true],
[exports.isMXPGN, () => true],
[exports.isPDGY, () => true],
[exports.isCandump1, () => true],
[exports.isCandump2, () => true],
[exports.isCandump3, () => true],
[exports.isPDGYdebug, () => true],
[exports.isActisenseN2KASCII, () => true],
[stubTrue, () => false],
])
exports.isN2KOver0183 = (msg) => { return exports.isPCDIN(msg) || exports.isMXPGN(msg) }
exports.parseN2KOver0183 = (msg) => { return exports.parsePCDIN(msg) || exports.parseMXPGN(msg) }