How to efficiently assemble and extract a mix of integer and float data for TTN

This topic is a solution for an issue I had with developing a temperature and water level monitoring system. I had a mix of float and integer values I wanted to put into one packet and then pull out on The Things Network.

LoRaWAN gets a major advantage in range by keeping the packet size and frequency down. Typically we can use about 50 bytes and a better way to use this restricted packet size is to assemble the various pieces of data into a structure which can then be sent as a stream of bytes and then have the data extracted. Sometimes people try sending the data as text however this is inefficient/frowned upon and sometimes by using offsets and various multipliers which is messy and error prone. It is much better to simply use the correct data type and pack the data into a structure. Here is how to do that.

Data is sent to TTN as a packet of bytes. If we want to send multiple pieces of information then this information needs to be assembled into a sequence and then sent. When received the packet needs to be broken up into the multiple pieces and decoded.

One way to do this assembly process is to use a union structure type of data. This way the numbers can automatically share the same memory as a byte sequence. This means that as we store the values to the numeric variables to a data structure, the data will be automatically be stored into the bytes comprising the packet of data. In the example below we wish to store two floats and then an integer. As each float is four bytes long and the integer is two bytes long the total packet will be ten bytes long.

Solution:
Part 1: At the node
In this case I was using an Arduino Uno with a Dragino Lora Shield however this is only to support why the following code is C, which is pretty standard on an Arduino platform. I was also using the LMiC library which has been modified for use in Australia.

In this case I had the temperature and humidity, both in float format, which had been measured by a DHT22 sensor. I also had a water level as a 2 byte integer, which was measured at the time by an unreliable ultrasonic sensor (Hint: Use a LIDAR module instead).

The aim of the following code is to have a structure where the 3 values can be easily inserted in their native format and then also easily sent as an array of bytes in the LoRa packet. No weird scaling or offsets required. And no evil text strings.

The following piece of code goes at the start of your program in the global defines area

// do stuff for assembling data packet
//start by building a structure to hold the two floats and one integer

typedef struct sensorData {
  float temperature;
  float humidity;
  int16_t level;
};

#define PACKET_SIZE sizeof(sensorData)

typedef union LoRa_Packet {
  sensorData sensor;
  byte LoRaPacketBytes[PACKET_SIZE];
};

//now create a variable called levelinfo to hold the data
LoRa_Packet levelinfo;

The union function allows data in the same memory range to be accessed by two different methods.

In the code below I first collect the data with my own routines, your methods will most likely be different.
I then load the values into the appropriate part of the structure.
And then the data is shoved into the LMIC library routine for sending .

//get the data values to send, caution: poor mixed coding practice for the next two lines
GetTemp(); //global values 't' for temp and 'h' for humidity as floats. (4 bytes)
int16_t tLevel = tank_depth - GetDistance(); //read water level (2 bytes)

// first put the individual numbers into the correct part of the structure
levelinfo.sensor.level = tLevel;
levelinfo.sensor.temperature = t;
levelinfo.sensor.humidity = h;

//we'll be sending the data levelinfo.LoRaPacketBytes of length 10

// Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(1, levelinfo.LoRaPacketBytes, sizeof(levelinfo.LoRaPacketBytes), 0);

Note that in the Arduino float data type is stored in little endian format. We have to take this into account when we convert the data from hexadecimal back to float where the data is consumed.

Part 2: At the The Things Network
At this stage and ignoring a fair amount of magic of actually getting LoRaWAN to work we will have a packet of 10 bytes, in this particular case, of data sitting up on TTN.
So, how do we extract this data back into the numbers we would like?

Below is the JavaScript code for the decoder in the ‘Payload Formats’ tab of the Application page for the device which is sending the data.
What we basically need to do extra the four or two bytes in the correct sequence and convert them back into the float and integer values. In the case of floats we also have to handle negative numbers which makes life a little more complicated.
(Thanks to Brady Aiello on TTN/GitHub for the Bytes2Float32 function)

function Bytes2Float32(bytes) {
    var sign = (bytes & 0x80000000) ? -1 : 1;
    var exponent = ((bytes >> 23) & 0xFF) - 127;
    var significand = (bytes & ~(-1 << 23));

    if (exponent == 128) 
        return sign * ((significand) ? Number.NaN : Number.POSITIVE_INFINITY);

    if (exponent == -127) {
        if (significand == 0) return sign * 0.0;
        exponent = -126;
        significand /= (1 << 22);
    } else significand = (significand | (1 << 23)) / (1 << 23);

    return sign * significand * Math.pow(2, exponent);
}

function Decoder(bytes, port) {
	var t = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
	var h = bytes[7] << 24 | bytes[6] << 16 | bytes[5] << 8 | bytes[4];
	var lvl = bytes[9] << 8 | bytes [8];
	return{
		Temp:  Bytes2Float32(t),
		Hum: Bytes2Float32(h),
		Level: lvl
	};
}

With t=22.2, h=55.5 and lvl=17 (cm)
Little endian will be ‘9A 99 B1 41 00 00 5E 42 11 00’ :
‘9A 99 B1 41’ because this is 22.2 as a 4 byte float on a little endian processor
‘00 00 5E 42’ because this is 55.5 as a 4 byte float on a little endian processor
‘11 00’ and this is 17 as a 2 byte integer.

These three numbers will now be visible in the data output tab of the application page.

Using this method you can nicely put together a mix of up to 24 integers and/or up to 12 floats with a few bytes thrown in for good measure just using a single packet.

5 Likes

Nice. Just a heads up when changing hardware:

As an aside, one could also move the bit shifting into the helper function and use bytes.slice(0, 4) to pass 4 bytes rather than a single number. And one might want to limit the number of decimals. See Decode float sent by Lopy as Node - #2 by arjanvanb.

Yep agreed with the byte order. However,owner effort in finding the byte order is better than using a bad algorithm. This was specifically for Arduino. YMMV with other processors.
And the bytes to float, yep, another way.
The main point of the thread was to show how to pack different data types, something I had trouble finding an example of when using TTN.

1 Like

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.