Alora RFM1262 – LoRa Mesh Network

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.

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 map\n", numElements + 1);
	Serial.printf("Node #01 id: %08X\n", 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 direct\n", idx + 2, nodeId[idx]);
		}
		else
		{
			Serial.printf("Node #%02d id: %08X first hop %08X #hops %d\n", 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.