lorano
Version:
Compact and opinionated LoRa communications library
261 lines (200 loc) • 8.17 kB
Markdown
High-level Node.js module for reading and writing LoRa packets, with
support for over-the-air activation (OTAA) of devices.
It uses streams supplied by
[node-lora-comms](https://github.com/davedoesdev/node-lora-comms) and
[Anthony Kirby’s excellent packet
decoder](https://github.com/anthonykirby/lora-packet).
Your application is given a single
[Duplex](https://nodejs.org/dist/latest-v9.x/docs/api/stream.html#stream_class_stream_duplex)
stream operating in object mode for reading and writing packets.
Replying to a packet typically involves just supplying a payload, though
your application can override the default transmission settings if
required.
Tested on a Raspberry Pi 3 Model B with an IMST iC880A-SPI.
API documentation is available
[here](http://rawgit.davedoesdev.com/davedoesdev/lorano/master/docs/index.html).
# Example
This program works in conjunction with a [corresponding Arduino
sketch](test/lorano_test.ino) (tested on a [SODAQ
Explorer](http://support.sodaq.com/sodaq-one/explorer/)).
It reads 12-byte packets from the LoRa radio, leaves the first 6 bytes
unchanged and randomizes the last 6 bytes. It then sends the packet back
to the radio. The Explorer does the same but randomizes the first 6 byes
it receives, leaving the last 6 bytes unchanged.
Each side then checks whether it gets back the bytes it randomized in
the previous packet it sent.
**example.js.**
``` javascript
const path = require('path');
const crypto = require('crypto');
const { Link } = require('lorano');
const lora_comms = require('lora-comms');
const { Model } = require('objection');
// Start radio
process.on('SIGINT', lora_comms.stop);
lora_comms.start_logging();
lora_comms.log_info.pipe(process.stdout);
lora_comms.log_error.pipe(process.stderr);
lora_comms.start();
// Connect to database
const knex = require('knex')({
client: 'sqlite3',
useNullAsDefault: true,
connection: {
filename: path.join(__dirname, 'lorano.sqlite3')
}
});
Model.knex(knex);
lora_comms.on('stop', () => knex.destroy());
const link = new Link(Model, lora_comms.uplink, lora_comms.downlink, {
// USE YOUR OWN IDS!
appid: Buffer.alloc(8),
netid: crypto.randomBytes(3) // 7 lsb = NwkId
});
link.on('ready', async () =>
{
// Add device (usually you'll seed the database as a separate task)
const nwk_addr = crypto.randomBytes(4); // 25 lsb
nwk_addr[0] &= 0x01; // 7 msb must be 0
await knex('OTAADevices').insert({
// USE YOUR OWN VALUES!
NwkAddr: nwk_addr,
DevEUI: Buffer.from('0000000000000000', 'hex'),
AppKey: Buffer.alloc(16)
});
// Receive and send packets until we get a match
const duplex = require('awaitify-stream').createDuplexer(link);
const payload_size = 12;
let send_payload = crypto.randomBytes(payload_size);
while (true) {
const recv_data = await duplex.readAsync();
if (recv_data === null) {
return;
}
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('ERROR: Received packet we sent');
continue;
}
if (recv_data.payload.compare(send_payload,
payload_size/2,
payload_size,
payload_size/2,
payload_size) === 0) {
console.log('SUCCESS: Received matching data');
return lora_comms.stop();
}
send_payload = Buffer.concat([recv_data.payload.slice(0, payload_size/2),
crypto.randomBytes(payload_size/2)]);
recv_data.reply.payload = send_payload;
await duplex.writeAsync(recv_data.reply);
}
});
```
First you need to modify at least `DevEUI` with the unique identifier of
your device. You can then run the example like this:
``` bash
grunt example
```
``` bash
npm install lorano
```
In the top-level directory you’ll find a file called
`lorano.empty.sqlite3`. This contains an empty copy of the database
`lorano` uses to store information about your devices (addresses, keys,
activation state etc).
You should use a *copy* of this file in your application and pass its
location when setting up [Knex.js](http://knexjs.org/). By default the
database is SQLite but Knex.js supports many other databases. If you
want to use a different database, use the lorano
[migrations](migrations) to populate it and edit
[knexfile.js](knexfile.js) accordingly.
## Seeding device data
You need to add your devices to the database otherwise lorano will
ignore packets it receives from them.
### Over-The-Air Activation (OTAA) devices
For each of your devices, you must add a row to the `OTAADevices` table
containing the following columns:
- `NwkAddr`: Unique network address of the device. This can be random
as long as it’s unique. It must be a binary value 4 bytes long but
only the 25 least significant bits are used and the 7 most
significant must be 0.
- `DevEUI`: Unique global device ID in the IEEE EUI64 address space.
This must be a binary value 8 bytes long and can usually be quered
from the hardware by calling a device API.
- `AppKey`: The AES-128 key that you’ve assigned to the device for
your application. This is a shared secret between your application
and the device and is used to derive session keys specific for the
device to encrypt and verify packets communication with it. It must
be a binary value 16 bytes long.
An example of seeding the database for an OTAA device using Knex.js can
be found in [test/seeds/test\_otaa.js](test/seeds/test_otaa.js).
### Activation By Personalization (ABP) devices
For each of your devices, you must add a row to the `ABPDevices` table
containing the following columns:
- `DevAddr`: The identifier of your network (7 most significant bits)
and the unique address of the device within it (25 least significant
bits). This must be a binary value 4 bytes long.
- `NwkSKey`: Network session key for the device. This is a shared
secret between your application and the device and is used to
calculate and verify the message integrity code of data messages. It
must be a binary value 16 bytes long.
- `AppSKey`: Application session key for the device. This is a shared
secret between your application and the device and it used to
encrypt and decrypt data messages. It must be a binary value 16
bytes long.
An example of seeding the database for an ABP device using Knex.js can
be found in [test/seeds/test\_abp.js](test/seeds/test_abp.js).
# IMST iC880A-SPI reset
If you’re using an IMST iC880A-SPI, it needs to be reset after it’s
powered up.
My iC880A-SPI is connected to a Pi via a
[backplane](https://shop.coredump.ch/product/ic880a-lorawan-gateway-backplane/)
which brings the reset line out on GPIO 25. I run the following shell
script to perform the reset:
**iC880A-SPI\_reset.sh.**
``` sh
#!/bin/sh
echo "25" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio25/direction
echo "1" > /sys/class/gpio/gpio25/value
sleep 5
echo "0" > /sys/class/gpio/gpio25/value
sleep 1
echo "0" > /sys/class/gpio/gpio25/value
chmod o+rw /dev/spidev0.0
```
# Test
By default, the tests simulate LoRa packets and can be run with:
``` bash
grunt test
```
If you have a LoRa device that can run
[test/lorano\_test.ino](test/lorano_test.ino) then you can pass its
DEVEUI as an argument like this:
``` bash
grunt test --deveui=XXXXXXXXXXXXXXXX
```
I’ve tested this with a SODAQ Explorer.
``` bash
grunt lint
```
``` bash
grunt coverage
```
Or with a LoRa device running
[](test/lorano_test.ino):
``` bash
grunt coverage --deveui=XXXXXXXXXXXXXXXX
```
[](https://github.com/bcoe/c8) results are available
[](http://rawgit.davedoesdev.com/davedoesdev/lorano/master/coverage/lcov-report/index.html).
[](LICENCE)