Laird RS1xx payload format

After the RS1xx send a payload informing the firmware update, the payload began to be sent. But the information in the payload isn´t making sense:

{
  "AlarmMsgCount": 31237,
  "BatteryCapacity": "80-100%",
  "BcklogMsgCount": 41988,
  "MsgType": "Send Temp RH Data Notification",
  "Options": "Inform Server to send UT to sensor in the next downlink transmission",
  "humidity": 166.44,
  "temperature": 64.45
}

I mean, the temperature isn´t 64.45 C, the correct is 25.37. The humidity isn´t 166.44, the correct is 66.1

What’s the payload? How’s that payload being decoded? Did you validate that against some documentation? And isn’t that a different question than the topic at hand?

I will send the payload when I be back to home. The documentation is the data Laird RS1xx data dictionary: https://assets.lairdtech.com/home/brandworld/files/RS1xx%20LoRa%20Protocol_v2_2.pdf

This is the decoder that I developed:

function Decoder(Payload) {
	var Param = {
		MsgType:         0      ,
		optVlues:        0      ,
		BatteryCapacity: "none" ,
		BatType:         "none" ,
		humidity:        0      ,
		temperature:     0      ,
		AlarmMsgCount:   0      ,
		BcklogMsgCount:  0      ,
		NumReads:        0      ,
		Tmtstp:          0      ,
		ReadSenPer:      0      ,
		SenAggr:         0      ,
		TmpAlarmEna:     false  ,
		HumAlarmEna:     false  ,
		LED_BLE:         0      ,
		LED_Heartb:      0      ,		
		Year:            0      ,
		Month:           0	    ,
		Day:             0      ,
		VerMaj:		     0      ,
		VerMin:          0	    ,
		PartNum:         0		    
		};
	Param.MsgType         = Payload[0];
	Param.optVlues        = Payload[1];
	  switch (Param.optVlues) {
		  case 0:
			   Param.Options = "Server response is LoRa ack.";
			   break;
		  case 1:
			   Param.Options = "Inform Server to send UT to sensor in the next downlink transmission";
			  break;
		  case 2:
			  Param.Options = "Sensor configuration error.";
			  break;
		  case 4:
			  Param.Options = "Sensor has alarm condition.";
			  break;
		  default:
			  Param.Options = "This value is not supported";
			  break;
	  }
	  switch(Param.MsgType) {
		  case 1:
		  	  Param.humidity    = ((Payload[3] << 8) + Payload[2])/100;
			  Param.temperature = ((Payload[5] << 8) + Payload[4])/100;
			  switch(Payload[6]){
				  case 0:
					   Param.BatteryCapacity = "0-5%";
					   break;
				  case 1:
					   Param.BatteryCapacity = "5-20%";
					  break;
				  case 2:
					  Param.BatteryCapacity = "20-40%";
					  break;
				  case 3:
					  Param.BatteryCapacity = "40-60%";
					  break;
				  case 4:
					  Param.BatteryCapacity = "60-80%";
					  break;
				  case 5:
					  Param.BatteryCapacity = "80-100%";
					  break;
				  default:
					  Param.BatteryCapacity = "This value is not supported";
			  }
			  Param.AlarmMsgCount  = (Payload[8] << 8) + Payload[7];
			  Param.BcklogMsgCount = (Payload[10] << 8) + Payload[9];
			  return {
				  MsgType: "Send Temp RH Data Notification",
				  Options: Param.Options,
				  humidity: Param.humidity,
				  temperature: Param.temperature,
				  BatteryCapacity: Param.BatteryCapacity,
				  AlarmMsgCount: Param.AlarmMsgCount,
				  BcklogMsgCount: Param.BcklogMsgCount
			  };
		  case 2:
		      Param.AlarmMsgCount  = Payload[2];
			  Param.BcklogMsgCount = (Payload[4] << 8) + Payload[3];
			  switch(Payload[5]){
				  case 0:
					  Param.BatteryCapacity = "0-5%";
					  break;
				  case 1:
					  Param.BatteryCapacity = "5-20%";
					  break;
				  case 2:
					  Param.BatteryCapacity = "20-40%";
					  break;
				  case 3:
					  Param.BatteryCapacity = "40-60%";
					  break;
				  case 4:
					  Param.BatteryCapacity = "60-80%";
					  break;
				  case 5:
					  Param.BatteryCapacity = "80-100%";
					  break;
				  default:
					  Param.BatteryCapacity = "This value is not supported";
			  }
			  Param.NumReads    = Payload[6];
			  Param.Tmtstp      =(Payload[10] << 24)+(Payload[9] << 16) +(Payload[8] << 8) + Payload[7];
			  Param.humidity    =((Payload[12] << 8) + Payload[11])/100;
			  Param.temperature =((Payload[14] << 8) + Payload[13])/100;
			  return {
				  MsgType: "Send Temp RH Aggregated Data",
				  Options: Param.optVlues,
				  AlarmMsgCount: Param.AlarmMsgCount,
				  BcklogMsgCount: Param.BcklogMsgCount,
				  BatteryCapacity: Param.BatteryCapacity,
				  NumReads: Param.NumReads,
				  Tmtstp: Param.Tmtstp,
				  humidity: Param.humidity,
				  temperature: Param.temperature
			  };               
		  case 3:
		      Param.Tmtstp      =(Payload[5] << 24)+(Payload[4] << 16) +(Payload[3] << 8) + Payload[2];
			  Param.humidity    =((Payload[7] << 8) + Payload[6])/100;
			  Param.temperature =((Payload[9] << 8) + Payload[8])/100;
			  return {
				  MsgType: "Send Backlog Message Notification",
				  Options: Param.Options,
				  Tmtstp: Param.Tmtstp,
				  humidity: Param.humidity,
				  temperature: Param.temperature
			  };
		  case 4:
		      Param.Tmtstp      =(Payload[6] << 24)+(Payload[5] << 16) +(Payload[4] << 8) + Payload[3];
			  Param.humidity    =((Payload[8] << 8) + Payload[7])/100;
			  Param.temperature =((Payload[10] << 8) + Payload[9])/100;
			  return {
				  MsgType: "Send Backlog Messages Notification",
				  Options: Param.Options,
				  Tmtstp: Param.Tmtstp,
				  humidity: Param.humidity,
				  temperature: Param.temperature
			  };
		  case 5:
			  switch(Payload[2]) {
				  case 1:
					  Param.BatType = "Zinc-Manganese Dioxide (Alkaline).";
					  break;
				  case 2:
					  Param.BatType = "Lithium/Iron Disulfide (Primary Lithium).";
					  break;
				  default:
					  Param.BatType = "Unknow battery type";
			  }
			  Param.ReadSenPer  =(Payload[4] << 8) + Payload[3];
			  Param.SenAggr     = Payload[5];
			  Param.TmpAlarmEna = Payload[6];
			  Param.HumAlarmEna = Payload[7];
			  return {
				  MsgType: "Send Sensor Config Simple Notification",
				  Options: Param.Options,
				  BatType: Param.BatType,
				  ReadSenPer: Param.ReadSenPer,
				  SenAggr: Param.SenAggr,  
				  TmpAlarmEna: Param.TmpAlarmEna,
				  HumAlarmEna: Param.HumAlarmEna
			  };
		  case 6:
			  switch(Payload[2]) {
				  case 1:
					  Param.BatType  = "Zinc-Manganese Dioxide (Alkaline).";
					  break;
				  case 2:
					  Param.BatType  = "Lithium/Iron Disulfide (Primary Lithium).";
					  break;
				  default:
					  Param.BatType  = "Unknow battery type";
			  }
			  Param.ReadSenPer   =(Payload[4] << 8) + Payload[3];
			  Param.SenAggr      = Payload[5];
			  Param.TmpAlarmEna  = Payload[6];
			  Param.HumAlarmEna  = Payload[7];
			  Param.temperature  = ((Payload[9] << 8) + Payload[8])/100;
			  Param.humidity     = ((Payload[11] << 8) + Payload[10])/100;
			  Param.LED_BLE      = (Payload[13] << 8) + Payload[12];
			  Param.LED_Heartb   = (Payload[15] << 8) + Payload[14];			
			  return {
				  MsgType: "Send Sensor Config Advanced Notification",
				  Options: Param.Options,
				  BatType: Param.BatType,
				  ReadSenPer: Param.ReadSenPer,
				  SenAggr: Param.SenAggr,  
				  TmpAlarmEna: Param.TmpAlarmEna,
				  HumAlarmEna: Param.HumAlarmEna,
				  humidity: Param.humidity,
				  temperature: Param.temperature,
				  LED_BLE: Param.LED_BLE,
				  LED_Heartb: Param.LED_Heartb
			  };
		  case 7:
		      Param.Year    = Payload[2];
			  Param.Month   = Payload[3];
			  Param.Day     = Payload[4];
			  Param.VerMaj  = Payload[5];
			  Param.VerMin  = Payload[6];
			  Param.PartNum = Payload[7];
			  return {
				  MsgType: "Send FW Version Notification",
				  Options: Param.Options,
				  Year: Param.Year, 
				  Month: 	Param.Month,
				  Day: Param.Day,
				  VerMaj:	Param.VerMaj,
				  VerMin:	Param.VerMin,
				  PartNum: Param.PartNum
			  };
	  }
  }

Without seeing examples of the payload and their expected values, we cannot help you debug the Decoder.

However, the documentation you linked to shows:

image

To me, “Decimal value of temperature measurement” and “Integer value of temperature measurement” suggest you might need something like:

// Unary plus-operator to convert string result to a number
Param.humidity = +(Payload[3] + "." + Payload[2]);

And with:

the temperature isn´t 64.45 C, the correct is 25.37. The humidity isn´t 166.44, the correct is 66.1

Decimal 6445 being 0x192D hexadecimal, the above would get you 0x19 + "." + 0x2D = 25.45 instead. And decimal 16644 being 0x4104, this translates to 65.4, assuming it should not read 65.04 instead? If it should, you’d need:

Param.humidity = Payload[3] + Payload[2] / 100;

(Quite a suggested accuracy in those numbers; I guess the true measurements are not that exact.)

First of all, thank you very much by your effort to help me :slight_smile:

This is one payload example: 01013047281905057A04A4

Here the decoder result:

 {
  "AlarmMsgCount": 31237,
  "BatteryCapacity": "80-100%",
  "BcklogMsgCount": 41988,
  "MsgType": "Send Temp RH Data Notification",
  "Options": "Inform Server to send UT to sensor in the next downlink transmission",
  "humidity": 182.24,
  "temperature": 64.4
}

Let me review my decoder logic

And what exact values are you expecting for that payload?

I copied the following from the Laird Node-RED example:

msg.humidity = bArray[idx++] / 100 + bArray[idx++];
msg.temp = convertTempUnits(bArray[idx++], bArray[idx++]);

…along with:

// Convert the two byte sensor data format to a signed number
// 
// tInt: the integer portion of the temp  
// tDec: the fractional portion of the temp  
// 
function convertTempUnits( tDec ,  tInt ){
    // the integer portion is a signed two compliment value convert it to a signed number
    if( tInt > 127 ){
        tInt -= 256
    }
    // the fractional portion of the number is unsigned and represents the part of the temp
    // after the base 10 decimal point
    let t = tInt + (tDec * Math.sign(tInt) / 100);

    // if the global flag for using degreesFahenheit is set convert the units
    if(global.get("degreesFahrenheit")){
        t = t * 1.8 + 32; 
    }
    return t; 
}

As the “otto” JavaScript parser as used by TTN does not support all of the above, you can modify this for use in a decoder function:

// Convert the two byte sensor data format to a signed number
// 
// tInt: the integer portion of the temperature  
// tDec: the fractional portion of the temperature  
function convertTempUnits(tDec, tInt) {
  // the integer portion is a signed two's complement value; convert it
  // to a signed number
  if (tInt > 127) {
    tInt -= 256;
  }
  // the fractional portion of the number is unsigned and represents the 
  // part of the temperature after the base 10 decimal point
  return tInt + (tDec * (tInt < 0 ? -1 : 1) / 100);
}

…and use it with:

// Humidity cannot be negative
var humidity = bytes[2]/100 + bytes[3];
var temperature = convertTempUnits(bytes[4], bytes[5]);

Of course, for use in your function Decoder(Payload), use Payload[2] and the like, rather than bytes[2] which matches the more commonly used function signature of function Decoder(bytes, port).

Also, you might want to validate this code:

The above, <<-shifting byte 4, suggests LSB. However, the order of the bytes in the Laird code suggests MSB:

var backLogMsb  = bArray[idx++];
var backLogLsb  = bArray[idx++];

Finally, just for future reference, see the full JavaScript source for the Laird Node-RED decoder below. I also noticed a funny conversion for the battery in their code:

// if the message contains battery capacity forward it 
// otherwise do nothing
if(typeof msg.batCapacity !== 'undefined')
{
    msg.batCapacity *= 20;
    return msg;
}
Node-RED "payload decoder" node source code (click to expand)
// the raw payload is a base64 encoded version of the binary data sent by the RS1xx sensor
if( msg.payload.payload_raw ){
    
var bArray = Buffer.from(msg.payload.payload_raw, 'base64');
var idx = 0;

// These two values are common across all packets 
var msgType = bArray[idx++];
var options  = bArray[idx++];

  switch(msgType) {
    case 1:  // handle the single temp and humidity reading
        msg.humidity = bArray[idx++] / 100 + bArray[idx++];
        msg.temp     = convertTempUnits(bArray[idx++], bArray[idx++]);
        msg.batCapacity = bArray[idx++];
        break;
    case 2:  // handle the aggregate temp and humidity reading 
        handleMsgType_2( msg );
        break;
    case 5:  // handle the simpleConfig message 
        var batteryType = bArray[idx++];
        var readPeriod = bArray[idx++] * 256 + bArray[idx++];
        var sensorAggregate = bArray[idx++]
        // the aggregate packet does not contain the read interval so we need to get 
        // it from the simple config and save it to the flow context for use later
        flow.set('readPeriod', readPeriod * 1000);
        flow.set('sensorAggregate', sensorAggregate)
        break;
    default:
        break;
  }
  
 return msg;
}

// Convert the two byte sensor data format to a signed number
// 
// tInt: the integer portion of the temp  
// tDec: the fractional portion of the temp  
// 
function convertTempUnits( tDec ,  tInt ){

    // the integer portion is a signed two compliment value convert it to a signed number
    if( tInt > 127 ){
        tInt -= 256
    }

    // the fractional portion of the number is unsigned and represents the part of the temp 
    // after the base 10 decimal point
    let t = tInt + (tDec * Math.sign(tInt) / 100);
    
    // if the global flag for using degreesFahenheit is set convert the units
    if(global.get("degreesFahrenheit")){
        t = t * 1.8 + 32; 
    }
    return t; 
}

// message type two is an aggreegate temperature and humidity reading 
// the timestamp is the time of the last temp / rh reading in the array
function handleMsgType_2( msg ) {

  var alarms      = bArray[idx++];
  var backLogMsb  = bArray[idx++];
  var backLogLsb  = bArray[idx++];
  var batCapacity = bArray[idx++];
  var valueCount  = bArray[idx++];
  var timestamp   = (bArray[idx++] * 256 * 256 * 256)  + (bArray[idx++] * 256 * 256) + (bArray[idx++] * 256) + bArray[idx++];

  var queuedData = [];    // array to hold the data while we are waiting to plot it 

  var temp; 
  var humidity;
  var reading = {};

  // queue the aggregate message temp/rh values so that we can display them over time rather than
  // bursting them all at once 
  for (var ii = 0; ii <  valueCount; ii++){
    let   h    = bArray[idx++] / 100 + bArray[idx++];
    let t = convertTempUnits(bArray[idx++] , bArray[idx++] );
    reading = { humidity: h, temp: t}
    queuedData.push(reading);

  } 
  
  // loop counter for queued messages 
  ii = 0;

  // timer id from the previous setInterval( ) if any
  timerId = flow.get('timerId');
  
  // if we have a current setInterval, free it
  if(timerId){
     clearInterval(timerId);
  }

  // this is the setInterval( ) callback function 
  function  sendData() {
    var msg = {payload:queuedData[ii]};
    if(ii < valueCount)
    {
      msg.humidity = queuedData[ii].humidity;
      msg.temp = queuedData[ii].temp;
      msg.batCapacity = batCapacity;
      node.send(msg);
      ii++;
    }
 }

 // send once as soon as we get data 
 sendData();

 // if we don't have a specified read interval use something 
 var interval = flow.get('readPeriod') || 5000;

 // set up the rest of the data to be sent at the read interval 
 timerId = setInterval(sendData, interval);

 // save the timer id so we can cancel this scheduled repeat when we get more data 
 flow.set('timerId', timerId); 

}

To develop my Decoder algorithm, I based on Node-RED Decoder algorithm. May be I did some mistake, but the values of temperature and humidity that I am seeing in the App Smartphone with bluetooth connection with the RS1xx, it is different of Decoder Output:

  • List item

Payload [5] = 19 conversion => 25 C
Payload[4] = 28 conversion => 0.40C
The result is 25.40C

Sorry, I don’t understand what you’re trying to say. The math in your example is correct; hexadecimal 0x19 equals decimal 25, and 0x28 is decimal 40. But I’ve no idea why you’re posting that. To debug we need the payload and the expected result; please don’t post only half of the details each time!

That said, surely the following is wrong, according to the documentation you linked to, and according to the very different Node-RED code I found:

This basically is the same as:

Param.humidity = (Payload[3] * 256 + Payload[2])/100; 
Param.temperature = (Payload[5] * 256 + Payload[4])/100;

I am trying to explain it is, if the payload is correct, the problem is in the decoder algorithm :slight_smile:

So I took whatever I found earlier in this thread and I cleaned it up. Result below.

// Convert the two byte sensor data format to a signed number
// 
// tInt: the integer portion of the temperature  
// tDec: the fractional portion of the temperature  
function convertTempUnits(tDec, tInt) {
  // the integer portion is a signed two's complement value; convert it
  // to a signed number
  if (tInt > 127) {
    tInt -= 256;
  }
  // the fractional portion of the number is unsigned and represents the 
  // part of the temperature after the base 10 decimal point
  return tInt + (tDec * (tInt < 0 ? -1 : 1) / 100);
}

function asTempAndRh(decoded, bytes) {
  
  decoded.Humidity = bytes[2]/100.0 + bytes[3];
  decoded.Temperature = convertTempUnits(bytes[4], bytes[5]);

  switch( bytes[6] ) {
    case 0:
      decoded.BatteryCapacity = "0-5%";
      break;
    case 1:
      decoded.BatteryCapacity = "5-20%";
      break;
    case 2:
      decoded.BatteryCapacity = "20-40%";
      break;
    case 3:
      decoded.BatteryCapacity = "40-60%";
      break;
    case 4:
      decoded.BatteryCapacity = "60-80%";
      break;
    case 5:
      decoded.BatteryCapacity = "80-100%";
      break;
    default:
      decoded.BatteryCapacity = "This value is not supported";
  }

  decoded.AlarmMsgCount  = ((bytes[7]<<8)>>>0) + bytes[8];
  decoded.BacklogMsgCount = ((bytes[9]<<8)>>>0) + bytes[10];

}

function asTempAndRhAggregate(decoded, bytes) {

  decoded.AlarmMsgCount  = bytes[2];
  decoded.BacklogMsgCount = ((bytes[3]<<8)>>>0) + bytes[4];

  switch( bytes[4] ) {
    case 0:
      decoded.BatteryCapacity = "0-5%";
      break;
    case 1:
      decoded.BatteryCapacity = "5-20%";
      break;
    case 2:
      decoded.BatteryCapacity = "20-40%";
      break;
    case 3:
      decoded.BatteryCapacity = "40-60%";
      break;
    case 4:
      decoded.BatteryCapacity = "60-80%";
      break;
    case 5:
      decoded.BatteryCapacity = "80-100%";
      break;
    default:
      decoded.BatteryCapacity = "This value is not supported";
  }

  var numberOfReadings = bytes[5];

  // 2015-01-01T00:00:00+00:00 = 1420070400
  
  var timestamp = ((bytes[6]<<24)>>>0) + ((bytes[7]<<16)>>>0) + ((bytes[8]<<8)>>>0) + bytes[9];
  timestamp = timestamp + 1420070400; // 1 Jan 2015 to 1 Jan 1970
  //decoded.Time = new Date(timestamp*1000);
  decoded.timestamp = timestamp;

  decoded.readings = [];
  for(var i=0; i<numberOfReadings; i++) {
    var offset = 10 + (i*4);
    var sample = {};

    if(offset+1>bytes.length-1) continue;
    sample['Humidity'] = bytes[offset]/100.0 + bytes[offset+1];
    if(offset+3>bytes.length-1) continue;
    sample['Temperature'] = convertTempUnits(bytes[offset+2], bytes[offset+3]);

    decoded.readings.push(sample);
  }

}

function asBacklogTempAndRh(decoded, bytes) {
  var timestamp = ((bytes[2]<<24)>>>0) + ((bytes[3]<<16)>>>0) + ((bytes[4]<<8)>>>0) + bytes[5];
  timestamp = timestamp + 1420070400; // 1 Jan 2015 to 1 Jan 1970
  //decoded.Time = new Date(timestamp*1000);
  decoded.timestamp = timestamp;

  decoded.Humidity = bytes[6]/100.0 + bytes[7];
  decoded.Temperature = convertTempUnits(bytes[8], bytes[9]);

}

function asBacklogTempAndRhAggregate(decoded, bytes) {
  var numberOfReadings = bytes[2];

  // 2015-01-01T00:00:00+00:00 = 1420070400
  
  decoded.readings = [];
  for(var i=0; i<numberOfReadings; i++) {
    var offset = 3 + (i*8);
    var sample = {};

    if(offset+3>bytes.length-1) continue;
    var timestamp = ((bytes[offset]<<24)>>>0) + ((bytes[offset+1]<<16)>>>0) + ((bytes[offset+2]<<8)>>>0) + bytes[offset+3];
    timestamp = timestamp + 1420070400; // 1 Jan 2015 to 1 Jan 1970
    //decoded.Time = new Date(timestamp*1000);
    sample['timestamp'] = timestamp;

    if(offset+5>bytes.length-1) continue;
    sample['Humidity'] = bytes[offset+4]/100.0 + bytes[offset+5];
    if(offset+7>bytes.length-1) continue;
    sample['Temperature'] = convertTempUnits(bytes[offset+6], bytes[offset+7]);

    decoded.readings.push(sample);
  }
}

function asSensorConfigSimple(decoded, bytes) {
  switch(bytes[2]) {
    case 1:
      decoded.BatteryType = "Zinc-Manganese Dioxide (Alkaline).";
      break;
    case 2:
      decoded.BatteryType = "Lithium/Iron Disulfide (Primary Lithium).";
      break;
    default:
      decoded.BatteryType = "Unknown battery type";
  }
  decoded.ReadSensorPeriod = ((bytes[3] << 8)>>>0) + bytes[4];
  decoded.SensorAggregate = bytes[5];
  decoded.TempAlarmEnabled = (bytes[6]==1);
  decoded.HumidityAlarmEnabled = (bytes[7]==1);
}

function asSensorConfigAdvanced(decoded, bytes) {
  switch(bytes[2]) {
    case 1:
      decoded.BatteryType  = "Zinc-Manganese Dioxide (Alkaline).";
      break;
    case 2:
      decoded.BatteryType  = "Lithium/Iron Disulfide (Primary Lithium).";
      break;
    default:
      decoded.BatteryType  = "Unknown battery type";
  }
  decoded.ReadSensorPeriod = ((bytes[3]<<8)>>>0) + bytes[4];
  decoded.SensorAggregate = bytes[5];
  decoded.TempAlarmsEnabled = (bytes[6]==1);
  decoded.HumidityAlarmsEnabled = (bytes[7]==1);
  decoded.TempAlarmLimitLow = bytes[8];
  decoded.TempAlarmLimitHigh = bytes[9];
  decoded.HumidityAlarmLimitLow = bytes[10];
  decoded.HumidityAlarmLimitHigh = bytes[11];
  decoded.LED_BLE = ((bytes[12]<<8)>>>0) + bytes[13];
  decoded.LED_Heartbeat = ((bytes[14]<<8)>>>0) + bytes[15];
}

function asFwVersion(decoded, bytes) {
  decoded.VersionYear  = bytes[2];
  decoded.VersionMonth = bytes[3];
  decoded.VersionDay   = bytes[4];
  decoded.VersionMajor = bytes[5];
  decoded.VersionMinor = bytes[6];
  decoded.PartNumber   = ((bytes[7]<<24)>>>0) + ((bytes[8]<<16)>>>0) + ((bytes[9]<<8)>>>0) + bytes[10];
}

function Decoder(bytes, port) {
  // Decode an uplink message from a buffer
  // (array) of bytes to an object of fields.
  var decoded = {};

  decoded.MsgType = bytes[0];
  options = bytes[1];

  decoded.Options = "";
  if(options & 0x01) {
    decoded.Options += "Server response is LoRa ack. ";
  }
  if(options & 0x02) {
    decoded.Options += "Inform Server to send UT to sensor in the next downlink transmission. ";
  }
  if(options & 0x04) {
    decoded.Options += "Sensor configuration error. ";
  }
  if(options & 0x08) {
    decoded.Options += "Sensor has alarm condition. ";
  }

  decoded.Options = decoded.Options.trim();
  if(decoded.Options === "") {
    delete decoded.Options;
  }


  switch(decoded.MsgType) {
    case 0x01:
      asTempAndRh(decoded, bytes);
      break;
    case 0x2:
      asTempAndRhAggregate(decoded, bytes);
      break;
    case 0x03:
      asBacklogTempAndRh(decoded, bytes);
      break;
    case 0x04:
      asBacklogTempAndRhAggregate(decoded, bytes);
      break;
    case 0x05:
      asSensorConfigSimple(decoded, bytes);
      break;
    case 0x06:
      asSensorConfigAdvanced(decoded, bytes);
      break;
    case 0x07:
      asFwVersion(decoded, bytes);
      break;
  }

  return decoded;

}

1 Like

It seems (nowadays) It has been confirmed by Laird (see post below) that nowadays the fractional part of temperature readings is signed as well. From another topic:

That implies byte[4] = 0xDB, being either decimal 219 or -37. So, when handling this as an unsigned integer, then this would yield a fractional part of 2.19, which is larger than 1?

According to Laird’s most recent RS1xx LoRa Protocol v2.7, emphasis mine:

Temperature, humidity and voltage values are represented as two or four-byte values, depending upon the range of the connected sensor. The first byte, or pair of bytes, is the fractional portion of the value. The second byte, or pair of bytes, is the integer portion of the value. For example, a temperature of 27.43 degrees C has a fractional portion of 43 and an integer portion of 27. A temperature of -15.87 C would have a fractional portion of -87 and an integer portion of -15. The following are the equations for temperature, humidity and voltage:

  • Temp = Integer Portion + (Fractional Portion/100)
  • Humidity = Integer Portion + (Fractional Portion/100)
  • Voltage = Integer Portion + (Fractional Portion/100)

Note: Temperature and voltage use signed eight and sixteen-bit values, while humidity uses unsigned eight-bit values. However, because humidity cannot be negative, the sign bit is never set and there is no practical difference between using signed or unsigned values for humidity.

The very same document also mentions a signed integer int8_t for the fractional part:

So, despite Laird’s comments in their (old) Node-RED library mentioned in the (old) posts above: given that example payload, I guess that for later versions the fractional part needs sign extension as well? A simple Decoder might then be:

/**
 * Basic decoder for Laird RS1xx Sensors, only supporting message
 * type 0x01 for RS1xx LoRa Protocol v2.7.
 */
function Decoder(bytes, port) {
  var messageType = bytes[0];
  // All Sensor-to-Server messages have the same options byte format.
  // The options byte is always at byte index 1.
  var options = bytes[1];
  var decoded = {
    messageType: messageType,
    // Note: the Data Storage Integration stores nested objects as text
    options: {
      value: options,
      // Show all 8 bits, ensuring leading zeroes
      bits: ('00000000' + options.toString(2)).slice(-8),
      sensorRequestForServerTime: (options & 1<<0) > 0,
      sensorConfigurationError: (options & 1<<1) > 0,
      sensorAlarmFlag: (options & 1<<2) > 0,
      sensorResetFlag: (options & 1<<3) > 0,
      sensorFaultFlag: (options & 1<<4) > 0
    }
  };

  // 0x01 = Send Temp RH Data Notification
  if (decoded.messageType === 0x01) {
    decoded.humidity = bytes[3] + bytes[2]/100;
    // Both the integer and fractional parts are signed.
    // Sign-extend a single byte to 32 bits to make JavaScript understand
    // negative values, by shifting 24 bits to the left, followed by a
    // sign-propagating right shift of the same number of bits.
    decoded.temperature = (bytes[5]<<24>>24) + (bytes[4]<<24>>24)/100;
    decoded.batteryIndex = bytes[6];
    decoded.batteryCapacity = {
      0: '0-5%',
      1: '5-20%',
      2: '20-40%',
      3: '40-60%',
      4: '60-80%',
      5: '80-100%'
    }[decoded.batteryIndex] || 'Unsupported value';
    decoded.alarmMsgCount = bytes[7]<<8 | bytes[8];
    decoded.backlogMsgCount = bytes[9]<<8 | bytes[10];
  }
  
  return decoded;
}

For the above example payload, this would yield -14.37, not something around -17. To be sure, I’d look for some change logs on the Laird website.

@JimK, please report back here!

Sorry for the tardy response. I cheated and used this code Laird RS1xx payload format

It works perfectly. I did however use a calibrated thermometer to check the reading of the Laird unit. And it was about 0.004 of a degree warmer. I’ll take that as a win.

Thank you for your help and pointing me in the right direction. And again sorry for the late response had some family issues to deal with.

Jim

Thanks for reporting back! Still, given the documentation, it’s weird that the following works, even though I initially found that in Laird’s own Node-RED example:

That yields -16.19 for your earlier 0x…DBF2… Whereas:

decoded.temperature = (bytes[5]<<24>>24) + (bytes[4]<<24>>24)/100;

…would yield -14.37 like explained above. So, apparently the fractional part 0xDB should indeed be interpreted as the unsigned 2.19, hence being larger than 1. Weird, but if true, I’m really curious what the encoding looks like (and why it doesn’t match the documentation)…

So, just to be sure: you might want to check that the tests you did for negative readings indeed include relatively large values for bytes[4]. (Both methods would even yield the exact same result when it’s zero, like for 0x…00EF…)

…and for large values of bytes[4] the difference can be as large as 2.54 degrees: -19.55 vs -17.01 for 0x…FFEF…

Ah, the device can also use Cayenne LPP; see Laird’s Application Note - Integrating TTN on Cayenne with RS1xx from its product page. But it may be hard to compare the output as one cannot simultaneously use both formats.

I got a response from Laird. The Node-RED example is outdated. Their documentation now even explains more in section 8 and 9:

Signed 8-bit data

For temperature data, signed 8-bit data is used to allow negative values to be expressed. In this case, both integer and fractional data are returned from the sensor as negative values.

So, @JimK and @jpmeijers the following is correct for “signed 8-bit data” (actually 2 bytes):

decoded.temperature = (bytes[5]<<24>>24) + (bytes[4]<<24>>24)/100;

For “signed 16-bit data” (actually 4 bytes), I’d say this would read:

var i = ...;
decoded.temperature = (bytes[i++]<<24>>16 | bytes[i++]) 
  + (bytes[i++]<<24>>16 | bytes[i++])/100;

Part of Laird’s response:

Please be sure to use the latest LoRa Protocol v2.10 app note uploaded under the ‘Application Note’ tab of the documentation section of the Product Page:- Sentrius RS1xx LoRa-Enabled Sensors | Laird Connectivity is now Ezurio

We have added two new sections, referenced at the relevant points in the document, which help users understand how to decode the 8bit and 16bit data, from the regular temp sensors and the RTD temp sensor respectively.

Apologies for any confusion based on the Node-RED examples, that is quite outdated now and our intention was to take it down, however elements of the documentation were still proving useful for some users, hence we’ve left in place for now.

I’ll either mark as ‘outdated and left for reference’ or remove altogether

There is a new Laird open/close sensor. Here is the part for the above mentioned decoder to get the value:

if (decoded.messageType === 0x09) {
         decoded.stateIndex = bytes[3];
         decoded.stateOpen = {
             0: false,
             1: true
         }[decoded.stateIndex];
}

PLEASE BE ADVISED that linked v2.7 Application Note is actually OLDER than the official v2.12 on their website here: https://www.lairdconnect.com/documentation/application-note-rs1xx-lora-protocol. You need to look at footer of the document to see this.

I found this confusing, too; I made Laird aware of this issue.