Examples

Here are some examples of custom decoders for a few very well known LoRaWAN devices to get you started.

Simple payload - RisingHF

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', label:'temperature'},
    {channelId: 2, type: ReadingType.humidity, value:+humidity.toFixed(2), unit:'%',label:'humidity' },
    {channelId: 3, type: ReadingType.time, value:periodSec, unit:'s',label:'uplink'},
    {channelId: 4, type: ReadingType.loraRssi, value:+rssi.toFixed(2),unit:'dB',label:'signal-strength'},
    {channelId: 5, type: ReadingType.loraSnr, value:+snr.toFixed(2),unit:'dB', label:'singal-to-noise'},
    {channelId: 6, type: ReadingType.voltage, value:+vcc.toFixed(2), unit:'V', label:'battery'},
  ]
}

Bitwise payload - Netvox device R311A

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

  if(buf.length != 11)
    throw new Error(`Invalid payload length. Expected exactly 11 bytes. Got ${buf.length}`);
      
  if(buf.readUInt8(0) !== 0x01) 
    throw new Error(`Invalid payload structure: expected loraWAN version 0x01 for byte 0, got => ${buf.readUInt8(0).toString(16)}`);
  
  // this is for R311A
  let expectedTypeCode:number = 0x02;
  
  if(buf.readUInt8(1) !== expectedTypeCode) 
    throw new Error(`Invalid payload structure: expected sensor type ${expectedTypeCode}, got => ${buf.readUInt8(1).toString(16)}`);
  
  if(buf.readUInt8(2) !== 0x01) 
    throw new Error(`Invalid report type ${buf.readUInt8(2)}, expected 0x01 got => ${buf.readUInt8(2).toString(16)}`);
  
  if(buf.readUInt8(4) !== 0x01 && buf.readUInt8(4) !== 0x00) 
    throw new Error(`Invalid payload structure: expected contact switch to be either 0x00 or 0x01, got => ${buf.readUInt8(4).toString(16)}`);

  let lowVoltage = (buf.readUInt8(3) & 0x80) === 0x80 ? true : false;
  let voltage = (buf.readUInt8(3) & 0x7F)/10; // byte 3

  let openContact =  buf.readUInt8(4) === 0x01 ? false : true;

  return {
    { channelId: 0, type:ReadingType.boolean, value: openContact ? 1 : 0, label:'open-contact'},
    { channelId: 1, type:ReadingType.voltage, value: voltage, label:'battery', unit:'V'},
    { channelId: 2, type:ReadingType.boolean, value: lowVoltage, label:'low-battery'}
  }
}

Variable length payload device - NSEN

function parseDeviceMsg(buf:Buffer, loraMsg:LoraMessage):ParserReading[] {
  
  // we support data messages only
  if(loraMsg.loraPort !== 3)
    return [];
    
  let offset = 0;

  let retList:ParserReading[] = [];

  while(offset < buf.length) {

    let {readingList, size } = this.readValue(buf);
    
    retList = retList.concat(readingList);

    // we move the cursor inside the buffer for the next reading
    buf = buf.slice(size);
  }

  return retList;
}

function readValue(buf:Buffer):{size:number, readingList:ParserReading[]} {

  let channelId = buf.readUInt8(0);
  let size = 2;

  let retList:ParserReading[] = [];

  // the channel meaning is fixed so we ignore the channel type information (byte 1)
  switch(channelId) {
    case 0: retList.push({channelId: channelId, type:ReadingType.voltage, value: buf.readInt16BE(2)/100, label:'primary-battery', unit:'V'}); size+=2; break;
    case 1: retList.push({channelId: channelId, type:ReadingType.current, value: buf.readInt16BE(2)/100, label:'primary-battery', unit:'mA'}); size+=2; break;
    case 2: retList.push({channelId: channelId, type:ReadingType.voltage, value: buf.readInt16BE(2)/100, label:'external-supply', unit:'V'}); size+=2; break;
    case 3: retList.push({channelId: channelId, type:ReadingType.voltage, value: buf.readInt16BE(2)/100, label:'secondary-battery', unit:'V'}); size+=2; break;
    case 4: retList.push({channelId: channelId, type:ReadingType.current, value: buf.readInt16BE(2)/100, label:'secondary-battery', unit:'mA'}); size+=2; break;
    case 5: retList.push({channelId: channelId, type:ReadingType.current, value: buf.readInt16BE(2)/100, label:'loop-current', unit:'mA'}); size+=2; break;
    case 6: retList.push({channelId: channelId, type:ReadingType.voltage, value: buf.readInt16BE(2)/100, label:'analog-input-1', unit:'V'}); size+=2; break;
    case 7: retList.push({channelId: channelId, type:ReadingType.voltage, value: buf.readInt16BE(2)/100, label:'analog-input-2', unit:'V'}); size+=2; break;
    case 8: retList.push({channelId: channelId, type:ReadingType.voltage, value: buf.readInt16BE(2)/100, label:'analog-input-3', unit:'V'}); size+=2; break;
    case 9: retList.push({channelId: channelId, type:ReadingType.analog, value: buf.readUInt16BE(2), label:'pulse-count-1', unit:'counts'}); size+=2; break;
    case 10: retList.push({channelId: channelId, type:ReadingType.analog, value: buf.readUInt16BE(2), label:'pulse-count-2', unit:'counts'}); size+=2; break;
    case 11: retList.push({channelId: channelId, type:ReadingType.analog, value: buf.readUInt16BE(2), label:'pulse-count-3', unit:'counts'}); size+=2; break;
    case 12: retList.push({channelId: channelId, type:ReadingType.humidity, value: buf.readUInt8(2)*0.5, unit:'%'}); size+=1; break;
    case 13: retList.push({channelId: channelId, type:ReadingType.temperature, value: buf.readInt16BE(2)/10, unit:'°C'}); size+=2; break;
    case 14: retList.push({channelId: channelId, type:ReadingType.pressure, value: buf.readUInt16BE(2)/10, unit:'hPa'}); size+=2; break;
    case 15: retList.push({channelId: channelId, type:ReadingType.acceleration, value:{x: buf.readInt16BE(2)/1000, y:buf.readInt16BE(4)/1000, z:buf.readInt16BE(6)/1000}, unit: 'G'}); size+=6;break;
    case 16: retList.push({channelId: channelId, type:ReadingType.pitch, value: buf.readInt16BE(2)/100, unit: '°'}); size+=2; break;
    case 17: retList.push({channelId: channelId, type:ReadingType.roll, value: buf.readInt16BE(2)/100, unit: '°'}); size+=2; break;
    case 18: retList.push({channelId: channelId, type:ReadingType.temperature, value: buf.readInt16BE(2)/10, unit: '°C', label:'thermistor-1'}); size+=2; break;
    case 19: retList.push({channelId: channelId, type:ReadingType.temperature, value: buf.readInt16BE(2)/10, unit: '°C', label:'thermistor-2'}); size+=2; break;
    case 20: retList.push({channelId: channelId, type:ReadingType.gps, value: { lat: buf.readIntBE(2, 3)/10000, long: buf.readIntBE(5, 3)/10000, alt: buf.readIntBE(8, 3)/100}}); size+=9; break;
    case 21: retList.push({channelId: channelId, type:ReadingType.percentage, value: buf.readInt16BE(2)/100, unit:'%', label:'pb-state-of-charge'}); size+=2; break;
    case 22: retList.push({channelId: channelId, type:ReadingType.percentage, value: buf.readInt16BE(2)/100, unit:'%', label:'cpu-usage'}); size+=2; break;
    case 23: retList.push({channelId: channelId, type:ReadingType.time, value: buf.readInt16BE(2)/100, unit:'months', label:'cpu-uptime'}); size+=2; break;
    case 24: retList.push({channelId: channelId, type:ReadingType.loraRssi, value: buf.readInt16BE(2)/100, unit:'db', label:'rssi-down'}); size+=2; break;
    case 25: retList.push({channelId: channelId, type:ReadingType.loraSnr, value: buf.readInt16BE(2)/100, label:'snr-down'}); size+=2; break;
    case 26: retList.push({channelId: channelId, type:ReadingType.boolean, value: buf.readUInt8(2), label:'pulse-state-1'}); size+=1; break;
    case 27: retList.push({channelId: channelId, type:ReadingType.boolean, value: buf.readUInt8(2), label:'pulse-state-2'}); size+=1; break;
    case 28: retList.push({channelId: channelId, type:ReadingType.boolean, value: buf.readUInt8(2), label:'pulse-state-3'}); size+=1; break;
    case 29: retList.push({channelId: channelId, type:ReadingType.digital, value: buf.readUInt32BE(2), label:'accum-pulse-count-1'}); size+=4; break;
    case 30: retList.push({channelId: channelId, type:ReadingType.digital, value: buf.readUInt32BE(2), label:'accum-pulse-count-2'}); size+=4; break;
    case 31: retList.push({channelId: channelId, type:ReadingType.digital, value: buf.readUInt32BE(2), label:'accum-pulse-count-3'}); size+=4; break;
    case 32: retList.push({channelId: channelId, type:ReadingType.pressure, value: buf.readInt16BE(2), unit:'kPa', label:'bridge-1'}); size+=2; break;
    case 33: retList.push({channelId: channelId, type:ReadingType.analog, value: buf.readUInt16BE(2)/100, label:'bridge-2'}); size+=2; break;
      
  }
    
  return { readingList: retList, size: size };
}