LoRa Message Format

Version 1.3
Date: December 28, 2019
(c) M. Westenberg (2016-2019)


This page describes the format of messages exchanged between the various LoRa sensors and their Gateway. In today's world we often use JSON messages to code complex structure into readible format and exchange these messages over the internet. However, LoRa has different requirements. As LoRa nodes are not supposed to talk more than 1% of the time, sending messages as short as possible makes sense.

So where we normally would send the temperature like this:

{ "temperature": "20.41" }

it makes sense to shorten that message as much as possble so we can send more messages in the available 1% of time. So a better way would be:


And assuming we know that it is the temperature that we're receiving in 2 integers and a fraction of 2 digit, all would fit in 2 bytes:

"0x14 0x29"

However, such shortcuts only work it both sender and receiver know what's coming and they will not send other messages for other sensor values. Because if they do, there is a need to make these messages a little more descriptive, yet as short as possible.


Sensor message Format

Although heavily discouraged by the community it is safe to assume that some messages will be JSON still, but preferrable we should code our messages such that they contain as less space as possible.

Header Sensor values [1:n]
Start bit==1 Length of message including header Parity bit Opcode byte Value Byte(s) [1:n]

The parity bit is set after all sensors values are added to the message. We use even parity (in other words: the sum of all 1-bits in the message must be even). As LoRa itself uses error checking algorithms we decided not to add any more to the message by implementing a more sophysticated error checking and correcting scheme.

The Sensor Values are encoded as follows:

OpCode Byte Value Bytes [1:n}
6-bit ID 2-bit Length 1-4 byte value *

(*) Some OpCodes such as GPS and MultiButton use more than 4 bytes to encode their value. In this case, the length field is not used (don't care) and the number of value bytes is defined completely by the opCode.

Based on this format he following message types (Id's) are defined:

Sensor Id 6-bit Len 2-bit Encoding
Temperature (-100-150 degrees C) 0b0000 01 0b01 (+1)

byte[0] = INT(t +100); byte[1]=2 digit fraction (00-99)

result = (byte[0] - 100) + (byte[1] / 100)

Humidity (0-100%) 0b0000 10 0b00 (+1)

byte[0] = INT(Humidity * 2)

result = byte[0] / 2 (as %)

Airpressure (800-1104 hPa) 0b0000 11 0b00 (+1)

byte[0] = (Airpressure in hPa - 850)

result = byte[0] + 850

GPS Short Info 0b0001 00 d.c. byte[0-2] = LAT, byte[3-5]=LNG
GPS Long Info 0b0001 01 d.c. byte[0-3] = LAT,
byte[4-7] = LNG,
byte[12-15] =UTime,
byte[16] = nr of satellites
PIR 0b0001 10 0b00 (+1)

byte[0]=0x00 off, 0x01 armed, 0x02 on, 0x03 sent

result = byte[0]

AirQuality 0b0001 11 0b01 (+1)

byte[0-1] = AQ (most signicicant byte first)

result = (byte[0] *256) + byte[1]

Real-Time Clock (RTC) 0b0010 00 0b11 (+1) byte[0-[3] unsigned long value of secs since 1-1-1970
Compass Sensor (X,Y,Z) 0b0010 01   t.b.d.
Multi-Button 0b0010 10 d.c. byte[0-3] = Address
byte[4-5] = Unit
total 6 bytes: 4 bytes address and 2 byte unit code of button code activated
Moisture 0b0010 11 0b00 byte[0] = M / 4;
One Byte moisture (value 0-1023) / 4.  255 is dry, 0 is super wet.
Luminescense 0b0011 00 0b01

Two bytes with luminescense code. 0 is dark, 3999 is bright sunlight.

result = (byte[0]*256)+byte[1]) / 10 Lux

Distance 0b001101 0b01

byte[0-1]=Distance 0-65,535

result = (byte[0] * 256) + byte[1] cm

Battery condition (0-12 V) 0b1000 00 0b00 (+1)

byte[0] = INT (V * 20)

result = byte[0] / 20 Volt

AD converter A0 (256 steps) 0b1000 01 0b00  
AD converter A1 (256 steps) 0b1000 02 0b00  
User Codes      
General Integer 0b1000 01 0b00,
uint8_t (1 byte),
uint16_t (2 bytes),
uint32_t (4 bytes)
General Character String 0b1000 10 0b00 byte[0] defines length of char string. Note: Total message size <128 chars
Downlink Codes      
Status 0b1100 00 0b00 Ask for status of the node
SF 0b1100 01 0b00 One byte is spreading factor 0x07 - 0x0C
Timing for messages 0b1100 10 0b01 2 bytes timing in seconds
Other ID's are free to use      



General notes

Where it makes sense, the two last bits of the opcode byte are used to express the number of bytes that represent the sensor value and that are following the opcode byte. For example: For humidity we use one byte to encode the sensor value. Therefore the last two bits of the opcode byte are 0x00 (+1 = 1 byte).

For some sensors such as GPS we need more information than just the 3 bytes that can be coded in 2 bits. As we use fixed amount of fields and bytes to encode the GPS coordinates, this does not matter. So for these devices the amount of bytes needed is found in the table above.



This section contains a few examples that show how the lCode library encodes (and decodes) the various message formats.

Example 1

Imagine a simple sensor transmitting a battery value of 3.2Volt (float). The message payload sent by LoRa is coded in three bytes as follows:
0x87 0x80 0x40 which is in binary format: 1000 0111 1000 0000 0100 0000

The start byte 0x87 consists of

The second opcode byte 0x80 has binary value 1000 0000, consisting of a command id of 100000 (O_BAT) and two length bits that are zero (0x00). As the length bits are zero this means that one byte value will follow for this sensor value.

The third byte with value 0x40 has decimal value 64. As the battery value was multiplied by 20 it means that the battery value measured was 3.2V.


Example 2, Downlink messages

This second example handles downlink messages. Just like uplink messages that are sent by the node containing sensor data, downlink messages contain commands and information from the LoRa server (/router).

1. Ask for node status: 0x84 0xC0  computed as follows: ( 0x80 | 0x02<< 1 )  ; ( 0x03<<2 | 0x00 )

2. Set the Spreading Factor to 7: 0x86 0xC4 0x07

3. Set the timing between messages to 32 seconds: 0x88 0xC8 0x00 0x20



The following opcodes are defined both for the node (IDE library) and for the backend:

	#define O_TEMP		0x01 	// Temperature is a one-byte code
   #define O_HUMI		0x02 	// Humidity is a one-byte code
   #define O_AIRP		0x03 	// Air pressure is a one-byte code
   #define O_GPS		0x04  	// Short version: ONLY 3 bytes LAT and 3 bytes LONG
   #define O_GPSL		0x05 	// Long GPS
   #define O_PIR		0x06  	// Movement, 1 bit (=1 byte)
   #define O_AQ		0x07   	// Airquality
   #define O_RTC		0x08  	// Real Time Clock
   #define O_COMPASS	0x09	// Compass
   #define O_MB		0x0A	// Multi Sensors 433
   #define O_MOIST 	0x0B	// Moisture	is one-byte
   #define O_LUMI  	0x0C 	// Luminescense u16
   #define O_DIST		0x0D 	// Distance is 2-byte
	#define O_GAS		0x0E  	// GAS
/* 0x10 to 0x1F are free */
	#define O_BATT		0x20	// Internal Battery
   #define O_ADC0		0x21	// AD converter on pin 0
 	#define O_ADC1		0x22
// Reserved for LoRa messages (especially downstream)
   #define O_STAT		0x30	// Ask for status message from node
   #define O_SF		0x31	// Spreading factor change OFF=0, values 7-12
   #define O_TIM		0x32	// Timing of the wait cyclus (20 to 7200 seconds)
   #define O_1CH		0x33	// Single channel ON=1, OFF==0
   #define O_LOC		0x34	// Ask for the location. Responds with GPS location (if available)


Encoding/Decoding Library (C++)

For encoding sensor values in the LoRa payload, a simple set of library functions is developed that make adding values to the payload very easy. The libary LoRaCode contains 2 files: LoRaCode.h and LoRaCode.cpp.

The following encoding functions are present in the Arduino LoRaCode library: (LoRaCode.h):

int eTemperature(float val, byte *msg);
int eHumidity(float val, byte *msg);
int eAirpressure(float val, byte *msg);
int eGps(double lat, double lng, byte *msg);
int eGpsL(double lat, double lng, long alt, int sat, byte *msg);
int ePir(int val, byte *msg);
int eAirquality(int val, byte *msg);
int eRtc

int eMbuttons(byte val, unsigned long address, unsigned short channel, byte *msg);
int eMoist(int val, byte *msg);
int eLuminescense(int val, byte *msg);
int eBattery(float val, byte *msg);

Downlink messages are (de-)coded as follows:

int LoRaCode::dMsg (byte *msg, byte *val, byte *mode) {}


Also, two supporting functions are defined. eMsg will analyse the message buffer after all sensors are added. It will compute the total length of the buffer and add parity bit when necessary. The lPrint function will print the buffer in hexadecimal format.

bool eMsg(byte *msg, int len);
void lPrint(byte *msg, int len);

Decoding functions (Javascript)

Decoding can be done for every language. We chose to decode in Node.js (JavaScript) which makes adding fields to an MQTT message very easy. Once all fields are decoded the resulting object can be sent with MQTT without problems (as a JSON object). Based on the node.js library it is easy to port this library to other languages.

The dCode function in the lCode module will take care of the decoding of the lCode messages. The result returned by the dCode function is an object with fields that represent the sensor values that were present in the message and successfully decoded. The following fields can be added to the object (only the fields that are decoded will be present):

var LoRaMsg = require(par.homeDir+'/modules/lCode');

var sensorData = LoRaMsg.dCode(DevAddr,dataRaw);

// sensorData has several field after LoRaMsg.dCode() has finished
sensorData = {
   temperature: <value>,
   humidity: <value>,
   airpressure: <value>,
   gps: {
        lat: <value>,
        lng: <value>
   gps = {
     lat: <value>,

     lng: <value>,
     alt: <value>,
     sat: <value>
   pir: <value>,
   airquality: <value>,
   button: <value>,
      b_addr: <value>,       // Only present for multi-button concentrator
      b_unit: <value> ,      // idem

   moist: <value>,
   luminescense: <value>,

   battery: <value>


Although using this lCode message format does not result in the shortest messages one can send over LoRa, the overhead of one length byte per message and one ID byte per sensor value is low compared to the full message sent over the air (containing all sorts of header and address data as well).

So therefore we will use this message format in most our Arduion/ESP8266 based sensor nodes as well as in our own node.js backend.