Payload format for the sensebox

Best TTN members,

I am having difficulty in finding the correct decoder function for an Arduino based device : the sensebox.The VOID code part where the mesages are constructed for this SAMD24 board in the Arduino IDE is the following:

//-----Temperature-----//
    //-----Humidity-----//
    #ifdef HDC1080_CONNECTED
      DEBUG(F("Temperature: "));
      temperature = HDC.readTemperature();
      DEBUG(temperature);
      message.addUint16((temperature + 18) * 771);
      delay(2000);
      DEBUG(F("Humidity: "));
      humidity = HDC.readHumidity();
      DEBUG(humidity);
      message.addHumidity(humidity);
      delay(2000);
    #endif
  //-----Pressure-----//
    #ifdef BMP280_CONNECTED
      float altitude;
      pressure = BMP.readPressure()/100;
      altitude = BMP.readAltitude(1013.25); //1013.25 = sea level pressure
      DEBUG(F("Pressure: "));
      DEBUG(pressure);
      message.addUint16((pressure - 300) * 81.9187);
      delay(2000);
    #endif

   //-----Lux-----//
    #ifdef TSL45315_CONNECTED
      DEBUG(F("Illuminance: "));
      lux = TSL.readLux();
      DEBUG(lux);
      message.addUint8(lux % 255);
      message.addUint16(lux / 255);
      delay(2000);
    #endif

   //-----UV intensity-----//
    #ifdef VEML6070_CONNECTED
      DEBUG(F("UV: "));
      uv = VEML.getUV();
      DEBUG(uv);
      message.addUint8(uv % 255);
      message.addUint16(uv / 255);
      delay(2000);
    #endif

   //-----PM-----//
    #ifdef SDS011_CONNECTED
      uint8_t attempt = 0;
      while (attempt < 5) {
        bool error = SDS.read(&pm25, &pm10);
        if (!error) {
          DEBUG(F("PM10: "));
          DEBUG(pm10);
          message.addUint16(pm10 * 10);
          DEBUG(F("PM2.5: "));
          DEBUG(pm25);
          message.addUint16(pm25 * 10);
          break;
        }
        attempt++;
      }
    #endif

The datatypes:

#ifdef HDC1080_CONNECTED
  Adafruit_HDC1000 HDC = Adafruit_HDC1000();
  float temperature = 0;
  float humidity = 0;
#endif
#ifdef BMP280_CONNECTED
  Adafruit_BMP280 BMP;
  double pressure;
#endif
#ifdef TSL45315_CONNECTED
  uint32_t lux;
  Makerblog_TSL45315 TSL = Makerblog_TSL45315(TSL45315_TIME_M4);
#endif
#ifdef VEML6070_CONNECTED
  VEML6070 VEML;
  uint16_t uv;
#endif
#ifdef SDS011_CONNECTED
  SDS011 SDS(SDS_UART_PORT);
  float pm10 = 0;
  float pm25 = 0;
#endif 

Could some one give me the javascript function to display the temperature, humidity, lux, pressure, uv, pm10 and pm2.5 values?
As you can see in the code for the temperature and humidity for example, the total 16 bytes of the payload are constructed with some formules and I can not decode the proper values with the right payload format.
I have done several attempts for some days, but to no avail, because I am no programmer at all!
At the end, an HTTP-integration will be made from TTN to OpenSenseMap with a JSON profile.

Thanks for the help and best wishes for 2020.

1 Like

It would help a lot to see (a link to) the full code and the libraries that are used, any documentation of the device itself, and a few example payloads with the expected values, preferably also with negative values when applicable. If those cannot be found then a log of the device, matching those example payloads, helps to see what’s in the program’s variables before the encoding happens. And to help future users of the same device find this topic, also please add some type number?

Also, if you didn’t read it yet, please see Working with bytes.

Lacking documentation, there are quite a few things you need to determine:

  • Are values transmitted MSB or LSB? In code and on paper, a decimal reading such as 300 could be written in the human readable hexadecimal representation 0x012C, denoting the two bytes 0x01 and 0x2C. But one needs to know which byte is transmitted first. (In fact, these two bytes will be transmitted as binary data, being either the 16 bits 0b00000001 00101100, or 0b00101100 00000001. But the order of the bits within each 8 bits byte never needs to be reversed when decoding.)

    Now, lacking any documentation of, e.g., message.addUint16 one cannot tell if the result is MSB or LSB. However, the following snippet gives us a clue:

    message.addUint8(lux % 255);
    message.addUint16(lux / 255);
    

    Above, the name addUint8 suggests that it handles 8 bits, and the % modulo operator gives us the remainder after dividing by 255. So, this shows that the LSB of the variable lux is added first. It’s probably safe to assume that this is transmitted first too, and that addUint16 adds 2 bytes using LSB as well. When comparing to Working with bytes this would roughly be the equivalent of:

    data[0] = lux;
    data[1] = lux >> 8;
    data[2] = lux >> 16;
    

    Unfortunately, this also raises a question: why does this use lux % 255 instead of lux % 256? As 0x012C % 255 yields 0x2D, not 0x2C, this would transmit 0x2D 0x01 instead of 0x2C 0x01. And 0xFF00 / 255 yields 0x100 rather than 0xFF, which would even be transmitted as 0x00.

    That’s a major error in the encoding.

    I’d look for a different example.

  • How are decimal values and negative values transmitted? Names such as addUint16 suggest that only unsigned integer values are sent (even though such method typically really just splits the bytes and transmits the values without doing any interpretation or processing at all).

    So, to handle decimals a value of 30.0 could be multiplied by 10, and be sent as 300, to be divided in the decoder again. To handle negative values, one would add some offset before transmitting it, and then subtract that during decoding. (Aside: sending and decoding signed integer values is not that hard either; you’ll find many examples on the forum.)

    Now, what’s going on with this?

    message.addUint16((temperature + 18) * 771);
    

    An unsigned 16 bits integer ranges from 0 to 65,535 and it seems the author of the encoding wanted to use every available value to get the highest possible precision for some specific range in those 16 bits. Doing the reverse math this suggests a precision of 1/771 ≈ 0.0013 degrees, to encode a range of 0 / 771 - 18 = -18.000 through 65,535 / 771 - 18 = 67.000. That’s either a very expensive and accurate temperature sensor there, or this is a very unrealistic accuracy. Also, there is no special handling of values below -18 or above 67, so what if the sensor returns an unexpected value? Again, I’d look for a different example…

    Still then, this could probably be decoded using:

    function Decoder(bytes, port) {
      var decoded = {};
      // The index in the bytes array that needs to be handled next;
      // use along with "i++" which returns the current value, and
      // then increments it for the next usage
      var i = 0;
    
      // Temperature with 0.0013 precision, -18.000 through 67.000, LSB
      var temperature = (bytes[i++] | bytes[i++]<<8) / 771 - 18;
      // Unary plus operator to cast string result of toFixed to number
      decoded.temperature = +temperature.toFixed(3);
      
      // More magic to be added here
    
      return decoded;
    }
    
  • Next, what does message.addHumidity(humidity) do? Lacking documentation, the only clue we have: if the total payload is 16 bytes, then this apparently uses 2 bytes. Assuming LSB, assuming this is an unsigned 16 bits integer again, and assuming relative humidity (so, 0 through 100%), then using 2 bytes might suggest this has two decimals, so ranges from 0 to 10,000 (well below the maximum of 65,535). Again, this implies an unrealistic accuracy of the sensor. Did I say I’d look for another encoding example? Anyway, this might work:

    // Relative humidity with 0.01 precision, 0.00 through 100.00, LSB
    decoded.humidity = (bytes[i++] | bytes[i++]<<8) / 100;
    

    But beware: maybe the library also tries to use all possible values of a signed integer and multiplies by 655.35 to encode 0 through 100. This might then need:

    // Relative humidity with 0.0015 precision, 0.000 through 100.000, LSB
    decoded.humidity = (bytes[i++] | bytes[i++]<<8) / 655.35;
    
  • Another funny one:

    message.addUint16((pressure - 300) * 81.9187);
    

    Just like the temperature, this suggests a precision of 1/81.9187 ≈ 0.012, and a range from 0 / 81.9187 + 300 = 300.00 through 65,535 / 81.9187 + 300 = 1100.00. Again, unrealistic:

    // Pressure with 0.012 precision, 300.00 through 1100.00, LSB
    var pressure = (bytes[i++] | bytes[i++]<<8) / 81.9187 + 300;
    // Unary plus operator to cast string result of toFixed to number
    decoded.pressure = +pressure.toFixed(2);
    
  • As the library apparently has no built-in support for 24 bits numbers, we already saw the erroneous:

    message.addUint8(lux % 255);
    message.addUint16(lux / 255);
    ...
    message.addUint8(uv % 255);
    message.addUint16(uv / 255);
    

    This seems wrong, but the decoder would read:

    // NOT RECOMMENDED; fix the encoding instead
    decoded.lux = bytes[i++] + 255 * (bytes[i++] | bytes[i++]<<8);
    decoded.uv = bytes[i++] + 255 * (bytes[i++] | bytes[i++]<<8);
    

    I’d either change all occurrences of 255 to 256, or rewrite this to:

    message.addUint8(lux);
    message.addUint16(lux >> 8);
    ...
    message.addUint8(uv);
    message.addUint16(uv >> 8);
    

    …or even:

    message.addUint8(lux);
    message.addUint8(lux >> 8);
    message.addUint8(lux >> 16);
    ...
    message.addUint8(uv);
    message.addUint8(uv >> 8);
    message.addUint8(uv >> 16);
    

    Whatever fix you choose, all should then decode just fine with:

    // 24 bits unsigned integer, 0 through 16,777,215, LSB
    decoded.lux = bytes[i++] | bytes[i++]<<8 | bytes[i++]<<16;
    decoded.uv = bytes[i++] | bytes[i++]<<8 | bytes[i++]<<16;
    
  • And finally something I guess I can agree with, if these are low-concentration sensors:

    message.addUint16(pm10 * 10);
    ...
    message.addUint16(pm25 * 10);
    

    …which decodes using:

    // Particulate matter with 0.1 precision, 0.0 through 6,553.5, LSB
    decoded.pm10 = (bytes[i++] | bytes[i++]<<8) / 10;
    decoded.pm25 = (bytes[i++] | bytes[i++]<<8) / 10;
    
  • Of course, seeing some repeated code, one could define a helper function in the Payload Format in TTN Console:

    /**
     * Convert the array of bytes to an unsigned integer, LSB. 
     *
     * BEWARE: This is only safe up to 0x1FFFFFFFFFFFFF, so: 6 bytes.
     */
    function uint(bytes) {
      return bytes.reduceRight(function(acc, b) {
        // We expect an unsigned value, so to support more than 3 bytes
        // don't use any bitwise operators, which would always yield a
        // signed 32 bits integer instead.
        return acc * 0x100 + b;
      }, 0);
    }
    

    …along with:

    function Decoder(bytes, port) {
      var decoded = {};
      // The index in the bytes array that needs to be handled next;
      // use along with "i++" which returns the current value, and
      // then increments it for the next usage
      var i = 0;
    
      // Temperature with 0.0013 precision, -18.000 through 67.000, LSB
      var temperature = uint(bytes.slice(i, i+=2)) / 771 - 18;
      // Unary plus operator to cast string result of toFixed to number
      decoded.temperature = +temperature.toFixed(3);
      // Relative humidity with 0.01 precision, 0.00 through 100.00, LSB
      decoded.humidity = uint(bytes.slice(i, i+=2)) / 100;
      // Pressure with 0.012 precision, 300.00 through 1100.00, LSB
      var pressure = uint(bytes.slice(i, i+=2)) / 81.9187 + 300;
      decoded.pressure = +pressure.toFixed(2);
      decoded.lux = uint(bytes.slice(i, i+=3));
      decoded.uv = uint(bytes.slice(i, i+=3));
      decoded.pm10 = uint(bytes.slice(i, i+=2)) / 10;
      decoded.pm25 = uint(bytes.slice(i, i+=2)) / 10;
      return decoded;
    }
    

Lacking example payloads: all untested. Even if the above works for you, I’d still look for another example. Also, please report the error about using 255 instead of 256 to the original author.

2 Likes

Dear Arjan,
we were expecting you to formulate such an extensive and professional answer …

My answers to your questions on the other hand are less significant and technical, but I can give you the links to all the necessary repositories on Github/sensebox. For sure, there you will find an answer for all of your uncertainties.

The first repository I would like to mention is the one from the MCU vendor itself.
Here you will find plenty of Arduino examples of all the libraries. I am sure you will find your answer throughout the code examples here, to find out if it is MSB or LSB for the Adafruit and other sensors.


The only thing I know is that in the LMIC library the AppEUI, DevEUI are LSB and the AppKey is MSB, no clue about the other Arduino libraries.

The only decoder I could find is indeed for the PM sensor data as I discovered by accident that these values were in the 12th and 14th byte. The results of only these two values are now displayed with this simplistic decoder function after reading https://www.thethingsnetwork.org/docs/devices/bytes.html

function Decoder(bytes) {
var decoded = {};
  var PM10 = (bytes[12]);
  var PM25 = (bytes[14]);
  decoded.pm10 = PM10/10;
  decoded.pm25= PM25/10
  return decoded;
}

But of course the decoding of the other values (temperature, humidity, lux, uv and pressure) are much more complex as you pointed it out yourself, even with coding errors as you discovered. I guess you will find the explanation/argumentation for this on the author’s Github repositories from above.

In any case, I will try this weekend your suggested helper functions and see if I can send the decoded values to the OSeM platform for displaying them correctly.
If you find the time after pouring around in the repositories of the Sensebox project to develop a function, I would appreciate a lot, as this one is not that simple. Luckily, it is all open source and with the support of this TTN forum, we will manage.

Sorry, that’s quite a huge repository; I’m not going to browse it to see if I happen to find the code you’re using. In fact, I couldn’t quickly find your example code as it’s a fork and GitHub doesn’t support code search in forks. Where did you get that lux % 255 from?

But to be sure: the MSB and LSB that you need to know about (and which I think is LSB; see my previous answer) is that of the addUint16 and addHumidity functions. It won’t be different for each sensor, as in the code you have proper types already, like float, double or uint16_t, which is all handled automatically for you. It’s only when encoding them for the LoRa transmission that you need to know explicitly what bit order is used. (Again: I’m quite sure it’s LSB.)

Above you’re only using 8 of the 16 bits transmitted for each value. So this will only be correct for small values, where the MSB values in byte[13] and in byte[15] happen to be 0x00.

Oh, I just noticed a difference for the types of uv and lux:

So, unsigned 32 bits vs 16 bits. That’s weird, as your encoding handles both as 8 + 16 = 24 bits:

Above, the MSB of the 32 bits lux is ignored (so, hopefully is always 0x00), while for uv the MSB for message.addUint16(uv / 255) will always be 0x00 as uv is only 16 bits to start with…

Maybe you’d better use:

message.addUint16(lux);
message.addUint16(lux >> 16);
...
message.addUint16(uv);

…along with:

// 32 bits unsigned integer, LSB
decoded.lux = uint(bytes.slice(i, i+=4));
// 16 bits unsigned integer, LSB
decoded.uv = uint(bytes.slice(i, i+=2));

Messy!

Sure it is messy Arjan. And I do understand it is a whole job to browse their extensive GitHub.

There is a script generator on opensensemap (when you have an account) which gave me that 255 instead of the 256.

Uptil now I have managed to decode the DHT22 and SDS011 sensors values. I left out the other ones and with this decoder function, I succeeded to decode the right values.
The weird thing is, when I point to the values for the PM data with that loop you proposed; I get the values of 4096 for PM10 and 102.4 for PM25. When I point directly to the byte position 12 and 14, I get the right ones.

  var i = 0;
  var PM10 = (bytes[12]);
  var PM25 = (bytes[14]);
  var temperature = (bytes[i++] | bytes[i++]<<8) / 771 - 18;
  var humidity =  ((bytes[i++] | bytes[i++]<<8) / 100);
  //var pm10 = (bytes[i++] | bytes[i++]<<8) / 10;
  //var pm25 = (bytes[i++] | bytes[i++]<<8) / 10;
 
  
  var decoded = {};
  decoded.temperature = +temperature.toFixed(1);
  decoded.humidity = +humidity.toFixed(1); 
  //decoded.pm10 = +pm10.toFixed(2);
  //decoded.pm25 = +pm25.toFixed(2); 
  decoded.pm10 = PM10/10;
  decoded.pm25 = PM25/10;
  return decoded;

We keep on working on it this weekend. In any case, thanks a lot for your contribution and we are looking out for other reactions from TTN members who are as acquainted as you are in javascript coding.

The order matters: the counter i will be zero at the start, and only after handling temperature, humidity, pressure, lux, all through UV, it will eventually become 12. So, decoding in TTN Console must be done in the same order as the encoding in the device, and must decode all values, when using such counter. Just like in the last bullet of my first answer. :slight_smile:

Also, I’ve created:

Finally the author of the sensebox found a correct payload format after the pull request created by @arjanvanb He also admitted that for the UV and lux values, a shift change with %255 was not the correct one. Thanks Arjan!

The results of the HTTP integration can be seen here
https://opensensemap.org/explore/5e10d6b0b0c088001bf22393

And the full sketch proposed by the author for the sensebox is published at Github as well.