nimcodec
Version:
Encoder/decoder for satellite IoT using Non-IP Messages
625 lines (565 loc) • 19 kB
JavaScript
const chai = require('chai');
chai.config.includeStack = false;
const expect = chai.expect;
// const should = chai.should();
// const rewire = require('rewire');
const bitwise = require('bitwise');
const { nimo } = require('../lib');
const { decodeMessage, encodeMessage, importCodec } = nimo;
const { decodeField, encodeField, encodeFieldLength } = require('../lib/codecs/nimo/field/common');
const idpmsgPath = './test/codecs/idpmodem.idpmsg';
const modemCodecPath = './test/codecs/idpmodem.json';
describe('#nimo/importCodec()', function() {
const serviceKeys = ['serviceKey', 'name'];
const messageKeys = ['messageKey', 'name', 'fields'];
const fieldKeys = ['name', 'type'];
it('should import idpmsg (xml)', function() {
const converted = importCodec(idpmsgPath);
expect(converted).to.be.an('Object').with.key('services');
for (const s of converted.services) {
s.should.include.all.keys(serviceKeys);
if (s.mobileOriginatedMessages) {
for (const m of s.mobileOriginatedMessages) {
m.should.include.all.keys(messageKeys);
for (const f of m.fields) {
f.should.include.all.keys(fieldKeys);
}
}
}
}
});
it('should import json', function() {
const imported = importCodec(modemCodecPath);
expect(imported).to.be.an('Object').with.key('services');
for (const s of imported.services) {
s.should.include.all.keys(serviceKeys);
if (s.mobileOriginatedMessages) {
for (const m of s.mobileOriginatedMessages) {
m.should.include.all.keys(messageKeys);
for (const f of m.fields) {
f.should.include.all.keys(fieldKeys);
}
}
}
}
});
});
describe('#nimo/encodeFieldLength()', function() {
// const encodeFieldLength = field.__get__('encodeFieldLength');
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode 8 bits', function() {
const size = 4;
buffer = Buffer.from([0]);
offset = 0;
({buffer, offset} = encodeFieldLength(size, buffer, offset));
expect(buffer.length).to.equal(1);
expect(bitwise.buffer.readUInt(buffer, 1, 7)).to.equal(size);
});
it('should encode 16 bits with high bit set', function() {
const size = 128;
buffer = Buffer.from([0]);
offset = 0;
({buffer, offset} = encodeFieldLength(size, buffer, offset));
expect(buffer.length).to.equal(2);
expect(bitwise.buffer.readUInt(buffer, 1, 15)).to.equal(size);
expect(bitwise.buffer.read(buffer, 0, 1)[0]).to.equal(1);
});
});
describe('#nimo/Boolean Field', function() {
const testField = {
name: "boolTest",
type: "bool",
// optional: false,
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode a boolean field', function() {
const testValues = [true, false];
const startBufLen = buffer.length;
for (const testVal of testValues) {
let startOff = offset;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(bitwise.buffer.readUInt(buffer, startOff, 1)).to.equal(+ testVal);
expect(offset).to.equal(startOff + 1);
}
});
it('should decode a boolean field', function() {
buffer = Buffer.from([128]);
offset = 0;
let startOff = offset;
let decoded;
({decoded, offset} = decodeField(testField, buffer, offset));
expect(decoded).to.be.an('Object');
expect(decoded.value).to.equal(true);
expect(offset).to.equal(startOff + 1);
});
});
describe('#nimo/Enum Field', function() {
const testField = {
name: "testEnum",
type: "enumField",
size: 2,
items: ['ONE', 'TWO', 'THREE'],
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode an enumField', function () {
const testVal = 'TWO';
const startBufLen = buffer.length;
const startOff = offset;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(bitwise.buffer.readUInt(buffer, startOff, testField.size))
.to.equal(testField.items.indexOf(testVal));
expect(offset).to.equal(startOff + testField.size);
});
it('should decode an enumField', function () {
buffer = Buffer.from([64]);
offset = 0;
let startOff = offset;
let decoded;
({decoded, offset} = decodeField(testField, buffer, offset));
expect(decoded).to.be.an('Object');
expect(decoded.value).to.equal(testField.items[1]);
expect(offset).to.equal(startOff + testField.size);
});
});
describe('#nimo/Int Field', function () {
const testField = {
name: "testInt",
type: "intField",
size: 4,
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode negative numbers', function () {
let startOff;
const testVals = [-1, -5];
for (const testVal of testVals) {
startOff = offset;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(bitwise.buffer.readInt(buffer, startOff, testField.size)).to.equal(testVal);
}
});
it('should error if the number is too big for size', function() {
let startOff;
const testVals = [-(2**testField.size / 2)-1, 2**testField.size];
for (const testVal of testVals) {
startOff = offset;
expect(function() { encodeField(testField, testVal, buffer, offset) })
.to.throw('Invalid signed integer value for size');
}
});
it('should decode a negative number', function() {
const testVal = -5;
buffer = Buffer.from([testVal << (8 - testField.size)]);
offset = 0;
({decoded,offset} = decodeField(testField, buffer, offset));
expect(decoded.value).is.equal(testVal);
});
it('should decode a 32-bit signed', () => {
testField.size = 32;
const testVals = ['002e7001', 'fffe23ea'];
const expected = [3043329,-121878];
for (const [i, testVal] of testVals.entries()) {
let buffer = Buffer.from(testVal, 'hex');
let {decoded} = decodeField(testField, buffer, 0);
expect(decoded.value).is.equal(expected[i]);
}
});
it('should encode 32-bit signed integers', () => {
testField.size = 32;
const testVals = [3043329,-121878];
const expected = ['002e7001', 'fffe23ea'];
for (const [i, testVal] of testVals.entries()) {
let buffer = Buffer.from([0]);
({buffer} = encodeField(testField, testVal, buffer, 0));
expect(buffer.toString('hex')).to.equal(expected[i]);
expect(bitwise.buffer.readInt(buffer, 0, testField.size)).to.equal(testVal);
}
});
});
describe('#nimo/Uint Field', function () {
const testField = {
name: "testUint",
type: "uintField",
size: 4,
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode positive numbers', function () {
let startOff;
const testVals = [1, 2**testField.size - 1];
for (const testVal of testVals) {
startOff = offset;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(bitwise.buffer.readUInt(buffer, startOff, testField.size))
.to.equal(testVal);
}
});
it('should error if the number is negative or too big for size', function() {
let startOff;
const testVals = [-1, 2**testField.size];
for (const testVal of testVals) {
startOff = offset;
expect(function() { encodeField(testField, testVal, buffer, offset) })
.to.throw('Invalid unsigned integer value for size');
}
});
it('should decode an unsigned integer', function() {
const testVal = 1;
buffer = Buffer.from([testVal << (8 - testField.size)]);
offset = 0;
({decoded,offset} = decodeField(testField, buffer, offset));
expect(decoded.value).to.equal(testVal);
});
});
describe('#nimo/String Field', function() {
const testField = {
name: "variableString",
type: "stringField",
size: 32,
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode a basic string', function() {
buffer = Buffer.from([0]);
offset = 0;
const startOff = offset;
const testVal = 'Test string';
const lBytes = testVal.length < 128 ? 1 : 2;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(buffer.length).to.equal(lBytes + testVal.length);
expect(buffer.toString('utf8', lBytes)).to.equal(testVal);
expect(offset).to.equal(startOff + (lBytes + testVal.length) * 8);
});
it('should pad a fixed length string', function() {
buffer = Buffer.from([0]);
offset = 0;
testField.fixed = true;
const startOff = offset;
const testVal = 'Test string';
const lBytes = testField.size < 128 ? 1 : 2;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(buffer.length).to.equal(lBytes + testField.size);
expect(buffer.toString('utf8', lBytes).slice(0, testVal.length)).to.equal(testVal);
});
it('should append a string a non-byte boundary', function() {
buffer = Buffer.from([0]);
offset = 0;
const startOff = 3;
const testVal = 'Test string';
const lBytes = testVal.length < 128 ? 1 : 2;
({buffer, offset} = encodeField(testField, testVal, buffer, startOff));
expect(buffer.length).to.equal(lBytes + testVal.length + 1);
const eBits = bitwise.buffer.read(buffer, startOff + lBytes * 8, testVal.length * 8);
expect(bitwise.buffer.create(eBits).toString()).to.equal(testVal);
});
it('should decode a string field', function() {
const testStr = 'Test';
const te = new TextEncoder();
const enc = te.encode(testStr);
buffer = Buffer.concat([Buffer.from([enc.length]),Buffer.from(enc)]);
offset = 0;
({decoded, offset} = decodeField(testField, buffer, offset));
expect(decoded.value).to.equal(testStr);
});
});
describe('#nimo/Data Field', function() {
const testField = {
name: "variableData",
type: "dataField",
size: 32,
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode basic blob', function() {
const startOff = offset;
const testVal = Buffer.from([1, 2, 3, 4]);
const lBytes = testVal.length < 128 ? 1 : 2;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(buffer.length).to.equal(lBytes + testVal.length);
expect(Buffer.compare(buffer.slice(lBytes, lBytes + testVal.length), testVal)).to.equal(0);
expect(offset).to.equal(startOff + (lBytes + testVal.length) * 8);
});
it('should pad a fixed length blob', function() {
buffer = Buffer.from([0]);
offset = 0;
testField.fixed = true;
const startOff = offset;
const testVal = Buffer.from([1, 2, 3, 4]);
const lBytes = testField.size < 128 ? 1 : 2;
({buffer, offset} = encodeField(testField, testVal, buffer, offset));
expect(buffer.length).to.equal(lBytes + testField.size);
expect(Buffer.compare(buffer.slice(lBytes, lBytes + testVal.length), testVal)).to.equal(0);
});
it('should append data on a non-byte boundary', function() {
buffer = Buffer.from([0]);
offset = 0;
const startOff = 3;
const testVal = Buffer.from([1, 2, 3, 4]);
const lBytes = testVal.length < 128 ? 1 : 2;
({buffer, offset} = encodeField(testField, testVal, buffer, startOff));
expect(buffer.length).to.equal(lBytes + testVal.length + 1);
const eBits = bitwise.buffer.read(buffer, startOff + lBytes * 8, testVal.length * 8);
expect(Buffer.compare(bitwise.buffer.create(eBits), testVal)).to.equal(0);
});
it('should decode a data field', function() {
const testVal = Buffer.from([1,2,3,4]);
buffer = Buffer.concat([Buffer.from([testVal.length]), testVal]);
offset = 0;
({decoded,offset} = decodeField(testField, buffer, offset));
expect(Buffer.compare(decoded.value, testVal)).to.equal(0);
});
});
describe('#nimo/Array Field', function() {
const testCase1d = {
fieldDef: {
name: 'array1d',
type: 'array',
size: 32,
fields: [
{
name: 'temperature',
type: 'int',
size: 9,
}
],
},
encoded: [2, 253, 131, 64],
decoded: {
name: 'array1d',
value: [{temperature: -5}, {temperature: 13},],
},
};
const testCase2d = {
fieldDef: {
name: 'array2d',
type: 'array',
size: 128,
fields: [
{
name: 'parameterName',
type: 'string',
size: 16
},
{
name: 'parameterValue',
type: 'uint',
size: 32
}
],
},
encoded: [1, 9, 116, 101, 115, 116, 80, 97, 114, 97, 109, 0, 0, 0, 1],
decoded: {
name: 'testArray',
type: 'array',
value: [{parameterName: 'testParam', parameterValue: 1},],
},
};
const testCases = [testCase1d, testCase2d,];
const testField = {
name: 'testArray',
type: 'array',
size: 128,
fields: [
{
name: 'parameterName',
type: 'string',
size: 16
},
{
name: 'parameterValue',
type: 'uint',
size: 32
}
],
};
let buffer = Buffer.from([0]);
let offset = 0;
it('should encode an array', function() {
for (const tc of testCases) {
const { fieldDef, encoded, decoded } = tc;
buffer = Buffer.from([0]);
offset = 0;
({buffer, offset} = encodeField(fieldDef, decoded.value, buffer, offset));
expect(Buffer.compare(buffer, Buffer.from(encoded))).to.equal(0);
}
});
it('should decode an array', function() {
for (const tc of testCases) {
const { fieldDef, encoded, decoded: expected } = tc;
buffer = Buffer.from(encoded);
offset = 0;
let decoded;
({decoded, offset} = decodeField(fieldDef, buffer, offset));
decoded.value.forEach((value, i) => {
for (const [k, v] of Object.entries(value)) {
expect(v).to.equal(expected.value[i][k]);
}
});
}
});
});
describe('#nimo/Bitkeylist Field', function() {
const testCase1 = {
fieldDef: {
name: 'testBitkey',
type: 'bitkeylist',
size: 8,
items: ['bit0', 'bit1', 'bit2'],
fields: [
{
name: 'kpi1',
type: 'uint',
size: 4
},
{
name: 'kpi2',
type: 'uint',
size: 4
}
],
},
encoded: [5, 18, 2],
decoded: {
name: 'testBitKey',
type: 'bitkeylist',
value: [
{ "bit0": {"kpi1": 1, "kpi2": 2}},
{ "bit2": {"kpi1": 0, "kpi2": 2}},
],
},
};
const testCases = [testCase1,];
it('should encode a bitkeylist', function() {
for (const tc of testCases) {
const { fieldDef, encoded, decoded } = tc;
let buffer = Buffer.from([0]);
let offset = 0;
({buffer,offset} = encodeField(fieldDef, decoded.value, buffer, offset));
expect(Buffer.compare(buffer, Buffer.from(encoded))).to.equal(0);
}
});
it('should decode a bitkeylist', function() {
for (const tc of testCases) {
const { fieldDef, encoded, decoded: expected } = tc;
let buffer = Buffer.from(encoded);
let offset = 0;
let decoded;
({decoded, offset} = decodeField(fieldDef, buffer, offset));
expect(decoded.value).to.be.an('Array');
decoded.value.forEach((row, i) => {
const expBitkey = Object.keys(expected.value[i])[0];
const expEntry = Object.values(expected.value[i])[0];
const actBitkey = Object.keys(row)[0];
const actEntry = Object.values(row)[0];
expect(actBitkey).to.equal(expBitkey);
for (const [k, v] of Object.entries(actEntry)) {
expect(v).to.equal(expEntry[k]);
}
});
}
});
});
const tcLocation = {
codecKey: modemCodecPath,
isMo: true,
encoded: [0, 72, 1, 41, 117, 182, 221, 71, 130, 0, 90, 1, 99, 252, 107],
decoded: {
name: 'location',
serviceKey: 0,
messageKey: 72,
fields: {
fixStatus: 'Valid',
latitude: 2717110,
longitude: -4550908,
altitude: 90,
speed: 1,
heading: 99,
dayOfMonth: 31,
minuteOfDay: 1131,
},
},
};
const tcTxMetricsData = {
codecKey: modemCodecPath,
isMo: true,
idpmsgFile: '',
encoded: [0, 100, 6, 3,
0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0,
0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0],
decoded: {
name: 'txMetricsReport',
serviceKey: 0,
messageKey: 100,
fields: {
period: 'LastFullDay',
txMetrics: [
{ 'ack': {'packetsTotal': 3, 'packetsSuccess': 3, 'packetsFailed': 0} },
{ '0533': {'packetsTotal': 3, 'packetsSuccess': 3, 'packetsFailed': 0} },
],
},
},
};
const testMessages = [
// tcLocation,
tcTxMetricsData,
];
describe('#nimo/Message', function () {
const messageKeys = ['name', 'serviceKey', 'messageKey', 'fields'];
context('with idpmsg XML file', function() {
it('should decode the test messages', function() {
for (const tc of testMessages) {
const { encoded, decoded: expected, isMo } = tc;
const decoded = decodeMessage(encoded, idpmsgPath, isMo);
expect(decoded).to.be.an('Object').with.keys(messageKeys);
for (const [key, value] of Object.entries(decoded.fields)) {
if (!Array.isArray(value)) {
expect(expected.fields[key]).to.equal(value);
} else {
const expArray = expected.fields[key];
value.forEach((row, i) => {
for (const [k, v] of Object.entries(Object.values(row)[0])) {
const expObj = Object.values(expArray[i])[0];
expect(expObj[k]).to.equal(v);
}
});
}
}
}
});
});
context('with JSON file', function() {
it('should decode the test messages', function() {
for (const tc of testMessages) {
const { codecKey, encoded, decoded: expected, isMo } = tc;
const decoded = decodeMessage(encoded, codecKey, isMo);
expect(decoded).to.be.an('Object').with.keys(messageKeys);
for (const [key, value] of Object.entries(decoded.fields)) {
if (!Array.isArray(value)) {
expect(expected.fields[key]).to.equal(value);
} else {
const expArray = expected.fields[key];
value.forEach((row, i) => {
for (const [k, v] of Object.entries(Object.values(row)[0])) {
const expObj = Object.values(expArray[i])[0];
expect(expObj[k]).to.equal(v);
}
});
}
}
}
});
it('should encode the test messages', function() {
for (const tc of testMessages) {
const { codecKey, encoded: expected, decoded, isMo } = tc;
const encoded = encodeMessage(decoded, codecKey, isMo);
expect(Buffer.compare(encoded, Buffer.from(expected))).to.equal(0);
}
});
});
context('with XML string', function() {});
context('with JSON string', function() {});
context('with Codec object', function() {});
});