Packet Handler

Packet Structure

I have used a formal packet structure in LoRa transmissions to help provide a level of data integrity. Apart from incorporating specific sender/receiver MAC addresses, the packet header includes a checksum that is calculated over the packet payload. While it is a very simplistic approach, this seems to adequately deal with transmission collisions—a collision invariably results in a badly formed packet, which is subsequently rejected by the gateway either because the MAC addresses have been corrupted and are thus not recognised or because the payload checksum is invalid. To date, there has been enough variation in the timings of the individual processors for me not to have had to worry about any formal 'back-off' process to avoid repeated collisions—my current network comprises 8 Nodes, each transmitting every 60 seconds, and I rarely see a bad packet.

I realise that the Semtech LoRa chips include a checksumming capability in hardware and that this may be far more efficient than the software checksumming process I am currently using but, at the time I was initially developing a packet structure, I didn't really understand how this worked. I am yet to revisit this subject to assess whether or not the present packet checksum adds anything to the overall process so, for the time being, it remains.

I also recognise that much of this, as well as 'reliable' packet delivery, is handled by the WAN part of LoRaWAN and it has always been my intention to look at migrating my system to a LoRaWAN configuration at some point, although I do not ever want my network to become part of any wider network—I am happy for it to be an 'isolated' Intranet of Things. However, since the present arrangement provides a consistently reliable level of connectivity, my current efforts are focused on refining other elements of my set-up.

The basic packet [header] structure that I have used is illustrated below and described in the following file.

Packet Structure
h Langlo Packet Structure.docx [92 KB]

The latest implementation of this packet structure includes additional fields, REL and ACK flags to support the implementation of a basic form of reliable packet delivery (See below).

Data Elements

LoRa messages are ultimately transmitted as a stream of bytes. In the present case, union datatypes have been used to superimpose a formal structure on this byte stream. This greatly simplifies the task of assembling and reading byte streams comprising more complex data structures.

The basic element of the union that defines the packet in the present context is a simple array of bytes (specified as uint8_t). This is the data element that is referenced when sending or receiving a packet. The packet structure, the struct packetContent element, is then superimposed on top of this byte stream to simplify the storage and retrieval of the packet contents.

A second union, genericPayload, superimposes a variable structure on the packet payload, allowing the payload to be populated according to the requirements of individual sensors or applications.

A third union, involving the individual data elements associated with the payloads of individual packet types (e.g. powerPayload, voltagePayload etc.), provides access to the specific data elements of a given packet payload.

The structure of the individual packet data elements was originally described in the Langlo Packets.h file illustrated below and included in the Langlo.zip archive referenced elsewhere.

Langlo Packet Definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
210
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Langlo LoRa packet structure

const int PH_PacketSize = 64;
const int PH_HeaderSize = 16;
const int PH_PayloadSize = 48;// PH_PacketSize - PH_HeaderSize

union dataPacket {
uint8_t packetByte[PH_PacketSize];
struct packetContent {
uint32_t destinationMAC;// 4 bytes
uint32_t sourceMAC;// 4 bytes
uint32_t checksum;// 4 bytes
uint16_t sequenceNumber;// 2 bytes
uint8_t type;// 1 byte
uint8_t byteCount;// 1 byte
uint8_t payload[PH_PayloadSize];// max 48 bytes
} content;
};

const int powerTypePacket= 0x00;
const int voltageTypePacket= 0x01;
const int tankTypePacket= 0x11;
const int pumpTypePacket= 0x12;
const int weatherTypePacket= 0x20;
const int atmosphereTypePacket= 0x21;
const int temperatureTypePacket= 0x27;
const int rainfallTypePacket= 0x22;
const int windTypePacket= 0x23;
const int voxTypePacket= 0x24;
const int lightTypePacket= 0x25;
const int uvTypePacket= 0x26;
const int sprinklerTypePacket= 0x30;
const int gpsTypePacket= 0x40;
const int awtsTypePacket= 0x50;

const int powerPayloadBytes= 0x0C;
const int voltagePayloadBytes= 0x02;
const int tankPayloadBytes= 0x02;
const int pumpPayloadBytes= 0x01;
const int weatherPayloadBytes= 0x0B;
const int atmospherePayloadBytes= 0x06;
const int temperaturePayloadBytes= 0x02;
const int rainfallPayloadBytes= 0x02;
const int windPayloadBytes= 0x04;
const int voxPayloadBytes= 0x02;
const int lightPayloadBytes= 0x02;
const int uvPayloadBytes= 0x06;
const int sprinklerPayloadBytes= 0x30;
const int gpsPayloadBytes= 0x10;
const int awtsPayloadBytes= 0x04;
const int defaultPayloadBytes= 0x30;

// Power Type

union powerPayload {
uint8_t payloadByte[12];
struct payloadContent {
uint16_t voltage;
uint16_t current;
} battery, panel, load;
};

// Voltage Type

union voltagePayload {
uint8_t payloadByte[2];
struct payloadContent {
uint16_t voltage;
} source;
};

// Tank Type

union tankPayload {
uint8_t payloadByte[2];
struct payloadContent {
uint16_t level;// struct will allow flexibility to add more parameters
} tank;
};

// Pump Type

union pumpPayload {
uint8_t payloadByte[1];
struct payloadContent {
uint8_t on;// struct will allow flexibility to add more parameters
} power;
};

// Weather Type

union weatherPayload {
uint8_t payloadByte[11];
struct payloadContent {
int16_t temperature;
uint16_t pressure;
uint16_t humidity;
uint16_t rainfall;
uint16_t windBearing;
uint8_t windSpeed;
} recorded;
};

// Atmosphere Type

union atmospherePayload {
uint8_t payloadByte[6];
struct payloadContent {
int16_t temperature;
uint16_t pressure;
uint16_t humidity;
} recorded;
};

// Temperature Type

union temperaturePayload {
uint8_t payloadByte[2];
struct payloadContent {
int16_t temperature;
} recorded;
};

// Rainfall Type

union rainfallPayload {
uint8_t payloadByte[2];
struct payloadContent {
uint16_t rainfall;
} recorded;
};

// Wind Type

union windPayload {
uint8_t payloadByte[4];
struct payloadContent {
uint16_t windBearing;
uint16_t windSpeed;
} recorded;
};

// VOX Type

union voxPayload {
uint8_t payloadByte[2];
struct payloadContent {
uint16_t voxLevel;
} recorded;
};

// Light Type

union lightPayload {
uint8_t payloadByte[2];
struct payloadContent {
uint16_t ambientLight;
} recorded;
};

// UV Type

union uvPayload {
uint8_t payloadByte[6];
struct payloadContent {
uint16_t uvA;
uint16_t uvB;
uint16_t uvIndex;
} recorded;
};

// Controller Type

union controllerPayload {
uint8_t payloadByte[PH_PayloadSize];// No structure defined at this time
};

// GPS Type

union gpsPayload {
uint8_t payloadByte[16];
struct payloadContent {
float latitude;
float longitude;
float altitude;
float hdop;
} position;
};

// AWTS Type

union awtsPayload {
uint8_t payloadByte[4];
struct payloadContent {
int16_t blowerPressure;
uint16_t itLevel;
} recorded;
};

// Generic Type

union genericPayload {
uint8_t payload[PH_PayloadSize];
powerPayload power;
voltagePayload voltage;
tankPayload tank;
pumpPayload pump;
weatherPayload weather;
atmospherePayload atmosphere;
temperaturePayload temperature;
rainfallPayload rainfall;
windPayload wind;
voxPayload vox;
lightPayload light;
uvPayload uv;
controllerPayload controller;
gpsPayload gps;
awtsPayload awts;
};

More recently, I have moved all of these details into a library containing everything to do with the management of packet assembly and transmission (see below). Please note that the organisation of packet structures—the range and identification of individual payload types—has expanded since the original implementation, with some associated changes to data values and/or elements, but the principle and actual structures remain the same.

I am also aware that the IoT community encourages brevity in communications and that I am not adhering to that principle very well at all with the current packet structure—my minimum information unit is generally one byte, or 8 bits, when a single bit is all that is required in some situations. I have also seen JSON-based data structures that are probably more elegant and/or flexible in general IoT applications. Once again I will simply say that this is a pilot system, established to verify the viability of what I am trying to do. Optimisation of the communications protocol, which may involve migrating to LoRaWAN and/or the use of a more economical data format, is something I'll get on to when all the more critical issues are under control.

Packet Buffer

While my initial applications, essentially just monitoring the status of various sensors, had no real need of a reliable packet delivery service, the control of a pump was more critical. The commercial system that I was using had failed on a couple of occasions, with the result that a pump kept running when it shouldn't have and emptied one of my tanks. This was not a major problem at the time, but it could have been if we were in drought.

In implementing a basic reliable packet delivery mechanism then, my primary goal was to be able to reliably identify when a pump was or was not running, regardless of whether or not it was meant to be. More generally, however, I was faced with two primary issues in relation to the reliable delivery of packets.

The first was that I would need to save any packet that required acknowledgement, in case it needed to be resent, until acknowledgement had been received.

The second was the question of how to manage multiple reliable streams in parallel. If multiple streams were to be managed, there was the potential need to retain copies of an arbitrary number of packets until their receipt had been acknowledged. I would then also need some practical means of checking off acknowledged packets so that I could delete the copy that had been retained.

These are not really issues for an end Node, which will most likely only ever be managing a single 'conversation' [with a Gateway Node], but a Gateway Node may be managing 'conversations' with several end Nodes and may thus well need to be concurrently keeping track of several exchanges.

In the event, I have implemented a buffer arrangement using a linked list managed through Ivan Seidel's LinkedList library.

Packets that require reliable delivery are stored in the sender's buffer list until receipt has been acknowledged, or until the packet resend limit has been exceeded. Up to three attempts are currently made, at 0.5 second intervals, to send a packet that requires acknowledgement (both the maximum number of resend attempts and the resend interval are configurable). On receipt of an acknowledgement, or on exceeding the resend limit, the sender locates the relevant packet in the buffer list and deletes it.

If the resend limit has been exceeded with no acknowledgement of packet receipt, the sending Node must take appropriate action—for the Gateway, this will usually be to either notify the MQTT broker, if the original packet was generated in response to an MQTT message, or perform some default action. Similarly, there would need to be some fallback option if the packet originated at a Node that was seeking further instruction in response to some change in local conditions.

My original packet structure has been extended to support a very simple implementation of reliable delivery by using the top bit (REL) of the packet Type field to indicate whether or not the sender requires acknowledgement of receipt, and the top bit (ACK) of the Length to indicate whether or not the packet is an acknowledgement of receipt. There is no need to include the payload of the original packet in an acknowledgement (so the Length field will actually be unused) as the sender will retain a copy of the entire packet until receipt is acknowledged and each packet can be uniquely identified by the sender's MAC address and the packet Sequence Number. In this way, any packet can be subject to reliable delivery at the request of the sending Node.

Packet Structure + ACK
h Langlo Packet Structure + ACK.docx [82 KB]

I am yet to decide whether it might be logical, convenient or otherwise to include functions to support reliable packet delivery, beyond those required to store and retrieve the relevant packet elements, in the library package discussed below. I will need to work through a couple of applications first, to see how much common code there might be that would lend itself to inclusion in a library.

In the mean time, I've started making notes on the reliable delivery protocol on a separate page and my first implementation is also described, with code examples, elsewhere on this site.

Further details pending...

The PacketHandler Library

The PacketHandler library contains everything needed to manage the encoding, transmission, subsequent receipt and decoding of data in my network. The PacketHandler currently uses the packet structure described above, but could be used equally well to manage any packet or data structure or, indeed, no structure at all—just an arbitrary byte stream.

Much of the motivation for building this library was simply to remove any need to deal with packet structure detail when developing application sketches—the application can simply pass data to the PacketHandler and the PacketHandler will take care of packaging that data for delivery to the Gateway. The Gateway then uses the same library to interpret received data.

The library also includes the necessary conditional compiler instructions to enable it to be used with any of the hardware platforms described on this site.

Some PacketHandler methods also draw on the NodeHandler library, which includes methods used in the management of individual Nodes. The NodeHandler is not really necessary when dealing with the software for individual Nodes, but it does simplify the management of Gateway Nodes, which need to know about all of the Nodes with which they communicate. My ultimate goal is for this information to be gathered by a Gateway Node dynamically, when Sensor Nodes first communicate with a Gateway. For the time being however, the Gateway's 'Node Table' is configured statically, through the NodeHandler.

Early versions of the PacketHandler package also included the EepromHandler library. The EepromHandler library really has very little to do with packets or Nodes as such but was included, at the time, for pure convenience (I only had to manage a single library package). In practice, this library is entirely independent of the PacketHandler and NodeHandler libraries and more recently has been packaged independently.

PacketHandler

The PacketHandler class defines the structure and manages the content of data packets.

The normal flow then, within an application sketch, would be to provide the common header information that would identify a Node and its intended Gateway, identify the general nature of the information to be sent to the Gateway, assign values to associated variables, then send the data to the Gateway.

Predefined Data Types

The PacketHandler library includes several predefined, public data types that are used at various points in the process of packet assembly. The most fundamental type is that which effectively defines the individual packet structures.

Predefined types

typedef enum: uint8_t {
ACK = 0,//0
VOLTAGE,//1
POWER,//2
TANK,//3
PUMP,//4
WEATHER,//5
ATMOSPHERE,//6
TEMPERATURE,//7
RAINFALL,//8
WIND,//9
VOX,// 10
LIGHT,// 11
UV,// 12
SPRINKLER,// 13
GPS,// 14
AWTS,// 15
RESET,// 16
DEFAULT_TYPE,// 17
FLOW,// 18
NUM_TYPES
} PH_packetType;

The order in the above list simply reflects the order in which the different types have been progressively defined. At some point I may come back and organise the list in alphabetical order, as I have done below when describing the different access methods, but while the structure is under development, it helps backward compatibility to just add new types to the end of the list.

Data Packets and Methods

A packet instance is created, and optionally initialised, using the begin method. Several versions of the method are available, with varying degrees of packet header initialisation (refer above for the structure/content of the packet header):

void begin();
void begin(uint32_t destinationMAC);
void begin(uint32_t destinationMAC, uint32_t sourceMAC);
void begin(uint32_t destinationMAC, uint32_t sourceMAC, uint16_t sequenceNumber);
void begin(uint32_t destinationMAC, uint32_t sourceMAC, uint16_t sequenceNumber, uint8_t type);

The relevant header fields can, nonetheless, be set at any point within a sketch:

void setDestinationMAC(uint32_t destinationMAC);
void setSourceMAC(uint32_t sourceMAC);
void setSequenceNumber(uint16_t sequenceNumber);
void setPacketType(PH_packetType type);

A corresponding set of methods is provided to retrieve header information:

uint32_t destinationMAC();
uint32_t sourceMAC();
uint16_t sequenceNumber();
PH_packetType packetType();

Checksumming is discussed in more detail below.

When sending or receiving packets, the structure of the packet data element is established or determined by setting or reading the packet type with the respective methods:

void setPacketType(PH_packetType type);
PH_packetType packetType();

To simplify the processing of received packets, the structure of which is defined by the Type field in the packet header, the PacketHandler library includes methods that recognise packet types and output the packet contents in a predefined format, to the Serial Monitor, local OLED display or MQTT Broker:

void serialOut();
void displayOut();
void mqttOut();

The currently available packet types are listed in the enumerated type PH_packetType (see above), which is, in turn, defined within the PacketHandler library. The specific methods used with each of these packet types are listed below (click to expand a type):

ATMOSPHERE

setPacketType(ATMOSPHERE);
Sender
void setTemperature(uint16_t temperature);
void setPressure(uint16_t pressure);
void setHumidity(uint16_t humidity);
Receiver
uint16_t temperature();
uint16_t pressure();
uint16_t humidity();

AWTS

setPacketType(AWTS);
Sender
void setBlowerPressure(uint16_t blowerPressure);
void setItLevel(uint16_t itLevel);
Receiver
uint16_t blowerPressure();
uint16_t itLevel();

FLOW

setPacketType(FLOW);
Sender
void setFlowRate(uint16_t flowRate);
void setFlowVolume(uint16_t flowVolume);
Receiver
uint8_t flowRate();
uint16_t flowVolume();

GPS

setPacketType(GPS);
Sender
void setLatitude(float latitude);
void setLongitude(float longitude);
void setAltitude(float altitude);
void setHdop(float hdop);
Receiver
float latitude();
float longitude();
float altitude();
float hdop();

LIGHT

setPacketType(LIGHT);
Sender
void setAmbientLight(uint16_t ambientLightLevel);
Receiver
uint16_t ambientLight();

POWER

setPacketType(POWER);
Sender
void setBatteryVoltage(uint16_t batteryVoltage);
void setBatteryCurrent(uint16_t batteryCurrent);
void setPanelVoltage(uint16_t panelVoltage);
void setPanelCurrent(uint16_t panelCurrent);
void setLoadVoltage(uint16_t loadVoltage);
void setLoadCurrent(uint16_t loadCurrent);
Receiver
uint16_t batteryVoltage();
uint16_t batteryCurrent();
uint16_t panelVoltage();
uint16_t panelCurrent();
uint16_t loadVoltage();
uint16_t loadCurrent();

PUMP

Predefined types

typedef enum: uint8_t {
PH_POWER_FAIL = 0,
PH_POWER_LIVE,
PH_POWER_UNDEFINED,
NUM_SUPPLIES
} PH_powerSupply;

typedef enum: uint8_t {
PH_RELAY_OFF = 0,
PH_RELAY_ON,
PH_RELAY_UNDEFINED,
NUM_STATES
} PH_relayState;

typedef enum: uint8_t {
PH_MODE_REMOTE = 0,
PH_MODE_LOCAL,
PH_MODE_UNDEFINED,
NUM_MODES
} PH_controlMode;
setPacketType(PUMP);
Sender
void setPowerSupply(PH_powerSupply powerSupply);
void setRelayState(PH_relayState relayState);
void setControlMode(PH_controlMode controlMode);
Receiver
PH_powerSupply powerSupply();
PH_relayState relayState();
PH_controlMode controlMode();

RAINFALL

setPacketType(RAINFALL);
Sender
void setRainfall(uint16_t rainCount);
Receiver
uint16_t rainfall();

RESET

setPacketType(RESET);
Sender
void setResetCode(uint8_t resetCode);
Receiver
uint8_t resetCode();

SPRINKLER

setPacketType(SPRINKLER);

The content of a SPRINKLER message and associated methods are yet to be defined.


TANK

setPacketType(TANK);
Sender
void setTankId(uint8_t tankId);
void setTankLevel(uint16_t tankLevel);
Receiver
uint8_t tankId();
uint16_t tankLevel();

TEMPERATURE

setPacketType(TEMPERATURE);
Sender
void setTemperature(uint16_t temperature);
Receiver
uint16_t temperature();

UV

setPacketType(UV);
Sender
void setUvA(uint16_t uva);
void setUvB(uint16_t uvb);
void setUvIndex(uint16_t uvIndex);
Receiver
uint16_t uvA();
uint16_t uvB();
uint16_t uvIndex();

VOLTAGE

setPacketType(VOLTAGE);
Sender
void setVoltage(uint16_t voltage);
Receiver
uint16_t voltage();

VOX

setPacketType(VOX);
Sender
void setVoxLevel(uint16_t voxLevel);
Receiver
uint16_t voxLevel();

WEATHER

setPacketType(WEATHER);
Sender
void setTemperature(uint16_t temperature);
void setPressure(uint16_t pressure);
void setHumidity(uint16_t humidity);
void setRainfall(uint16_t rainCount);
void setWindBearing(uint16_t windBearing);
void setWindSpeed(uint8_t windSpeed);
Receiver
uint16_t temperature();
uint16_t pressure();
uint16_t humidity();
uint16_t rainfall();
uint16_t windBearing();
uint8_t windSpeed();

WIND

setPacketType(WIND);
Sender
void setWindBearing(uint16_t windBearing);
void setWindSpeed(uint8_t windSpeed);
Receiver
uint16_t windBearing();
uint8_t windSpeed();

An error message will be generated if an attempt is made to set or get an attribute that has not been defined for a given packet type.

When sending a packet, a checksum is automatically calculated over the packet payload—when invoking the bytestream function—and included in the packet header. When receiving a packet, the checksum should be verified to guarantee data integrity. Public methods are nonetheless provided to generate, get or simply verify (returns true if the calculated checksum matches that in the packet header, false otherwise) the payload checksum.

void generatePayloadChecksum();
uint32_t payloadChecksum();
bool verifyPayloadChecksum();
Adding New Packet Types

To add a new packet type:

  1. Add an appropriate descriptor to the PH_packetType enumerated type
  2. Define the packet payload structure (use existing definitions to formulate a compatible structure)
  3. With reference to step 2, add appropriate elements to the PH_genericPayload union and _packetID array
  4. Define set and get methods for the associated data elements
  5. If serial or display output is required, add appropriate code segments to the serialOut() and/or displayOut() methods respectively
  6. If MQTT messages are to be generated, add appropriate topic descriptors and definitions to the PH_topicList enumerated type and SENSOR_TOPIC array, and add an appropriate code segment to the mqttOut() method.
Sending and Receiving Packets

My original motivation for this project derived from the lack of consistency in garden watering controllers. I had been progressively expanding my watering system over a period of 10 years and every time I went to purchase an additional controller, the unit that I had previously purchased had been made obsolete and I was facing the prospect of a fourth different controller in my set-up. In looking for a more uniform system, I discovered the [open source] OpenSprinkler system, based on the Espressif ESP8266 MCU.

Looking more broadly at the task of water management on my property, I then discovered the work of Charles-Henri Hallard and Wijnand Nijs that employed the Arduino Pro Mini [ATmega] MCU. This work used the LMIC software library for LoRa communications.

The first MCUs that I used, however, were the Heltec Automation WiFi LoRa 32 development boards that conveniently included an on-board Semtech SX1276 LoRa node chip. The LoRa software most commonly used with these boards at the time was the Sandeep Mistry LoRa library. Since this library supported all of the platforms—ATmega328, ESP8266 and ESP32—that I was using at the time, this became the basis for my own development effort.

Some time later, Heltec introduced their CubeCell platform, based on the Semtech SX1262 LoRa node chip and, unfortunately, not supported by the Sandeep Mistry library. Heltec provided an alternative LoRa software library for this platform that, fortunately, used similarly structured transmission functions—both libraries provide methods for the transmission of a sequence of bytes. Accordingly, in addition to accessing elements of the formal packet structure, the PacketHandler library includes methods that manage packet content as either individual bytes or as a pointer to a 'byte stream' representation of the current packet.

Note that the more recent ESP32-S3-based Heltec development boards also incorporate the SX1262 LoRa node chip and thus also require the use the Heltec LoRa software.

Sending Packets

Sending packets is relatively straightforward regardless of the LoRa library used.

PacketHandler Methods Used

uint8_t *byteStream();
uint8_t packetByteCount();
Usage

// Sandeep Mistry [SX1276] LoRa Send sequence

#include <SPI.h>
#include <LoRa.h>
#include <PacketHandler.h>

PacketHandler packet;

LoRa.beginPacket();
LoRa.write(packet.byteStream(), packet.packetByteCount());
LoRa.endPacket();
Usage

// Heltec [SX1262] LoRa Send sequence

#include <LoRaWan_APP.h>
#include <PacketHandler.h>

PacketHandler packet;

Radio.Send(packet.byteStream(), packet.packetByteCount());
Receiving Packets

Unfortunately, receiving packets is a little more complicated. Note that the Heltec radio library delivers received data from the input buffer as [a pointer to the head of] a stream of bytes while the Sandeep Mistry LoRa library only retrieves a single byte at a time. The PacketHandler library includes methods to accomodate both input mechanisms, as illustrated below.

PacketHandler Methods Used

uint8_t headerSize();
uint8_t packetByteCount();
void setByte(uint8_t i; uint8_t byte);
void setContent(uint8_t *payload; uint8_t byteCount);
void verifyPayloadChecksum();
void erasePacketHeader();
Usage

// Sandeep Mistry [SX1276] LoRa Receive sequence

#include <SPI.h>
#include <LoRa.h>
#include <PacketHandler.h>

PacketHandler packet;

void setup() {
.
.
// Code to initialise LoRa
.
.
}

void loop() {
.
.
if ( receivePacket() ) {
// Include code to process the packet here
}
.
.
}

bool receivePacket() {
if ( LoRa.parsePacket() ) {
while ( LoRa.available() ) {

// Read the packet header
int headerLimit = packet.headerSize();
for (int i = 0; i < headerLimit; i++) {
packet.setByte(i,LoRa.read());
}

// At this point, we would usually check the destination
// MAC address to verify that the packet is for us before
// reading in the payload

int byteCount = packet.packetByteCount();
for (int i = headerLimit; i < byteCount; i++) {
packet.setByte(i,LoRa.read());
}

// Verify the payload checksum
if (packet.verifyPayloadChecksum()) {
return true;
} else {
// Invalid checksum, discard the packet
packet.erasePacketHeader();
return false;
}
}
}
}

The available examples of SX1262-based code generally implement the send/receive process in the context of a state machine, which is, once you get the hang of what's going on, a 'tidier' way of implementing the overall send/receive/sleep cycle. The main loop thus simply includes a switch statement that identifies the possible states of the 'machine'—TX, RX or LOWPOWER. The programming challenge in this case is in working out how to leave the 'system' in a known condition as we transition between these states.

I have completely ignored this in the send example above and given it only cursory treatment in the receive example below, focusing simply on the sending and receiving functions respectively. I will provide a complete example of an operational state machine in the following Section in due course.

Usage

// Heltec [SX1262] LoRa Receive sequence

#include <LoRaWan_APP.h>
#include <PacketHandler.h>

PacketHandler packet;

static RadioEvents_t RadioEvents;

typedef enum {
TX,
RX,
LOWPOWER
} States_t;

States_t state;

void setup() {
.
.
RadioEvents.RxDone = OnRxDone;
Radio.Init( &RadioEvents );
// Additional code to initialise LoRa
.
.
}

void loop() {
switch(state) {
case TX:
// Code to assemble and send packets here
break;
case RX:
Radio.Rx(0);
break;
case LOWPOWER:
// LowPower code here
break;
}
Radio.IrqProcess();
}

void OnRxDone( uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr ) {
packet.setContent(payload,size);
Radio.Sleep();

// At this point, we would usually check the destination
// MAC address to verify that the packet is for us before
// going any further

// Verify the payload checksum
if (packet.verifyPayloadChecksum()) {
// Include code to process the packet here
}
else {
// Invalid checksum, discard the packet
packet.erasePacketHeader();
}
}
Putting It All Together

Simple examples of the essential elements of this flow are illustrated in the following sketch extracts for Sender, Receiver and Gateway Nodes. In these extracts, I have tried to exclude anything that does not relate directly to the PacketHandler, to more clearly illustrate its usage. The sketch downloads, however, are the fully functional sketches from which the extracts were derived.

The following extracts are primarily for SX1267-based Nodes that use the Sandeep Mistry LoRa library. I will include extracts that include the receive process for SX1262-based Nodes—Heltec CubeCell and V3 MCUs— as I get the time. In the mean time, several applications described herein provide examples of SX1262-based code, if only as senders.

Sending Node

The following outline is for a sketch that uses the PacketHandler to send a voltage measurement to a nominated Gateway Node.

PacketHandler Library Usage ([SX1276/SX1262] Sender)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <SPI.h>// [SX1267] LoRa radio interface
#include <LoRa.h>// LoRa protocol [SX1267]
#include <LoRaWan_APP.h>// LoRa protocol [SX1262]
#include <PacketHandler.h>// Packet Handler
[Other #include directives as required for the application]

const uint32_t gatewayMAC = [gateway MAC address];
const uint32_t myMAC = [Node MAC address];
uint16_t messageCounter = 0;
uint16_t batteryVoltage;

PacketHandler packet;

void setup() {

  [ Radio initialisation ]

// The packet header will also usually be initialised just
// once, during sketch setup as follows

  packet.begin(gatewayMAC,myMAC);// Initialise the Packet Handler
}

// The body of the sketch will then include a cycle,
// assembling and transmitting packets

void loop() {

  [Increment the Sequence Number]
  
  packet.setSequenceNumber(messageCounter);// Set the Sequence #

// The following could be any supported packet type and
// associated variables

  packet.setPacketType(VOLTAGE);// Identify the packet content

  [Read battery voltage]// Or whatever data/sensor(s) is/are to be read

  packet.setVoltage(batteryVoltage);// Set the relevant data variables

// Packet content can be output locally as required

  packet.hexDump();// Output a hex dump of the packet to the Serial Monitor
  packet.serialOut();// Output the packet elements to the Serial Monitor
  packet.displayOut(displayPtr);// Display the packet elements on the Node display (if present)

// Transmit the packet
// For platforms using the Sandeep Mistry LoRa library (SX1276 LoRa node chip)
  
  LoRa.beginPacket();// Initialise transmission
  LoRa.write(packet.byteStream(), packet.packetByteCount());// Load the output buffer
  LoRa.endPacket();// Send the buffer contents

// For platforms using the Heltec LoRaWan_APP library (SX1262 LoRa node chip)

  Radio.Send(packet.byteStream(), packet.packetByteCount());

// Repeat the above for any other packets that need to be sent in a cycle

  [Sleep for a while]
}
Receiving Node

The following outline is for a sketch that promiscuously receives packets and displays the content on both the Serial Monitor and an OLED display.

PacketHandler Library Usage ([SX1276] Receiver)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <SSD1306.h>// OLED display
#include <SPI.h>// LoRa radio interface
#include <LoRa.h>// LoRa protocol
#include <PacketHandler.h>// Packet Handler
[ Other #include directives as required for the application ]

const uint32_t myMAC = [Node MAC address];
[ Other definitions as required ]

SSD1306 display(0x3c, SDA_OLED, SCL_OLED), *displayPtr;

PacketHandler packet;

void setup() {
  
  [ Initialise Serial Monitor ]
  [ Initialise OLED Display ]
  [ Initialise LoRa ]
  
  pinMode(LED_BUILTIN,OUTPUT);

// Initialise the packet objects
  
  packet.begin();

// Set up the pointer to the OLED display
// This is passed to the Packet object when required for output

  displayPtr = &display;
}

// The body of the sketch will then include a cycle,
// receiving and processing packets

void loop() {
 
//  Just loop around checking (promiscuosly) to see if there's anything to process

  if ( receivePacket() ) {
  
    digitalWrite(LED_BUILTIN,HIGH);
         
// Send relevant information to the Serial Monitor
    
    packet.hexDump();
    packet.serialOut();
    
// print RSSI & SNR of packet
    
    Serial.print("[loop] RSSI ");
    Serial.print(LoRa.packetRssi());
    Serial.print("   SNR ");
    Serial.println(LoRa.packetSnr());
    Serial.println();

// Send relevant information to the OLED display
    
    packet.displayOut(displayPtr);   
      
// Print RSSI and SNR of packet
     
    display.drawHorizontalLine(0, 52, 128);
    display.setTextAlignment(TEXT_ALIGN_LEFT);
    display.drawString(0, 54, "RSSI " + (String)LoRa.packetRssi() + "dB");
    display.setTextAlignment(TEXT_ALIGN_RIGHT);
    display.drawString(128, 54, "SNR " + (String)LoRa.packetSnr() +"dB");
    display.display();
  }

  [ Do anything else that needs to be done with a received packet ]
  
  digitalWrite(LED_BUILTIN,LOW);
}

bool receivePacket() {
  
// Only if there's something there

  if ( LoRa.parsePacket() ) {
    while ( LoRa.available() ) {
    
//    Read the packet header
//    We're just reading promiscuously at this point, so we'll actually take anything

      int headerLimit = packet.headerSize();
      
      for (int i = 0; i < headerLimit; i++) {
        packet.setByte(i,LoRa.read());
      }
      
//    If transmissions are directed, we would check that the Destination MAC address was
//    ours before continuing. For the present, we'll take anything.

//    Read in the rest of the packet

      uint8_t byteCount = packet.packetByteCount();
      for (int i = headerLimit; i < byteCount; i++) {
        packet.setByte(i,LoRa.read());
      }
     
//  Verify the checksum before we go any furhter
   
      if (packet.verifyPayloadChecksum()) {

// All good
        
        return true;
      } else {
        Serial.println("[receivePacket] Invalid checksum, packet discarded");
        Serial.println();
        packet.erasePacketHeader();
        return false;
      }
    }
  } else {
    
//  Nothing there

    return false;     
  }
}
Gateway Node

The Gateway Node is a specific implementation of a Receiver Node. Not only does it receive packets, but it filters out packets not specifically directed to this Gateway Node and, using the PacketHandler method specifically designed for the task, generates and forwards relevant MQTT messages.

The following example is for an SX1276-based Node that uses the Sandeep Mistry LoRa library. The implementation for an SX1262-based Node, using the Heltec LoRaWan_APP library, requires the inclusion of that library and modifications in line with the details provided above.

PacketHandler Library Usage ([SX1276] Gateway)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#include <SSD1306.h>// OLED display
#include <SPI.h>// LoRa radio interface
#include <LoRa.h>// LoRa protocol
#include <WiFi.h>// WiFi client
#include <PubSubClient.h>// MQTT client
#include <PacketHandler.h>// Packet Handler
#include <Ticker.h>// Asynchronous task management
[ Other #include directives as required ]

const uint32_t myMAC = [Node MAC address];
const int statusCheckInterval = 600;// 600 seconds (10 min)
[ Other definitions as required ]

// Set up supporting clients

Ticker heartbeatTicker, healthCheckTicker, statusTicker;
WiFiClient localClient;
PubSubClient mqttClient(localClient), *mqttClientPtr;
SSD1306 display(0x3c, SDA_OLED, SCL_OLED), *displayPtr;

// Instantiate the Packet Handler
// In addition to generating MQTT messages for received
// packets (inPacket), the Gateway will issue its own
// MQTT status messages (outPacket)

PacketHandler inPacket, outPacket;

void setup() {

  [ Initialise Serial Monitor ]
  [ Initialise OLED Display ]
  [ Initialise LoRa ]
  [ Connect to WiFi ]
  [ Connect to the MQTT Server ]

// Initialise the packet objects

  inPacket.begin();
  outPacket.begin(myMAC,myMAC,0);

// Set up the pointers to the OLED display and MQTT client
// These are passed to the Packet object when required for output

  displayPtr = &display;
  mqttClientPtr = &mqttClient;

// Let the broker know there's been a reset

  outPacket.setPacketType(RESET);
  outPacket.setResetCode(0);
  outPacket.mqttOut(mqttClientPtr);

// And finally, set up the timers

  heartbeatTicker.attach( inPacket.getHeartbeatInterval(), heartbeat );
  healthCheckTicker.attach( inPacket.getHealthCheckInterval(), healthCheck );
  statusTicker.attach( statusCheckInterval, statusCheck );
}

// The body of the Gateway sketch just receives packets
// and sends associated MQTT messages

void loop() {

  if ( receivePacket() ) {
    inPacket.mqttOut(mqttClientPtr);

// We can also output the packet contents, display them
// on the OLED display or flash a LED on receipt

    inPacket.hexDump();// Output a hex dump of the packet to the Serial Monitor
    inPacket.serialOut();// Output the packet elements to the Serial Monitor
    inPacket.displayOut(displayPtr);// Display the packet elements on the Node display

// The tickers will also chime in when their respective timers
// fire and execute the Health Check and Gateway status functions

  }
}

bool receivePacket() {
  if ( LoRa.parsePacket() ) {
    while ( LoRa.available() ) {
      LoRa.beginPacket();
    
// Read the packet header and check the destination MAC

      int headerLimit = inPacket.headerSize();
      uint8_t nextByte;
      
      for (int i = 0; i < headerLimit; i++) {
        nextByte = LoRa.read();
        inPacket.setByte(i,nextByte);
        Serial.printf( "%02X", nextByte);
        if ((i % 4) == 3) {
          Serial.println("    ");
        } else {
          Serial.print("  ");
        }
      }
      
      if (inPacket.destinationMAC() == myMAC) {

// Read in the rest of the packet

        uint8_t byteCount = inPacket.packetByteCount();
        for (int i = headerLimit; i < byteCount; i++) {
          inPacket.setByte(i,LoRa.read());
        }
       
// Verify the checksum
     
        if (inPacket.verifyPayloadChecksum()) {
          return true;
        } else {
          Serial.println("[receivePacket] Invalid checksum, packet discarded");
          Serial.println();
          inPacket.erasePacketHeader();
          return false;
        }
      } else {
        Serial.print("[receivePacket] This appears to be for someone else--To: 0x");
        Serial.print(inPacket.destinationMAC(),HEX);
        Serial.print(" From: 0x");
        Serial.println(inPacket.sourceMAC(),HEX);
        inPacket.erasePacketHeader();
        return false; 
      }
    }
  } else {
    return false;     
  }
}

void heartbeat() {
  inPacket.incrementNodeTimers();
}

void healthCheck() {
  inPacket.nodeHealthCheck(mqttClientPtr);
}

void statusCheck() {
       
// readBatteryVoltage() and readDS18B20Sensor() are examples
// of local functions that return Gateway status information
     
  outPacket.setPacketType(VOLTAGE);
  outPacket.setVoltage(readBatteryVoltage());
  outPacket.serialOut();
  outPacket.mqttOut(mqttClientPtr);

  outPacket.setPacketType(TEMPERATURE);
  outPacket.setTemperature(readDS18B20Sensor());
  outPacket.serialOut();
  outPacket.mqttOut(mqttClientPtr);
}

Update: 23 Mar 2023  I do now have operational SX1262 send/receive sketches for the Heltec V3 MCUs that also include the queuing features mentioned below. I will provide downloads as soon as the code has proven stable and I have tidied up (i.e. removed all the debugging elements from) the code.

The function of a Gateway Node in the present environment is to act as a relay between LoRa-connected Sensor [Sender] Nodes and an ethernet or WiFi-connected MQTT server. In this capacity, a basic Gateway Node implementation, as described above, simply receives packets and invokes the PacketHandler to send out the relevant MQTT messages. The Gateway Node does, nonetheless, read the destination MAC address from each packet header to verify that it is the intended recipient before invoking the PacketHandler to process the packet payload.

More recently, however, I have added internal queuing features to both the Gateway and Sensor Node sketches to support a basic form or reliable packet delivery. I have also added a similar queuing mechanism to the Gateway Node to improve the handling of MQTT callbacks.

Further details pending...

07-09-2024