UNPKG

@canboat/canboatjs

Version:

Native javascript version of canboat

545 lines (543 loc) 21.1 kB
"use strict"; /** * 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