Custom Decoders

N2N-DL automatically tries to parse payloads coming from LoRaWAN devices by leveraging the deviceType field associated with a device.

N2N-DL will try and match the deviceType against a list of built-in device types associated with a built in decoder.

Custom decoders

In addition to the mechanism described above, a custom LoraParser can be associated with a device.

Custom LoraParsers take precedence over built in decoders and are Typescript snippets of code with a structure that matches the following example:

function parseDeviceMsg(buf:Buffer, loraMessage:LoraMessage) {

  if(buf.length != 9)
    throw new Error(`Invalid payload length. Expected exactly 9 bytes. Got ${buf.length}`);
      
  let status = buf.readUInt8(0);

  if(status !== 0x81 && status !== 0x01)
    throw new Error(`Invalid payload. Status can be 0x81 = OK or 0x01 = NO_DOWNLINK. Got 0x${status.toString(16)}`);
      
  let temperature = ((buf.readInt16LE(1) * 175.72) / 65536) - 46.85;
  let humidity = ((buf.readUInt8(3) * 125) / 256) -6;
  let periodSec = buf.readInt16LE(4) * 2;
  let rssi = buf.readUInt8(6) - 180;
  let snr = buf.readUInt8(7) / 4;
  let vcc = (buf.readUInt8(8) + 150) / 100;
  
  return [
    {channelId: 0, type: ReadingType.digital, value:status, label:'status'},
    {channelId: 1, type: ReadingType.temperature, value:+temperature.toFixed(2), unit:'°C'},
    {channelId: 2, type: ReadingType.humidity, value:+humidity.toFixed(2), unit:'%'},
    {channelId: 3, type: ReadingType.time, value:periodSec, unit:'s'},
    {channelId: 4, type: ReadingType.loraRssi, value:+rssi.toFixed(2)},
    {channelId: 5, type: ReadingType.loraSnr, value:+snr.toFixed(2)},
    {channelId: 6, type: ReadingType.voltage, value:+vcc.toFixed(2), unit:'V', label:'battery'},
  ]
}

Each snippet of code:

  • is valid Typescript
  • contains one parseDeviceMsg(buf:Buffer, loraMessage:LoraMessage) function that
    • accepts a Buffer and a LoraMessage object.
    • returns a list of ParserReadings

Built in libraries

To simplify development and allow for more coincise and correct code the following objects/functions can be safely referenced from your code:

Buffer object

The buffer object is a pure javascript re-implementation of the Node.JS buit in Buffer module according to the following type definitions

declare class Buffer {
  constructor(payload:string)            
  static from(payload:string):Buffer
  static concat(list:Buffer[]):Buffer
  
  from(payload:string):Buffer
  slice(start?:int, end?:int)
  
  readInt8(offset:number):number                        
  readUInt8(offset:number):number
  readUInt16BE(offset:number):number
  readUInt16LE(offset:number):number
  readInt16BE(offset:number):number
  readInt16LE(offset:number):number
  readUInt32BE(offset:number):number
  readUInt32LE(offset:number):number
  readInt32BE(offset:number):number
  readInt32LE(offset:number):number
  readUIntBE(offset:number, byteLength:number)
  readUIntLE(offset:number, byteLength:number)
  readIntBE(offset:number, byteLength:number)
  readIntLE(offset:number, byteLength:number)
  readFloatBE(offset:number):number
  readFloatLE(offset:number):number
  readDoubleBE(offset:number):number
  readDoubleLE(offset:number):number
  length:number;
}

Arguments and return types definitions

To enforce data quality and reduce typos and unwanted behaviour the following additional type definitions are provided

export enum ReadingType {
  analog = "analog",
  digital = "digital",
  boolean = "boolean",
  light = "light",
  distance = "distance",
  voltage = "voltage",
  humidity = "humidity",
  temperature = "temperature",
  sound = "sound",
  pitch = "pitch",
  roll = "roll",
  yaw = "yaw",
  inclination = "inclination",
  azimuth = "azimuth",
  radius = "radius",
  heading = "heading",
  direction = "direction",
  flow = "flow",
  gps = "gps",
  pressure = "pressure",
  concentration = "concentration",
  current = "current",
  speed = "speed",
  frequency = "frequency",
  percentage = "percentage",
  altitude = "altitude",
  weight = "weight",
  acidity = "acidity",
  power = "power",
  reactivePower = "reactive-power",
  energy = "energy",
  reactiveEnergy = "reactive-energy",
  loraRssi = "lora-rssi",
  loraSnr = "lora-snr",
  time = "time",
  acceleration = "acceleration",
  precipitation = "precipitation",
  circumference = "circumference",
  rain = "rain",
  uv = "uv",
  radiation = "radiation",
  evapotranspiration = "evapotranspiration",
  tag = "tag",
  tagList = "tagList"
}

declare interface ParserReading {
  channelId: number;
  type: ReadingType;
  value: any;
  label?: string;
  unit?: string;
  tsDelta?: number;
}

declare interface LoraMessage {
  ts: number;
  nowTs: number;
  deviceType:string,
  deviceName:string,
  deviceId:string,
  payloadHex:string,
  customMeta: Record<string, string|number|boolean|string[]|number[]>;
  lastDownlinkTs?: number,
  uplinkCnt:number,
  loraPort:number,
  late: boolean,
  quality?: {
    rssi: number,
    snr: number,
    spFact: number
  } 
}

Returned readings

The parser executes the parseDeviceMsg and expects a Javascript array of valid ParserReadings.

Strict data validation is applied to the returned data to assess compliance with the following rules

  1. channelId values must be integers >= 0
  2. type values must be as definited by the ReadingType enum
  3. value values must be numbers or strings or GPS objects (lat/long/alt)
  4. label values must match the following regex: ^[a-z-0-9]{3,}$
  5. unit values must match the following regex: ^[a-zA-Z0-9°%⁻¹²³±×μ/()]+$

Security

Each javascript code invocation is sandboxed and run by a stand-alone interpreter (not written in Javascript) that implements a small subset of the ES5 standard.

Each code snippet is validated before being accepted:

  • Typescript coversion and linting takes place before each execution
  • infinite while and for loops are not accepted
  • a part from the provided libraries no other built in functions/objects are available
  • setTimeout / setInterval functions are not implemented
  • a built in timeout of 50ms is strictly enforced to prevent resource starvation

Typescript

While plain Javascript is supported by the online IDE, Typescript bindings are provided to provide an improved developer experience and prevent typos.

While it’s not necessary to make use of the type definitions provided, they are highly recommended.

If you are not familiar with Typescript there are plenty of resources online to help you getting started

Defining a parser

Currently custom parsers are available through the N2N-DL V3 admin interface.

The user interface is self-explicatory and allows to define, modify, test and delete custom LoRaWAN parsers.

Debugging

Each custom parser can make use of the commonly used console.log statements. The output of these statements is routinely discarded but made available via the N2N-DL V3 admin interface custom parser testing page for testing and debugging purposes.

Parsing errors and exceptions are caught and logged as device events and made available through the device page in the Admin interface.