@canboat/canboatjs
Version:
Native javascript version of canboat
545 lines (543 loc) • 21.1 kB
JavaScript
;
/**
* Copyright 2018 Scott Bender (scott@scottbender.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.actisenseToN2KActisenseFormat = exports.actisenseToN2KAsciiFormat = exports.actisenseToiKonvert = exports.actisenseToMXPGN = exports.actisenseToPCDIN = exports.actisenseToYdgwFullRawFormat = exports.actisenseToYdgwRawFormat = void 0;
exports.toPgn = toPgn;
exports.pgnToActisenseSerialFormat = pgnToActisenseSerialFormat;
exports.pgnToActisenseN2KAsciiFormat = pgnToActisenseN2KAsciiFormat;
exports.pgnToN2KActisenseFormat = pgnToN2KActisenseFormat;
exports.toiKonvertSerialFormat = toiKonvertSerialFormat;
exports.pgnToiKonvertSerialFormat = pgnToiKonvertSerialFormat;
exports.pgnToYdgwRawFormat = pgnToYdgwRawFormat;
exports.pgnToYdgwFullRawFormat = pgnToYdgwFullRawFormat;
exports.pgnToPCDIN = pgnToPCDIN;
exports.pgnToMXPGN = pgnToMXPGN;
exports.pgnToCandump1 = pgnToCandump1;
exports.pgnToCandump2 = pgnToCandump2;
exports.pgnToCandump3 = pgnToCandump3;
const ts_pgns_1 = require("@canboat/ts-pgns");
const fromPgn_1 = require("./fromPgn");
const pgns_1 = require("./pgns");
const lodash_1 = __importDefault(require("lodash"));
const bit_buffer_1 = require("bit-buffer");
const int64_buffer_1 = require("int64-buffer");
const stringMsg_1 = require("./stringMsg");
const n2k_actisense_1 = require("./n2k-actisense");
const utilities_1 = require("./utilities");
const debug = (0, utilities_1.createDebug)('canboatjs:toPgn');
const RES_STRINGLAU = 'STRING_LAU'; //'ASCII or UNICODE string starting with length and control byte'
const RES_STRINGLZ = 'STRING_LZ'; //'ASCII string starting with length byte'
const fieldTypeWriters = {};
const fieldTypeMappers = {};
//const lengthsOff: {[key: number]: number} = { 129029: 45, 127257:8, 127258:8, 127251:8 }
const a126208_oldKey = '# of Parameters';
const a126208_newKey = 'Number of Parameters';
function toPgn(data) {
const customPgns = (0, pgns_1.getCustomPgn)(data.pgn);
let pgnList = (0, pgns_1.getPgn)(data.pgn);
if (!pgnList && !customPgns) {
debug('no pgn found: ' + data.pgn);
return;
}
if (customPgns) {
pgnList = [...customPgns.definitions, ...(pgnList || [])];
}
if (!pgnList || pgnList.length === 0) {
debug('no pgn found: ' + data.pgn);
return undefined;
}
//we would never write fallback pgns
pgnList = pgnList.filter((pgn) => pgn.Fallback === undefined || pgn.Fallback === false);
const pgn_number = data.pgn;
let pgnData = pgnList[0];
const bs = new bit_buffer_1.BitStream(Buffer.alloc(500));
if (data.fields) {
data = data.fields;
}
if (pgn_number === 126208 && !data[a126208_newKey] && data[a126208_oldKey]) {
//a bit of a hack because this field name changed and I'm sure there is code out
//there that still uses the old field name
data[a126208_newKey] = data[a126208_oldKey];
}
let fields = pgnData.Fields;
let RepeatingFields1 = pgnData.RepeatingFieldSet1Size ?? 0;
let RepeatingFields2 = pgnData.RepeatingFieldSet2Size ?? 0;
let totalRepeatingFields = RepeatingFields1 + RepeatingFields2;
let targetPgnForCondition;
for (let index = 0; index < fields.length - totalRepeatingFields; index++) {
const field = fields[index];
// Skip conditional proprietary fields when target PGN is not proprietary
if (field.Condition === 'PGNIsProprietary' &&
targetPgnForCondition !== undefined &&
!(0, utilities_1.isPGNProprietary)(targetPgnForCondition)) {
continue;
}
let value = data[field.Name] !== undefined ? data[field.Name] : data[field.Id];
// Capture the target PGN value for conditional field checks
if (field.FieldType === 'PGN' && typeof value === 'number') {
targetPgnForCondition = value;
}
if (!lodash_1.default.isUndefined(field.Match)) {
//console.log(`matching ${field.Name} ${field.Match} ${value} ${_.isString(value)}`)
if (lodash_1.default.isString(value)) {
pgnList = pgnList.filter((f) => (f.Fields[index].Description == value ||
f.Fields[index].Description === undefined) &&
f.Fallback !== true);
}
else {
pgnList = pgnList.filter((f) => (f.Fields[index].Match == value ||
f.Fields[index].Match === undefined) &&
f.Fallback !== true);
}
if (pgnList.length > 0) {
//console.log(`matched ${field.Name} ${pgnList[0].Fields[index].Match}`)
pgnData = pgnList[0];
value = pgnData.Fields[index].Match;
fields = pgnData.Fields;
RepeatingFields1 = pgnData.RepeatingFieldSet1Size ?? 0;
RepeatingFields2 = pgnData.RepeatingFieldSet2Size ?? 0;
totalRepeatingFields = RepeatingFields1 + RepeatingFields2;
}
}
writeField(bs, pgn_number, field, data, value, fields);
}
// Process RepeatingFieldSet1 from data.list
if (data.list) {
const set1Start = fields.length - totalRepeatingFields;
data.list.forEach((repeat) => {
for (let index = 0; index < RepeatingFields1; index++) {
const field = fields[set1Start + index];
const value = repeat[field.Name] !== undefined
? repeat[field.Name]
: repeat[field.Id];
writeField(bs, pgn_number, field, data, value, fields);
}
});
}
// Process RepeatingFieldSet2 from data.list2
if (data.list2 && RepeatingFields2 > 0) {
const set2Start = fields.length - RepeatingFields2;
data.list2.forEach((repeat) => {
for (let index = 0; index < RepeatingFields2; index++) {
const field = fields[set2Start + index];
const value = repeat[field.Name] !== undefined
? repeat[field.Name]
: repeat[field.Id];
writeField(bs, pgn_number, field, data, value, fields);
}
});
}
const bitsLeft = bs.byteIndex * 8 - bs.index;
if (bitsLeft > 0) {
//finish off the last byte
bs.writeBits(0xffff, bitsLeft);
//console.log(`bits left ${bitsLeft}`)
}
if (pgnData.Length !== undefined &&
pgnData.Length !== 0xff &&
fields[fields.length - 1].FieldType !== RES_STRINGLAU &&
fields[fields.length - 1].FieldType !== RES_STRINGLZ &&
!totalRepeatingFields) {
//const len = lengthsOff[pgnData.PGN] || pgnData.Length
//console.log(`Length ${len}`)
//if ( bs.byteIndex < pgnData.Length ) {
//console.log(`bytes left ${pgnData.Length-bs.byteIndex}`)
//}
for (let i = bs.byteIndex; i < pgnData.Length; i++) {
bs.writeUint8(0xff);
}
}
return bs.view.buffer.slice(0, bs.byteIndex);
}
/*
function dumpWritten(bs, field, startPos, value) {
//console.log(`${startPos} ${bs.byteIndex}`)
if ( startPos == bs.byteIndex )
startPos--
let string = `${field.Name} (${field.BitLength}): [`
for ( let i = startPos; i < bs.byteIndex; i++ ) {
string = string + bs.view.buffer[i].toString(16) + ', '
}
console.log(string + `] ${value}`)
}
*/
function writeField(bs, pgn_number, field, data, value, fields, bitLength = undefined) {
//const startPos = bs.byteIndex
if (bitLength === undefined) {
if (field.BitLengthVariable && field.FieldType === 'DYNAMIC_FIELD_VALUE') {
bitLength = lookupKeyBitLength(data, fields);
}
else {
bitLength = field.BitLength;
}
}
// console.log(`${field.Name}:${value}(${bitLength}-${field.Resolution})`)
if (value === undefined || value === null) {
if (field.FieldType && fieldTypeWriters[field.FieldType]) {
fieldTypeWriters[field.FieldType](pgn_number, field, value, bs);
}
else if (bitLength !== undefined && bitLength % 8 == 0) {
const bytes = bitLength / 8;
//const byte = field.Name.startsWith('Reserved') ? 0x00 : 0xff
for (let i = 0; i < bytes - 1; i++) {
bs.writeUint8(0xff);
}
bs.writeUint8(field.Signed ? 0x7f : 0xff);
}
else if (bitLength !== undefined) {
bs.writeBits(0xffffffff, bitLength);
}
else {
//FIXME: error! should not happen
}
}
else {
const type = field.FieldType;
if (type && fieldTypeMappers[type]) {
value = fieldTypeMappers[type](field, value);
}
else if ((field.FieldType === 'LOOKUP' ||
field.FieldType === 'DYNAMIC_FIELD_KEY') &&
lodash_1.default.isString(value)) {
value = lookup(field, value);
}
if (field.FieldType == 'NUMBER' && lodash_1.default.isString(value)) {
value = Number(value);
}
if (field.Resolution && typeof value === 'number') {
value = Number((value / field.Resolution).toFixed(0));
}
if (field.FieldType && fieldTypeWriters[field.FieldType]) {
fieldTypeWriters[field.FieldType](pgn_number, field, value, bs);
}
else {
/*
if ( _.isString(value) && typeof bitLength !== 'undefined' && bitLength !== 0 ) {
value = Number(value)
}
*/
if (field.Unit === 'kWh') {
value /= 3.6e6; // 1 kWh = 3.6 MJ.
}
else if (field.Unit === 'Ah') {
value /= 3600.0; // 1 Ah = 3600 C.
}
if (field.Offset) {
value -= field.Offset;
}
if (field.FieldType === 'VARIABLE') {
writeVariableLengthField(bs, pgn_number, data, field, value, fields);
}
else if (lodash_1.default.isBuffer(value)) {
value.copy(bs.view.buffer, bs.byteIndex);
bs.byteIndex += value.length;
}
else if (bitLength !== undefined) {
if (bitLength === 8) {
if (field.Signed) {
bs.writeInt8(value);
}
else {
bs.writeUint8(value);
}
}
else if (bitLength === 16) {
if (field.Signed) {
bs.writeInt16(value);
}
else {
bs.writeUint16(value);
}
}
else if (bitLength === 32) {
if (field.Signed) {
bs.writeInt32(value);
}
else {
bs.writeUint32(value);
}
}
else if (bitLength === 48 || bitLength == 24) {
let count = bitLength / 8;
let val = value;
if (value < 0) {
val++;
}
while (count-- > 0) {
if (value > 0) {
bs.writeUint8(val & 255);
val /= 256;
}
else {
bs.writeUint8((-val & 255) ^ 255);
val /= 256;
}
}
}
else if (bitLength === 64) {
let num;
if (field.Signed) {
num = new int64_buffer_1.Int64LE(value);
}
else {
num = new int64_buffer_1.Uint64LE(value);
}
const buf = num.toBuffer();
buf.copy(bs.view.buffer, bs.byteIndex);
bs.byteIndex += buf.length;
}
else {
bs.writeBits(value, bitLength);
}
}
}
}
//dumpWritten(bs, field, startPos, value)
}
function writeVariableLengthField(bs, pgn_number, pgn, field, value, fields) {
const refField = (0, fromPgn_1.getField)(pgn.pgn | pgn.PGN, bs.view.buffer[bs.byteIndex - 1] - 1, pgn);
if (refField) {
let bits;
if (refField.BitLength !== undefined) {
bits = (refField.BitLength + 7) & ~7; // Round # of bits in field refField up to complete bytes: 1->8, 7->8, 8->8 etc.
}
return writeField(bs, pgn_number, refField, pgn, value, fields, bits);
}
}
function lookup(field, stringValue) {
let res;
if (field.LookupEnumeration) {
res = (0, ts_pgns_1.getEnumerationValue)(field.LookupEnumeration, stringValue);
}
else {
res = (0, ts_pgns_1.getFieldTypeEnumerationValue)(field.LookupFieldTypeEnumeration, stringValue);
}
return lodash_1.default.isUndefined(res) ? stringValue : res;
}
function lookupKeyBitLength(data, fields) {
const field = fields.find((field) => field.Name === 'Key');
if (field) {
let val = data['Key'] || data['key'];
if (typeof val === 'string') {
val = (0, ts_pgns_1.getFieldTypeEnumerationValue)(field.LookupFieldTypeEnumeration, val);
}
return (0, ts_pgns_1.getFieldTypeEnumerationBits)(field.LookupFieldTypeEnumeration, val);
}
}
/*
function parseHex(s:string): number {
return parseInt(s, 16)
};
function canboat2Buffer(canboatData:string) {
return Buffer.alloc(canboatData
.split(',')
.slice(6)
.map(parseHex), 'hex')
}
*/
function pgnToActisenseSerialFormat(pgn) {
return (0, stringMsg_1.encodeActisense)({
pgn: pgn.pgn,
data: toPgn(pgn),
dst: pgn.dst,
src: pgn.src,
prio: pgn.prio,
timestamp: undefined
});
}
function pgnToActisenseN2KAsciiFormat(pgn) {
return (0, stringMsg_1.encodeActisenseN2KACSII)({
pgn: pgn.pgn,
data: toPgn(pgn),
dst: pgn.dst,
src: pgn.src,
prio: pgn.prio,
timestamp: undefined
});
}
function pgnToN2KActisenseFormat(pgn) {
const data = toPgn(pgn);
if (data) {
return (0, n2k_actisense_1.encodeN2KActisense)(pgn, data);
}
}
function toiKonvertSerialFormat(pgn, data, dst = 255) {
return `!PDGY,${pgn},${dst},${data.toString('base64')}`;
}
function pgnToiKonvertSerialFormat(pgn) {
const data = toPgn(pgn);
if (data) {
return toiKonvertSerialFormat(pgn.pgn, data, pgn.dst);
}
}
function pgnToYdgwRawFormat(info) {
return (0, stringMsg_1.encodeYDRAW)({ ...info, data: toPgn(info) });
}
function pgnToYdgwFullRawFormat(info) {
return (0, stringMsg_1.encodeYDRAWFull)({ ...info, data: toPgn(info) });
}
function pgnToPCDIN(info) {
return (0, stringMsg_1.encodePCDIN)({ ...info, data: toPgn(info) });
}
function pgnToMXPGN(info) {
return (0, stringMsg_1.encodeMXPGN)({ ...info, data: toPgn(info) });
}
function pgnToCandump1(info) {
return (0, stringMsg_1.encodeCandump1)({ ...info, data: toPgn(info) });
}
function pgnToCandump2(info) {
return (0, stringMsg_1.encodeCandump2)({ ...info, data: toPgn(info) });
}
function pgnToCandump3(info) {
return (0, stringMsg_1.encodeCandump3)({ ...info, data: toPgn(info) });
}
exports.actisenseToYdgwRawFormat = lodash_1.default.flow(stringMsg_1.parseActisense, stringMsg_1.encodeYDRAW);
exports.actisenseToYdgwFullRawFormat = lodash_1.default.flow(stringMsg_1.parseActisense, stringMsg_1.encodeYDRAWFull);
exports.actisenseToPCDIN = lodash_1.default.flow(stringMsg_1.parseActisense, stringMsg_1.encodePCDIN);
exports.actisenseToMXPGN = lodash_1.default.flow(stringMsg_1.parseActisense, stringMsg_1.encodeMXPGN);
exports.actisenseToiKonvert = lodash_1.default.flow(stringMsg_1.parseActisense, stringMsg_1.encodePDGY);
exports.actisenseToN2KAsciiFormat = lodash_1.default.flow(stringMsg_1.parseActisense, stringMsg_1.encodeActisenseN2KACSII);
exports.actisenseToN2KActisenseFormat = lodash_1.default.flow(stringMsg_1.parseActisense, n2k_actisense_1.encodeN2KActisense);
function bitIsSet(field, index, value) {
const enumName = (0, ts_pgns_1.getBitEnumerationName)(field.LookupBitEnumeration, index);
return enumName ? value.indexOf(enumName) != -1 : false;
}
fieldTypeWriters['BITLOOKUP'] = (pgn, field, value, bs) => {
if (field.BitLength !== undefined) {
if (value === undefined || value.length === 0) {
if (field.BitLength % 8 == 0) {
const bytes = field.BitLength / 8;
//const lastByte = field.Signed ? 0x7f : 0xff
for (let i = 0; i < bytes - 1; i++) {
bs.writeUint8(0x0);
}
bs.writeUint8(0x0);
}
else {
bs.writeBits(0xffffffff, field.BitLength);
}
}
else {
for (let i = 0; i < field.BitLength; i++) {
bs.writeBits(bitIsSet(field, i, value) ? 1 : 0, 1);
}
}
}
};
fieldTypeWriters['STRING_FIX'] = (pgn, field, value, bs) => {
if (field.BitLength !== undefined) {
let fill = 0xff;
if ((pgn === 129810 &&
(field.Name === 'Vendor ID' || field.Name === 'Callsign')) ||
(pgn === 129809 && field.Name === 'Name')) {
if (lodash_1.default.isUndefined(value) || value.length == 0) {
{
fill = 0x40;
value = '';
}
}
}
if (value === undefined) {
value = '';
}
const fieldLen = field.BitLength / 8;
for (let i = 0; i < value.length; i++) {
bs.writeUint8(value.charCodeAt(i));
}
for (let i = 0; i < fieldLen - value.length; i++) {
bs.writeUint8(fill);
}
}
};
fieldTypeWriters[RES_STRINGLZ] = (pgn, field, value, bs) => {
if (lodash_1.default.isUndefined(value)) {
value = '';
}
bs.writeUint8(value.length);
for (let i = 0; i < value.length; i++) {
bs.writeUint8(value.charCodeAt(i));
}
bs.writeUint8(0);
};
fieldTypeWriters['String with start/stop byte'] = (pgn, field, value, bs) => {
if (lodash_1.default.isUndefined(value)) {
value = '';
}
bs.writeUint8(0x02);
for (let i = 0; i < value.length; i++) {
bs.writeUint8(value.charCodeAt(i));
}
bs.writeUint8(0x01);
};
fieldTypeWriters[RES_STRINGLAU] = (pgn, field, value, bs) => {
if (pgn === 129041 && field.Name === 'AtoN Name') {
if (value.length > 18) {
value = value.substring(0, 18);
}
else {
value = value.padEnd(18, ' ');
}
}
bs.writeUint8(value ? value.length + 2 : 2);
bs.writeUint8(1);
if (value) {
for (let idx = 0; idx < value.length; idx++) {
bs.writeUint8(value.charCodeAt(idx));
}
}
};
fieldTypeMappers['DATE'] = (field, value) => {
if (lodash_1.default.isString(value)) {
const parts = value.split('.');
const date = new Date(Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])));
return date.getTime() / 86400 / 1000;
}
return value;
};
fieldTypeMappers['TIME'] = (field, value) => {
if (lodash_1.default.isString(value)) {
const split = value.split(':');
const hours = Number(split[0]);
const minutes = Number(split[1]);
const seconds = Number(split[2]);
value = hours * 60 * 60 + minutes * 60 + seconds;
}
return value;
};
fieldTypeMappers['DURATION'] = fieldTypeMappers['TIME'];
fieldTypeMappers['Pressure'] = (field, value) => {
if (field.Unit) {
switch (field.Unit[0]) {
case 'h':
case 'H':
value /= 100;
break;
case 'k':
case 'K':
value /= 1000;
break;
case 'd':
value *= 10;
break;
}
}
return value;
};
//# sourceMappingURL=toPgn.js.map