lorano
Version:
Compact and opinionated LoRa communications library
868 lines (770 loc) • 25.5 kB
JavaScript
;
// Tested with a SODAQ ExpLoRer running ./lorano_test.ino
const { Link } = require('..'),
lora_comms = require('lora-comms'),
aw = require('awaitify-stream'),
lora_packet = require('lora-packet'),
{ Model } = require('objection'),
Knex = require('knex'),
expect = require('chai').expect,
path = require('path'),
crypto = require('crypto'),
promisify = require('util').promisify,
delay = promisify(setTimeout),
{ LeftDuplex } = require('./memory-duplex'),
yargs = require('yargs'),
argv = yargs
.option('deveui', {
type: 'string',
coerce: arg => Buffer.from(arg, 'hex'),
default: Buffer.alloc(8)
}).argv,
deveui = argv.deveui,
PROTOCOL_VERSION = 2,
pkts = {
PUSH_DATA: 0,
PUSH_ACK: 1,
PULL_DATA: 2,
PULL_RESP: 3,
PULL_ACK: 4,
TX_ACK: 5
};
let link, TestModel, uplink, downlink;
before(function ()
{
const knex = Knex({
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: path.join(__dirname, 'lorano.sqlite3')
}
});
TestModel = class extends Model {};
TestModel.knex(knex);
});
after(function ()
{
TestModel.knex().destroy();
});
function start_simulate(options, cb)
{
uplink = new LeftDuplex();
downlink = new LeftDuplex();
const appid = Buffer.alloc(8),
netid = crypto.randomBytes(3),
app_key = Buffer.alloc(16);
link = new Link(TestModel, uplink.right, downlink.right, Object.assign(
{
appid: appid,
netid: netid
}, options));
(async () => { try {
const up = aw.createDuplexer(uplink),
down = aw.createDuplexer(downlink),
pull_data = Buffer.alloc(12);
if (options.send_initial_unknown_packet)
{
await down.writeAsync(pull_data.slice(0, 1));
}
for (let i = 0; i < (options.multiple_pull_data ? 10 : 1); ++i)
{
pull_data[0] = PROTOCOL_VERSION;
crypto.randomFillSync(pull_data, 1, 2);
pull_data[3] = pkts.PULL_DATA;
await down.writeAsync(pull_data);
const pull_ack = await down.readAsync();
expect(pull_ack.length).to.equal(4);
expect(pull_ack[0]).to.equal(PROTOCOL_VERSION);
expect(pull_ack[1]).to.equal(pull_data[1]);
expect(pull_ack[2]).to.equal(pull_data[2]);
expect(pull_ack[3]).to.equal(pkts.PULL_ACK);
}
const payload_size = 12;
let dev_addr, nwk_skey, app_skey;
let fcnt_up = 0, fcnt_down = 0;
if (options.otaa)
{
const dev_nonce = crypto.randomBytes(2);
if (options.send_data_before_joined)
{
const push_data = Buffer.alloc(12);
push_data[0] = PROTOCOL_VERSION;
crypto.randomFillSync(push_data, 1, 2);
push_data[3] = pkts.PUSH_DATA;
await up.writeAsync(Buffer.concat(
[
push_data,
Buffer.from(JSON.stringify(
{
rxpk: [{
data: lora_packet.fromFields({
MType: 'Unconfirmed Data Up',
DevAddr: link.nwk_addr_to_dev_addr((await link._deveui_to_otaa_device(deveui)).NwkAddr),
payload: Buffer.alloc(payload_size),
FCnt: fcnt_up++
}, Buffer.alloc(16), Buffer.alloc(16)).getPHYPayload().toString('base64')
}]
}))
]));
const push_ack = await up.readAsync();
expect(push_ack.length).to.equal(4);
expect(push_ack[0]).to.equal(PROTOCOL_VERSION);
expect(push_ack[1]).to.equal(push_data[1]);
expect(push_ack[2]).to.equal(push_data[2]);
expect(push_ack[3]).to.equal(pkts.PUSH_ACK);
}
const request_join = async (appid, deveui, app_key) =>
{
const push_data = Buffer.alloc(12);
push_data[0] = PROTOCOL_VERSION;
crypto.randomFillSync(push_data, 1, 2);
push_data[3] = pkts.PUSH_DATA;
await up.writeAsync(Buffer.concat(
[
push_data,
Buffer.from(JSON.stringify(
{
rxpk: [{
data: lora_packet.fromFields({
MType: 'Join Request',
AppEUI: appid,
DevEUI: deveui,
DevNonce: dev_nonce
}, null, null, app_key).getPHYPayload().toString('base64')
}]
}))
]));
const push_ack = await up.readAsync();
expect(push_ack.length).to.equal(4);
expect(push_ack[0]).to.equal(PROTOCOL_VERSION);
expect(push_ack[1]).to.equal(push_data[1]);
expect(push_ack[2]).to.equal(push_data[2]);
expect(push_ack[3]).to.equal(pkts.PUSH_ACK);
};
await request_join(appid, deveui, app_key);
if (options.send_join_with_unknown_appid)
{
const appid2 = Buffer.from(appid);
appid2[0] ^= 0xff;
await request_join(appid2, deveui, app_key);
}
if (options.send_join_with_unknown_deveui)
{
const deveui2 = Buffer.from(deveui);
deveui2[0] ^= 0xff;
await request_join(appid, deveui2, app_key);
}
if (options.send_join_with_wrong_appkey)
{
const app_key2 = Buffer.from(app_key);
app_key2[0] ^= 0xff;
await request_join(appid, deveui, app_key2);
}
if (options.replay_join)
{
await request_join(appid, deveui, app_key);
}
const pull_resp = await down.readAsync();
if (!pull_resp) { return; }
expect(pull_resp.length).to.be.at.least(4);
expect(pull_resp[0]).to.equal(PROTOCOL_VERSION);
expect(pull_resp[3]).to.equal(pkts.PULL_RESP);
let decoded = lora_packet.fromWire(Buffer.from(
JSON.parse(pull_resp.slice(4)).txpk.data, 'base64'));
expect(decoded.getMType()).to.equal('Join Accept');
const tx_ack = Buffer.alloc(12);
tx_ack[0] = PROTOCOL_VERSION;
tx_ack[1] = pull_resp[1];
tx_ack[2] = pull_resp[2];
tx_ack[3] = pkts.TX_ACK;
await down.writeAsync(tx_ack);
const cipher = crypto.createCipheriv('aes-128-ecb', app_key, '');
cipher.setAutoPadding(false);
let buffers = decoded.getBuffers();
decoded = lora_packet.fromWire(Buffer.concat(
[
buffers.MHDR,
cipher.update(buffers.MACPayloadWithMIC),
cipher.final()
]));
expect(decoded.getMType()).to.equal('Join Accept');
expect(lora_packet.verifyMIC(decoded, null, app_key)).to.be.true;
buffers = decoded.getBuffers();
expect(buffers.NetID.equals(netid)).to.be.true;
expect(buffers.DevAddr[0] >> 1).to.equal(netid[2] & 0x7f);
dev_addr = buffers.DevAddr;
expect((await link.dev_addr_to_deveui(dev_addr)).equals(deveui)).to.be.true;
const dev_addr2 = Buffer.from(dev_addr);
dev_addr2[0] ^= 0xff;
expect(await link.dev_addr_to_deveui(dev_addr2)).to.equal(null);
nwk_skey = Link.skey(netid, app_key, 0x01, buffers.AppNonce, dev_nonce);
app_skey = Link.skey(netid, app_key, 0x02, buffers.AppNonce, dev_nonce);
}
else
{
dev_addr = Buffer.alloc(4);
nwk_skey = Buffer.alloc(16);
app_skey = Buffer.alloc(16);
}
let recv_payload = Buffer.alloc(payload_size);
while (true)
{
const send_payload = Buffer.concat(
[
crypto.randomBytes(payload_size / 2),
recv_payload.slice(payload_size / 2)
]);
const send_data = async (dev_addr, nwk_skey, fcnt_up) =>
{
const push_data = Buffer.alloc(12);
push_data[0] = PROTOCOL_VERSION;
crypto.randomFillSync(push_data, 1, 2);
push_data[3] = pkts.PUSH_DATA;
await up.writeAsync(Buffer.concat(
[
push_data,
Buffer.from(JSON.stringify(
{
rxpk: [{
data: lora_packet.fromFields({
MType: 'Unconfirmed Data Up',
DevAddr: dev_addr,
payload: send_payload,
FCnt: fcnt_up
}, app_skey, nwk_skey).getPHYPayload().toString('base64')
}]
}))
]));
const push_ack = await up.readAsync();
expect(push_ack.length).to.equal(4);
expect(push_ack[0]).to.equal(PROTOCOL_VERSION);
expect(push_ack[1]).to.equal(push_data[1]);
expect(push_ack[2]).to.equal(push_data[2]);
expect(push_ack[3]).to.equal(pkts.PUSH_ACK);
};
await send_data(dev_addr, nwk_skey, fcnt_up++);
if (options.send_data_with_unknown_devaddr)
{
const dev_addr2 = Buffer.from(dev_addr);
dev_addr2[0] ^= 0xff;
await send_data(dev_addr2, nwk_skey, fcnt_up++);
}
if (options.send_data_with_wrong_session_key)
{
const nwk_skey2 = Buffer.from(nwk_skey);
nwk_skey2[0] ^= 0xff;
await send_data(dev_addr, nwk_skey2, fcnt_up++);
}
if (options.replay_data)
{
await send_data(dev_addr, nwk_skey, fcnt_up - 1);
}
if (options.send_no_rxpk)
{
const push_data = Buffer.alloc(12);
push_data[0] = PROTOCOL_VERSION;
crypto.randomFillSync(push_data, 1, 2);
push_data[3] = pkts.PUSH_DATA;
await up.writeAsync(Buffer.concat(
[
push_data,
Buffer.from(JSON.stringify({}))
]));
const push_ack = await up.readAsync();
expect(push_ack.length).to.equal(4);
expect(push_ack[0]).to.equal(PROTOCOL_VERSION);
expect(push_ack[1]).to.equal(push_data[1]);
expect(push_ack[2]).to.equal(push_data[2]);
expect(push_ack[3]).to.equal(pkts.PUSH_ACK);
}
const pull_resp = await down.readAsync();
if (!pull_resp) { break; }
expect(pull_resp.length).to.be.at.least(4);
expect(pull_resp[0]).to.equal(PROTOCOL_VERSION);
expect(pull_resp[3]).to.equal(pkts.PULL_RESP);
const decoded = lora_packet.fromWire(Buffer.from(
JSON.parse(pull_resp.slice(4)).txpk.data, 'base64'));
expect(decoded.getMType()).to.equal('Unconfirmed Data Down');
const tx_ack = Buffer.alloc(12);
tx_ack[0] = PROTOCOL_VERSION;
tx_ack[1] = pull_resp[1];
if (options.write_wrong_tx_ack_token)
{
tx_ack[1] ^= 0xff;
}
tx_ack[2] = pull_resp[2];
tx_ack[3] = pkts.TX_ACK;
await down.writeAsync(tx_ack);
const buffers = decoded.getBuffers();
expect(buffers.DevAddr.equals(dev_addr)).to.equal(true);
expect(lora_packet.verifyMIC(decoded, nwk_skey)).to.be.true;
const fcnt = Buffer.alloc(2);
fcnt.writeUInt16BE(fcnt_down++, 0);
expect(buffers.FCnt.equals(fcnt)).to.be.true;
recv_payload = lora_packet.decrypt(decoded, app_skey, nwk_skey);
expect(recv_payload.length).to.equal(payload_size);
expect(recv_payload.compare(send_payload,
0,
payload_size/2,
0,
payload_size/2)).to.equal(0);
}
} catch (ex) {
console.error(ex);
}})();
link.on('ready', options.throw_error_in_ready ? function ()
{
throw new Error('dummy');
} : cb);
link.on('error', options.error_handler || cb);
}
function stop_simulate(cb)
{
uplink.end();
downlink.end();
downlink.right.end();
cb();
}
function start(cb)
{
if (!yargs(process.argv).argv.deveui)
{
return start_simulate({ otaa: true }, cb);
}
lora_comms.start_logging();
lora_comms.log_info.pipe(process.stdout);
lora_comms.log_error.pipe(process.stderr);
lora_comms.start();
link = new Link(TestModel, lora_comms.uplink, lora_comms.downlink,
{
// USE YOUR OWN IDS!
appid: Buffer.alloc(8),
netid: crypto.randomBytes(3) // 7 lsb = NwkId
});
link.on('ready', cb);
link.on('error', cb);
}
function stop(cb)
{
if (!yargs(process.argv).argv.deveui)
{
return stop_simulate(cb);
}
if (!lora_comms.active)
{
return cb();
}
lora_comms.once('stop', cb);
lora_comms.stop();
}
process.on('SIGINT', () => stop(() => {}));
function wait_for_logs(cb)
{
if (!yargs(process.argv).argv.deveui || !lora_comms.logging_active)
{
return cb();
}
lora_comms.once('logging_stop', cb);
// no need to call lora_comms.stop_logging(), logging_stop will be emitted
// once the log streams end
}
async function same_data_sent_with_options(options)
{
options = options || {};
const payload_size = 12;
let duplex = aw.createDuplexer(link);
let send_payload = crypto.randomBytes(payload_size);
while (true)
{
let recv_data = await duplex.readAsync();
if (recv_data.payload.length !== payload_size)
{
continue;
}
if (recv_data.payload.equals(send_payload))
{
// Shouldn't happen because send on reverse polarity
console.error('Received packet we sent');
continue;
}
if (recv_data.payload.compare(send_payload,
payload_size/2,
payload_size,
payload_size/2,
payload_size) === 0)
{
return;
}
send_payload = Buffer.concat([recv_data.payload.slice(0, payload_size/2),
crypto.randomBytes(payload_size/2)]);
recv_data.reply.payload = send_payload;
if (options.write_to_unknown_device)
{
recv_data.reply.encoding.DevAddr[0] ^= 0xff;
}
await duplex.writeAsync(recv_data.reply);
if (options.delay_after_write !== undefined)
{
await delay(options.delay_after_write);
}
}
}
async function same_data_sent()
{
await same_data_sent_with_options();
}
describe('should emit error when writing to unjoined device', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
error_handler: function (err)
{
expect(err.message).to.equal('device not joined');
}
}, cb));
afterEach(stop_simulate);
it('should error sending data', async () =>
{
let err;
try
{
const dev_addr = link.nwk_addr_to_dev_addr((await link._deveui_to_otaa_device(deveui)).NwkAddr),
write = promisify((data, cb) => link.write(data, cb));
await write(
{
encoding: {
DevAddr: dev_addr
}
});
}
catch (ex)
{
err = ex;
}
expect(err.message).to.equal('device not joined');
});
});
describe('should emit not_joined event when packet received from unjoined OTAA device', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_data_before_joined: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let called = false;
link.on('not_joined', () =>
{
called = true;
});
await same_data_sent();
expect(called).to.be.true;
});
});
describe('echoing device with OTAA', function ()
{
this.timeout(60 * 60 * 1000);
beforeEach(start);
afterEach(stop);
afterEach(wait_for_logs);
it('should receive same data sent', same_data_sent);
});
describe('echoing device with ABP', function ()
{
beforeEach(cb => start_simulate({ otaa: false }, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});
describe('should cope with unknown packets', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_initial_unknown_packet: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});
describe('should emit error occurring while waiting for initial packet', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
throw_error_in_ready: true,
error_handler: function (err)
{
expect(err.message).to.equal('dummy');
cb();
}
}, cb));
afterEach(stop_simulate);
it('should emit error', same_data_sent);
});
describe('should pass options to Duplex', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
highWaterMark: 0
}, cb));
beforeEach(() =>
{
expect(link.readableHighWaterMark).to.equal(0);
expect(link.writableHighWaterMark).to.equal(0);
});
afterEach(stop_simulate);
it('should receive same data sent', async () =>
await same_data_sent_with_options(
{
delay_after_write: 500
}));
});
describe('should ignore packet with missing rxpk', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_no_rxpk: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});
describe('should emit errors that occur while reading', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
error_handler: function (err)
{
expect(err.message).to.equal('dummy');
}
}, cb));
beforeEach(() =>
{
link._pending.shift = function ()
{
throw new Error('dummy');
};
});
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let err;
try
{
await same_data_sent();
}
catch (ex)
{
err = ex;
}
expect(err.message).to.equal('dummy');
});
});
describe('should emit error when writing to unknown device', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
error_handler: function (err)
{
expect(err.message).to.equal('unknown device');
}
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let err;
try
{
await same_data_sent_with_options(
{
write_to_unknown_device: true
});
}
catch (ex)
{
err = ex;
}
expect(err.message).to.equal('unknown device');
});
});
describe('should emit error when FCntDownMax exceeded', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
FCntDownMax: -1,
error_handler: function (err)
{
expect(err.message).to.equal('send frame count exceeded');
}
}, cb));
afterEach(stop_simulate);
it('should error sending data', async () =>
{
let err;
try
{
const dev_addr = link.nwk_addr_to_dev_addr((await link._deveui_to_otaa_device(deveui)).NwkAddr),
write = promisify((data, cb) => link.write(data, cb));
await write({ encoding: { DevAddr: dev_addr } });
}
catch (ex)
{
err = ex;
}
expect(err.message).to.equal('send frame count exceeded');
});
});
describe('should emit error when TX_ACK token does not match', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
write_wrong_tx_ack_token: true,
error_handler: function (err)
{
expect(err.message).to.equal('TX_ACK token mismatch');
}
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let err;
try
{
await same_data_sent();
}
catch (ex)
{
err = ex;
}
expect(err.message).to.equal('TX_ACK token mismatch');
});
});
describe('should ignore join requests with unknown appid', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_join_with_unknown_appid: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});
describe('should ignore join requests with unknown deveui', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_join_with_unknown_deveui: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});
describe('should ignore join requests with wrong app key and emit verify_mic event', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_join_with_wrong_appkey: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let called = false;
link.on('verify_mic_failed', (device, decoded) =>
{
expect(device.DevEUI.equals(deveui)).to.be.true;
expect(decoded.getBuffers().DevEUI.equals(deveui)).to.be.true;
called = true;
});
await same_data_sent();
expect(called).to.be.true;
});
});
describe('should ignore replayed join requests and emit join_replay event', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
replay_join: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let called = false;
link.on('join_replay', (device, decoded, ex) =>
{
expect(device.DevEUI.equals(deveui)).to.be.true;
expect(decoded.getBuffers().DevEUI.equals(deveui)).to.be.true;
called = true;
});
await same_data_sent();
expect(called).to.be.true;
});
});
describe('should ignore data packets with unknown devaddr', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_data_with_unknown_devaddr: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});
describe('should ignore data packets with wrong session key and emit verify_mic event', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
send_data_with_wrong_session_key: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let called = false;
link.on('verify_mic_failed', (device, decoded) =>
{
expect(device.DevEUI.equals(deveui)).to.be.true;
called = true;
});
await same_data_sent();
expect(called).to.be.true;
});
});
describe('should ignore replayed data packets and emit data_replay event', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
replay_data: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', async () =>
{
let called = false;
link.on('data_replay', (device, decoded) =>
{
expect(device.DevEUI.equals(deveui)).to.be.true;
called = true;
});
await same_data_sent();
expect(called).to.be.true;
});
});
describe('should reply to multiple PULL_DATA messages', function ()
{
beforeEach(cb => start_simulate(
{
otaa: true,
multiple_pull_data: true
}, cb));
afterEach(stop_simulate);
it('should receive same data sent', same_data_sent);
});