Alora RFM1262 – LoRa Mesh Network

Mesh header

LoRa is a wireless communication known for its long range capability. But what if the distances between sensor nodes are farther than the range you can achieve with LoRa.

That’s when you need a LoRa Mesh network. A mesh network is not a network in the typical star topology where every node is connected to a central station (with WiFi that would be an access point, with LoRa that would usually be a gateway). Instead every node knows about all other nodes in the network, even if two nodes cannot communicate directly with each other because they are too far away from each other.

Star vs Mesh Topology
Comparison of Star topology vs Mesh topology

There is a nice article that compares both network topologies on TechDifferences. And of course there is something about Mesh networks on Wikipedia.

Before you go on!

Scope of the tutorial:
This tutorial will just create a simple Mesh network that sends randomly messages to other nodes within the Mesh network. There is no data structure or gateway functions to other networks like GSM or WiFi.
The goal here is to have a working core Mesh network that can be used to build up a second layer of communication which can be anything you want to do, e.g. a network of sensors that send out their sensor data.

Target hardware:
The code is written to work on either an ESP32 or a nRF52 microcontroller and is not compatible with other microcontrollers. The LoRa transceivers used are Semtech SX1262 transceivers. They are either connected to the microcontroller as a Adafruit Feather compatible breakout (Circuitrocks Alora RFM1262) or integrated into a SOC together with a Nordic nRF52 (Insight ISP4520).

And another important thing:
This code is NOT written to work with the ArduinoIDE. It is structured to be compiled under PlatformIO using one of the IDE’s supported by PlatformIO. My favorite is Visual Studio Code.

The heart of a Mesh network

To be able to communicate from any node in the network to any other one, each node has its own router. The router builds a map that is based on the nodes that are in reach (visible to the node). In addition it holds routes in the map about nodes that are not in reach (invisible to the node), the distance to this invisible node and which visible node can be used to reach the invisible node.

Building up the routing map

In order to build this map of the Mesh network, each node distributes its map of visible nodes to all its neighbours. Let’s imagine you have 4 nodes, called A, B, C and D. In this small network, A can only communicate with B, but nodes C and D are too far away. B can see all other nodes, while D and C sees only each other and B.

When the nodes start broadcasting their maps, in the beginning, each of them will only broadcast a map with the nodes it can see.

1st broadcast

A‘s map contains only B
B‘s map contains A, C and D
C‘s map contains B and D
D‘s map contains B and C
When A receives the map from node B, it adds nodes C and D as additional nodes into it’s map not as direct nodes, but as indirect nodes with an information about the distance (hops) to the node and a route address, which is the node that he got the map from. Same for nodes C and D. They will add node A into their map, but with B as route address and a distance of 1.

2nd broadcast

A‘s map now contains B and with a hop value of 1 the nodes C and D. Nodes C and D as well are entered with node B as their first hop address.
B‘s map is unchanged and contains A, C and D
C‘s map now contains B and D and in addition node A with a hop value of 1 and node B as the first hop address.
D‘s map now contains B and D and in addition node A with a hop value of 1 and node B as the first hop address.

So now node A knows, if it wants to send data to node C or D, it has to send it first to node B, which then forwards the message to the receiver node.

Add more nodes

Now lets make it a little bit more complicated and add another node E. E can only communicate with C and D, but it cannot communicate with A or B

After the map is build it would look in each node like

Map of A

B with hops 0 and no route address
C with hops 1 and route address B
D with hops 1 and route address B
E with hops 2 and route address B

Map of B

A with hops 0 and no route address
C with hops 0 and no route address
D with hops 0 and no route address
E with hops 1 and route address C

Map of C

A with hops 1 and route address B
B with hops 0 and no route address
D with hops 0 and no route address
E with hops 0 and no route address

Map of D

A with hops 1 and route address B
B with hops 0 and no route address
C with hops 0 and no route address
E with hops 0 and no route address

Map of E

A with hops 2 and route address D
B with hops 1 and route address C
C with hops 0 and no route address
D with hops 0 and no route address

Now node E knows, that if he wants to send data to node A, it has to send the package first to node D. Node D receives the data, and will forward it to node B. Finally node B will send the data to node A.

Restrictions:

Some restrictions you need to be aware off when you start playing around with the LoRa Mesh software.

  • LoRa is slow and frequency of sending data is limited by local regulations. Therefor the Mesh network takes some time (specially if you have many nodes) to build up its complete map in each node. After reboot a nodes starts to send its own map every 30 seconds for the first 5 minutes. After that it resend its map only every 60 seconds to lower the traffic in the network.
  • The size of the data package is limited. The SX126x has a send buffer of only 256 bytes (and some bytes are reserved to the CRC checksum). So the maximum size of a data package is limited to 250 bytes.
  • The number of nodes in the Mesh network is limited. Because of the limited size of the send buffer the map cannot contain more than 48 nodes.
  • Package receive is not confirmed. With the code in this tutorial, a data package can get lost at any node. If you need to make sure that a data package was received by the targeted node, you need to build an acknowledge functionality on top of this sample code.
  • Keep the amount of data packages limited in size and frequency of sending. Again this is to comply with local regulations, but it as well keeps the Mesh network stable and avoids data collisions which lead to loss of packages.

More restrictions

This code is written to work on ESP32 and nRF52 microcontrollers only. It is using task and message queue technologies that are only available in FreeRTOS. But the code is using the Arduino framework, so it is easy to understand.

It will take some effort to get this code to run on simple AVR, SAM or ESP8266 microcontrollers.

FreeRTOS

To keep the main application free from handling the Mesh events, a Mesh handler is running as its own FreeRTOS task. This way the main application does not need to call a function from its main loop to handle the Mesh. If the main application wants to send a data package to a specific node, it just has to call a function of the Mesh handler to put the data package into a queue which will be handled by the Mesh task to make sure data packages are not colliding with sending of map information or receiving of data from other nodes.

See also  ESP32-CAM with RTSP video streaming

To achieve above independence between the main application and the Mesh handler, several functions of FreeRTOS are used.

  • As already said, an independent FreeRTOS task is started and running in parallel to the Arduino loop() to handle all Mesh events.
  • To make sure that data is sent without colliding with an attempt to broadcast a Mesh map or interrupting an ongoing data receive, a FreeRTOS queue mechanism is used to manage outgoing packages.
  • To make sure that map data is not changed while accessed from the main app, FreeRTOS semaphores are used to control the access.

But getting this independence between main app and Mesh handler comes with the cost that this example code is at the moment only working on microprocessors that are running FreeRTOS.

Testing

I tested this software with a Mesh network of 12 nodes. 6 nodes were build with ESP32’s and 6 nodes were build with nRF52’s. All nodes were using the SX1262 LoRa transceiver chips.
To simulate isolation between certain nodes, the code includes filtering of incoming maps based on node ID’s. The test network (with isolation between certain nodes) looks like this:

As you can see, there are 9 nodes (Black and Blue circles) on the left side, where nearly every node can see each other. The only exception are the nodes 0C666CBF and 84E26CB, which cannot see each other.
On the right side is another bunch of 3 nodes (Red circles) that can see each other, but only node 30C2050B can see one node of the left group, the node 84E26CB.

To test the network. every node selects randomly another node from the Mesh network every 30 seconds and sends a data package to it. The receive of the data package is NOT confirmed, I only checked it with recorded logs from the serial output of the nodes.

In addition I equipped one of the nodes with a display that permanently shows the map to check visually if all nodes in the network are mapped.

The display node

12 nodes in the Mesh map. The nodes with a * at the end are “invisible” nodes, that are out of range of the display node, but can be reached over other nodes in the Mesh network.
The unit with the display is a custom board with an ESP32 (left) and a SX1262 breakout board (right).

The Feather ESP32 nodes

The other ESP32 nodes were build with Adafruit Huzzah ESP32 Feathers and the Circuitrocks Alora RFM1262 boards.

The nRF52 nodes

The 6 nRF52 boards used are some custom boards with an Insight ISP4520 module. This module combines a Nordic nRF52832 with a Semtech SX1262 transceiver chip in one module.

This test setup was running for nearly a week without major problems. As I needed some of the modules for other developments, frequently some nodes disappeared from the Mesh network. Other nodes in the network could adapt their map and the Mesh continued to function. After putting the node back into the Mesh the rebuild of the map worked as well.

The Software

Part 1 – Structures

To build up a map of the mesh, each node found is stored with some information how to connect to the node and its age. This information is stored in the nodesList structure.

struct nodesList
{ uint32_t nodeId; uint32_t firstHop; time_t timeStamp; uint8_t numHops;
};
nodeId the unique node ID of an entry
firstHop if the node is not a direct node, this is the node ID of the first node to send data to for further routing
timeStamp a timestamp created when the entry was created or refreshed
numHops if the node is not a direct node, this is the distance between the owner node of the map and the nodeId

Part 2 – The router

This is the heart of the Mesh network. The router creates the Mesh network map and returns routing information how to send a data package to a specific node.

The router uses above shown structure to store the routing information to all known nodes in the Mesh network.

The router part consists of 9 functions to add nodes to the map, delete nodes, clean up the map from expired nodes and return routing information to a specific node.

I will explain only the functions that are used by the Mesh application, not the housekeeping functions that are just called from inside the router code.

void addNode(uint32_t id, uint32_t hop, uint8_t hopNum)

@param uint32_t id
ID of the node to be added.
@param uint32_t hop
ID of the first hop if the node is an indirect node
@param uint8_t hopNum
Number of hops if the node is an indirect nodes
This function will add a node to the map if it does not exist already. It checks as well if the node already exists as a direct node or as a indirect node with a lower or higher number of hops.

The logic is

If the node to add is a direct node

  • if the node does not exist, add it
  • if the node exists, but as an indirect node then update the entry.

If the node to add is an indirect node

  • If the node does not exist, add it
  • If the node exists, but with a higher number of hops, replace the entry.

bool getRoute(uint32_t id, nodesList *route)

@param uint32_t id
The node ID we need a route to
@param nodesList *route
A pointer to a route structure that will be filled with the routing information
@return bool result
True if a route could be found
False if no route could be found

This routine searches the map for a route to a node ID. If it finds a route, it fills the route structure given by the pointer route and returns TRUE. If no route could be found it returns FALSE.

void clearSubs(uint32_t id)

@param uint32_t id
Node ID that are listed as first hop of indirect nodes

This routine is to clean up the map from indirect nodes that have a given ID as their first hop. This is called every time a map is received from another direct node to clean up evtl. dead entries.

bool cleanMap(void)

@return bool result
True if no expired nodes were found
False if expired nodes were found

At the time a node is added to the map, it gets a time stamp. If in following updates the node is still in the map of a neighboring node or sends a map as a direct node, the time stamp is updated.
This routine checks if the time stamp of an entry is older than 90 seconds. If a node wasn’t updated for more than 90 seconds it is removed from the list (together with its sub-nodes if it was a direct node).
In case an expired node was found, the routine returns false, which is used by the Mesh task to take actions to rebuild the Mesh map.

uint8_t nodeMap(uint8_t nodes[][5])

@param uint8_t nodes[][5]
Pointer to a 2 dimensional array that will be filled with map information of each node in the map
@return uint8_t number of nodes
Number of nodes found in the map.

When building up the data package to broadcast the map of the node, this function fills a 2 dimensional array with the content of the map.

The first dimension of the array is for the number of nodes in the map. The second dimension will be filled with the address of the node and the number of hops to that node.

The filled array would for example look like

Node ID (bytes 0 to 3)

0x87EB981E

0x30C2050B

0x28B0495F

Number of hops (byte 4)

0

1

3

uint8_t numOfNodes()

@return uint8_t numOfNodes
Number of nodes in the map

This function just returns the number of nodes that are stored in the map.

See also  FreeRTOS: ESP32 Deep Sleep

bool getNode(uint8_t nodeNum, uint32_t &nodeId, uint32_t &firstHop, uint8_t &numHops)

@param uint8_t nodeNum
The node ID to get information about
@param uint32_t &nodeId
Pointer to the variable that should be filled with the node ID found for requested info
@param uint32_t &firstHop
Pointer to the variable that should be filled with the first hop address if the requested info belongs to a indirect node
@param uint8_t &numHops
Pointer to the variable that should be filled with the number of hops to reach the node
@return bool result
True if the node information could be found
False if the node does not exist in the map

This routine is for showing the map either on a display or print out over Serial. It returns the stored route information for a node and fills the variables with the information. If the requested node ID could not be found in the map, the routine returns false.

Important!

To avoid collision of access to the map between the main application and the Mesh handler a semaphore is defined. Before accessing the router functions a request to take the semaphore has to be called. Only if the semaphore could be taken, the router functions can be called. Immediately after that the semaphore must be released. Here is an example how to use the semaphore from the main application to display the current Mesh map:

// Request access to the map by taking the semaphore
if (xSemaphoreTake(accessNodeList, (TickType_t)1000) == pdTRUE)
{ // The access was granted, get the number of nodes in the map numElements = numOfNodes(); Serial.printf("%d nodes in the mapn", numElements + 1); Serial.printf("Node #01 id: %08Xn", deviceID); for (int idx = 0; idx < numElements; idx++) { getNode(idx, nodeId[idx], firstHop[idx], numHops[idx]); } // Release access to nodes list xSemaphoreGive(accessNodeList); // Display the nodes for (int idx = 0; idx < numElements; idx++) { if (firstHop[idx] == 0) { Serial.printf("Node #%02d id: %08X directn", idx + 2, nodeId[idx]); } else { Serial.printf("Node #%02d id: %08X first hop %08X #hops %dn", idx + 2, nodeId[idx], firstHop[idx], numHops[idx]); } }
}
else
{ Serial.println("Could not access the nodes list");
}

Part 3 – The Mesh handler

The Mesh handler is responsible for handling everything related to the Mesh.

  • It will receive data packages over LoRa and handle them according to their type.
  • It will send out periodically the nodes map to other nodes to keep the Mesh alive and updated.
  • It will send data packages submitted from the main application over LoRa
  • It will periodically check the map for expired nodes and update the map if required.

The functions of the Mesh handler

A few words and some code extracts from the Mesh handler code.

void initMesh(MeshEvents_t *events, int numOfNodes)

@param MeshEvents_t *events
Pointer to a list of callback functions (currently only callback for data received)
@param int numOfNodes
Number of nodes the Mesh handler supports. With this value you can control the memory consumption of the handler.

This functions initializes the Mesh handler. It setup the callback function for the LoRa radio and initializes the radio with the defined parameters. The LoRa parameters are defined in the file mesh.h

// LoRa definitions
#define RF_FREQUENCY 910000000 // Hz
#define TX_OUTPUT_POWER 22 // dBm
#define LORA_BANDWIDTH 2 // [0: 125 kHz, 1: 250 kHz, 2: 500 kHz, 3: Reserved]
#define LORA_SPREADING_FACTOR 7 // [SF7..SF12] [1: 4/5, 2: 4/6, 3: 4/7, 4: 4/8]
#define LORA_CODINGRATE 1 // [1: 4/5, 2: 4/6, 3: 4/7, 4: 4/8]
#define LORA_PREAMBLE_LENGTH 8 // Same for Tx and Rx
#define LORA_SYMBOL_TIMEOUT 0 // Symbols
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#define RX_TIMEOUT_VALUE 5000
#define TX_TIMEOUT_VALUE 5000

initMesh initializes as well the semaphore for the map access, allocates the memory needed for the map (based on the number of nodes requested) and initializes the queue that will be used to queue up outgoing data packages.

The last step in initMesh is to start the task that will handle the Mesh network events.

if (!xTaskCreate(meshTask, "MeshSync", 2048, NULL, 1, &meshTaskHandle))
{ Serial.println("Starting Mesh Sync Task failed");
}
else
{ Serial.println("Starting Mesh Sync Task success");
}

void meshTask(void *pvParameters)

@param void *pvParameters
Unused pointer to the task parameters

The meshTask is running in an endless loop where it checks if any LoRa events occured by calling

Radio.IrqProcess();

The events itself are handled by call back functions. How this works will be explained later.

The meshTask is taking care as well that in defined periods the node is

  • cleaning up the map from expired nodes
  • sending out its current known nodes map using the message queue
// Time to sync the Mesh???
if ((millis() - notifyTimer) >= syncTime)
{... if (!cleanMap()) {... }... if (!addSendRequest((dataMsg *)&syncMsg, subsLen)) { Serial.println("Cannot send map because send queue is full"); }
}

It checks as well if it is ok to switch to a more relaxed Mesh network sync time and if the processing of LoRa events got stuck.

// Time to relax the syncing???
if (((millis() - checkSwitchSyncTime) >= SWITCH_SYNCTIME) && (syncTime!= DEFAULT_SYNCTIME))
{...
} // Check if loraState is stuck in MESH_TX
if ((loraState == MESH_TX) && ((millis() - txTimeout) > 2000))
{...
}

And last not least, meshTask is checking if any outgoing message requests are pending in the message queue.

// Check if we have something in the queue
if (xQueuePeek(sendQueue, &queueIndex, (TickType_t)10) == pdTRUE)
{...
}

Sending a data package is not directly started. Instead as first step a “Channel Activity Detection” is started. CAD is checking if any other transmissions from other nodes are active. Only if it finds that no other node is sending, it will start the sending of the data package.

Radio.Standby();
SX126xSetCadParams(LORA_CAD_08_SYMBOL, LORA_SPREADING_FACTOR + 13, 10, LORA_CAD_ONLY, 0);
SX126xSetDioIrqParams(IRQ_RADIO_ALL, IRQ_RADIO_ALL, IRQ_RADIO_NONE, IRQ_RADIO_NONE);
Radio.StartCad();

bool addSendRequest(dataMsg *package, uint8_t msgSize)

@param dataMsg *package
Pointer to the data package to be sent
@param uint8_t msgSize
Size of the data package

This function adds a data package to the queue of messages that needs to be sent out. The meshTask will take care of this queue and make sure that no multiple send requests are started and that the LoRa frequency is available for sending.

The message queue is designed to hold up to 10 messages in the queue. This was sufficient during the test with 12 nodes, but you can adjust it by changing the value of SEND_QUEUE_SIZE

Two buffers are created to hold SEND_QUEUE_SIZE number of data packages and their size.

When called, the function will scan through the buffers to find an unused entry. If a free slot is found, the data is copied into the buffer of this slot together with the data size. Then the index of this slot is sent into the request queue. The meshTask will find that index from the request queue and copy the data package into the transmit buffer and start the sending sequence.

TheSoftware

Part 4 – The other stuff

There are a few more SW modules involved in this tutorial, but I will only go into details for the setup() and loop() functions.
Other functions, like the display support, BLE UART output and my logging functions are relatively simple and you can check how they work in the source codes.

The setup()

As in all Arduino applications, setup() is called once after a fresh start or reboot of the device. Here we define out unique device ID, which will be used to identify the device in the Mesh network, initialize BLE and display (if attached) and initialize the Alora RFM1262 LoRa hardware.

Creating the node ID.

Each node in the Mesh network needs a unique identifier. The easiest way to create such an identifier is to use the MAC address of the device. The library that I use under the hood for the LoRa communication (SX126x-Arduino) has a function called BoardGetUniqueId, that I use for this purpose. The function returns an byte array of 8 bytes that is different for each device. This byte array is then used to create the node ID.

See also  ESP32 DHT Server: Monitoring Temperature and Humidity
// Create node ID
uint8_t deviceMac[8]; BoardGetUniqueId(deviceMac); deviceID += (uint32_t)deviceMac[2] << 24;
deviceID += (uint32_t)deviceMac[3] << 16;
deviceID += (uint32_t)deviceMac[4] << 8;
deviceID += (uint32_t)deviceMac[5]; myLog_n("Mesh NodeId = %08lX", deviceID);

Next is the initialization of the display and BLE UART functions

#ifdef HAS_DISPLAY initDisplay();
#endif // Initialize BLE initBLE();

As you can see I use #define compiler directives to integrate or skip certain parts of the code.

Last step is the initialization of the LoRa transceiver HW and start the Mesh network handler.

Depending on the used HW the LoRa transceiver initialization is slightly different. If the target HW is an ESP32 or an Adafruit nRF52 Feather board, the LoRa library needs to know the assignment for the GPIOs that are connecting the ESP32 or nRF52 with the SX1262 transceiver. If the target HW is the ISP4520 module the initialization is much easier, because the connections are predefined in the library.

	// Initialize the LoRa
#if defined(ESP32) || defined(ADAFRUIT) // Define the HW configuration between MCU and SX126x hwConfig.CHIP_TYPE = SX1262_CHIP; // eByte E22 module with an SX1262 hwConfig.PIN_LORA_RESET = PIN_LORA_RESET; // LORA RESET hwConfig.PIN_LORA_NSS = PIN_LORA_NSS; // LORA SPI CS hwConfig.PIN_LORA_SCLK = PIN_LORA_SCLK; // LORA SPI CLK hwConfig.PIN_LORA_MISO = PIN_LORA_MISO; // LORA SPI MISO hwConfig.PIN_LORA_DIO_1 = PIN_LORA_DIO_1; // LORA DIO_1 hwConfig.PIN_LORA_BUSY = PIN_LORA_BUSY; // LORA SPI BUSY hwConfig.PIN_LORA_MOSI = PIN_LORA_MOSI; // LORA SPI MOSI hwConfig.RADIO_TXEN = RADIO_TXEN; // LORA ANTENNA TX ENABLE hwConfig.RADIO_RXEN = RADIO_RXEN; // LORA ANTENNA RX ENABLE hwConfig.USE_DIO2_ANT_SWITCH = true; // Example uses an eByte E22 module which uses RXEN and TXEN pins as antenna control hwConfig.USE_DIO3_TCXO = true; // Example uses an eByte E22 module which uses DIO3 to control oscillator voltage hwConfig.USE_DIO3_ANT_SWITCH = false; // Only Insight ISP4520 module uses DIO3 as antenna control if (lora_hardware_init(hwConfig)!= 0) { myLog_e("Error in hardware init"); }
#else // ISP4520 if (lora_isp4520_init(SX1262)!= 0) { myLog_e("Error in hardware init"); }
#endif

Then we tell the Mesh network handler which function should be called if LoRa data was received.

MeshEvents.DataAvailable = OnLoraData;

The Mesh handler is running as a independent task in the background, handling the LoRa communication (sending and receiving) without interaction with the loop() task. The OnLoraData() function will be called from the Mesh handler if new data is available from the LoRa Mesh network.

Last thing here is to start the Mesh handler. As the nRF52 has less available heap memory, the maximum number of nodes is limited to 30. On an ESP32, we can set this to the maximum possible number of 48 nodes.

	// Initialize the LoRa Mesh
#ifdef ESP32 initMesh(&MeshEvents, 48);
#else initMesh(&MeshEvents, 30);
#endif

The OnLoraData callback function

Whenever a LoRa data package was received, this function is called with a pointer to the data, the size of the data and some transmission parameters.

Usually you would transfer the received data into a buffer and inform the loop() function about it. Then data handling would be done inside the loop().

But to keep the tutorial simple, we just print out the received data over the Serial port.

/** * Callback after a LoRa package was received * @param payload * Pointer to the received data * @param size * Length of the received package * @param rssi * Signal strength while the package was received * @param snr * Signal to noise ratio while the package was received */
void OnLoraData(uint8_t *rxPayload, uint16_t rxSize, int16_t rxRssi, int8_t rxSnr)
{ Serial.println("-------------------------------------"); Serial.println("Got"); for (int idx = 0; idx < rxSize; idx++) { Serial.printf("%02X ", rxPayload[idx]); } Serial.printf("nn%sn", rxPayload); Serial.println("-------------------------------------");
#if defined(HAS_DISPLAY) || defined(RED_ESP) digitalWrite(LED_BUILTIN, LOW);
#else digitalWrite(LED_BUILTIN, HIGH);
#endif
#ifdef ESP32 ledOffTick.detach(); ledOffTick.once(1, ledOff);
#else timer.attachInterrupt(&ledOff, 1000 * 1000); // microseconds
#endif
}

The loop()

As mentioned above, the loop() would normally include some data handling tasks, but in this simple tutorial we just print every 30 seconds the current list of known nodes to the Serial port (and display if available), select a random node and send a package to this node.

void loop()
{ delay(30000); Serial.println("---------------------------------------------"); if (xSemaphoreTake(accessNodeList, (TickType_t)1000) == pdTRUE) { numElements = numOfNodes(); Serial.printf("%d nodes in the mapn", numElements + 1); Serial.printf("Node #01 id: %08Xn", deviceID); if (bleUARTisConnected) { char sendData[512] = {0}; int sendLen = snprintf(sendData, 512, "%d nodes in the mapn", numElements + 1); bleUartWrite(sendData, sendLen); sendLen = snprintf(sendData, 512, "Node #01 id: %08Xn", deviceID); bleUartWrite(sendData, sendLen); }
#ifdef HAS_DISPLAY dispWriteHeader(); char line[128]; // sprintf(line, "%08X", deviceID); sprintf(line, "%02X%02X", (uint8_t)(deviceID >> 24), (uint8_t)(deviceID >> 16)); dispWrite(line, 0, 11);
#endif for (int idx = 0; idx < numElements; idx++) { getNode(idx, nodeId[idx], firstHop[idx], numHops[idx]); } // Select random node to send a package getRoute(nodeId[random(0, numElements)], &routeToNode); // Release access to nodes list xSemaphoreGive(accessNodeList); // Prepare data outData.mark1 = 'L'; outData.mark2 = 'o'; outData.mark3 = 'R'; if (routeToNode.firstHop!= 0) { outData.dest = routeToNode.firstHop; outData.from = routeToNode.nodeId; outData.type = 2; Serial.printf("Queuing msg to hop to %08X over %08Xn", outData.from, outData.dest); if (bleUARTisConnected) { char sendData[512] = {0}; int sendLen = snprintf(sendData, 512, "Queuing msg to hop to %08X over %08Xn", outData.from, outData.dest); bleUartWrite(sendData, sendLen); } } else { outData.dest = routeToNode.nodeId; outData.from = deviceID; outData.type = 1; Serial.printf("Queuing msg direct to %08Xn", outData.dest); if (bleUARTisConnected) { char sendData[512] = {0}; int sendLen = snprintf(sendData, 512, "Queuing msg direct to %08Xn", outData.dest); bleUartWrite(sendData, sendLen); } } int dataLen = MAP_HEADER_SIZE + sprintf((char *)outData.data, ">>%08X<<", deviceID); // Add package to send queue if (!addSendRequest(&outData, dataLen)) { Serial.println("Sending package failed"); if (bleUARTisConnected) { char sendData[512] = {0}; int sendLen = snprintf(sendData, 512, "Sending package failedn"); bleUartWrite(sendData, sendLen); } } // Display the nodes for (int idx = 0; idx < numElements; idx++) {
#ifdef HAS_DISPLAY if (firstHop[idx] == 0) { // sprintf(line, "%08X", nodeId[idx]); sprintf(line, "%02X%02X", (uint8_t)(nodeId[idx] >> 24), (uint8_t)(nodeId[idx] >> 16)); } else { // sprintf(line, "%08X*", nodeId[idx]); sprintf(line, "%02X%02X*", (uint8_t)(nodeId[idx] >> 24), (uint8_t)(nodeId[idx] >> 16)); } if (idx < 4) { dispWrite(line, 0, ((idx + 2) * 10) + 1); } else if (idx < 9) { dispWrite(line, 42, ((idx - 3) * 10) + 1); } else { dispWrite(line, 84, ((idx - 8) * 10) + 1); } #endif if (firstHop[idx] == 0) { Serial.printf("Node #%02d id: %08X directn", idx + 2, nodeId[idx]); if (bleUARTisConnected) { char sendData[512] = {0}; int sendLen = snprintf(sendData, 512, "Node #%02d id: %08LX directn", idx + 2, nodeId[idx]); bleUartWrite(sendData, sendLen); } } else { Serial.printf("Node #%02d id: %08X first hop %08X #hops %dn", idx + 2, nodeId[idx], firstHop[idx], numHops[idx]); if (bleUARTisConnected) { char sendData[512] = {0}; int sendLen = snprintf(sendData, 512, "Node #%02d id: %08X first hop %08X #hops %dn", idx + 2, nodeId[idx], firstHop[idx], numHops[idx]); bleUartWrite(sendData, sendLen); } } }
#ifdef HAS_DISPLAY dispUpdate();
#endif } else { Serial.println("Could not access the nodes list"); } Serial.println("---------------------------------------------");
}

And that’s it. If you have any questions, suggestions or problems with this tutorial, leave a message here or in my Github repository.

Frequently Asked Questions

How is a LoRa mesh different from a LoRaWAN star network?
LoRaWAN sends data point-to-point from a node to the nearest gateway. A mesh network has nodes that can relay messages through other nodes, extending coverage past the range of any single gateway.
Does LoRa mesh need internet to work?
No. A LoRa mesh works on its own radio. Internet is only needed if you want to bridge mesh data to a cloud server through one of the nodes (a gateway).
What library handles LoRa mesh routing?
Common choices include RadioLib with custom routing code, Meshtastic (a turnkey mesh firmware), and Reticulum. The article walks through the routing logic step by step using the Alora RFM1262.
How many hops can a LoRa mesh handle?
Most home-built meshes work well with 2-4 hops. Beyond that, latency grows and the chance of dropped packets increases. Meshtastic supports up to 7 hops by default.
Can I mix RFM1262 nodes with RFM95 nodes in the same mesh?
Only if all nodes are configured with matching frequency, bandwidth, spreading factor, and coding rate, and the firmware uses LoRa modulation (not LoRaWAN). Mixing chips means picking a common subset of features.