LoRaMesher Mesh Networking Library

repository·main·Indexed 43 days ago

https://github.com/loramesher/loramesher

A C++20 mesh networking library for LoRa nodes on ESP32 platforms. It implements a TDMA-based distance-vector routing protocol for deterministic slot scheduling and power efficiency. It leverages RadioLib for hardware abstraction, supporting SX1262, SX1268, SX1276, SX1278, and SX1280 modules. Key features include automatic route discovery, network manager election, capability-aware discovery, and broadcast messaging with TTL-based flooding.

Tokens
49.9K
Snippets
114
Records
187
Agent score
54%

What's inside LoRaMesher

  1. Overview of LoRaMesher

    main

    LoRaMesher is a C++20 mesh networking library designed for LoRa nodes. It implements a distance-vector routing protocol based on Time Division Multiple Access (TDMA) to provide deterministic slot scheduling, allowing nodes to sleep when not transmitting.

    Key features include:

    • Mesh routing: Automatic route discovery and maintenance.
    • TDMA superframe: Deterministic scheduling for power efficiency.
    • Auto join: Automatic network formation and discovery.
    • Network Manager (NM) election: Distributed election process with configurable priority.
    • Runtime role changes: Ability to promote/demote nodes between NETWORK_MANAGER and NODE_ONLY using SetNodeRole().
    • Capability-aware discovery: Find nodes or gateways based on capability bitmaps using GetClosestGateway or GetClosestNodeByCapability.
    • Multi-module support: Compatible with SX1262, SX1268, SX1276, SX1278, and SX1280 via RadioLib.
    • Broadcast messaging: TTL-based flooding with de-duplication.
    • Memory safety: No heap allocations per packet on ESP32; tested with ASAN, UBSAN, and TSAN.
  2. Understand the LoRaMesher Message Type Organization

    main

    LoRaMesher uses an 8-bit MessageType field organized into a bit-field structure for efficient parsing and systematic extension.

    • High Nibble (Bits 7-4): Represents the Main Message Category (e.g., DATA_MSG, CONTROL_MSG, ROUTING_MSG, SYSTEM_MSG).
    • Low Nibble (Bits 3-0): Represents the Subtype within that category.

    This structure allows for 16 main categories with 16 subtypes each, totaling 256 possible message types. Developers can quickly determine a message's category using bit masking.

    // Efficiently determine the main category using bit masking
    MessageType main_type = static_cast<MessageType>(message_type & 0xF0);
    if (main_type == MessageType::SYSTEM_MSG) {
        // Handle system message
    }
  3. Understand the impact of unidirectional links on TDMA

    main

    Unidirectional links break the assumptions of the distributed TDMA algorithm.

    • The Problem: For TDMA slot alignment to work, both nodes in a link must agree on the neighbor relationship.
    • The Failure: If Node A thinks Node B is a direct neighbor, it allocates a DATA TX slot to send to B. However, if Node B does not see Node A as a direct neighbor, it will allocate that same slot as SLEEP (radio off).
    • The Result: Node A transmits, but Node B's radio is off. Even if an intermediate relay receives the packet, it will ignore it because the next_hop does not match its address.

    Using the Unidirectional Link Detection mechanism ensures that the network routes around these dead links by preferring multi-hop paths where both sides have proper TDMA slot alignment.

  4. How Network Merging works (Proposed Design)

    main

    The Network Merge feature allows two independent mesh networks to combine into a single primary network when they come within range.

    Decision Logic

    Merging is deterministic: the network with the lower Network Manager ID is designated as the Primary network. The network with the higher ID becomes the Secondary network and must migrate its nodes to the primary.

    The Merge Lifecycle

    1. Detection: Nodes use periodic FOREIGN_DISCOVERY_RX slots to listen for foreign SYNC_BEACONs. When an edge node detects a foreign beacon, it sends a FOREIGN_NETWORK_DETECTED message to its own Network Manager.
    2. Negotiation: The two Network Managers communicate (via a bridge node) using NETWORK_MERGE_PROBE and NETWORK_MERGE_REQUEST messages. The Primary NM responds with a NETWORK_MERGE_RESPONSE containing a MergeResponseStatus (e.g., ACCEPTED, REJECTED_CAPACITY).
    3. Announcement: The Secondary NM broadcasts a NETWORK_MERGE_ANNOUNCEMENT to its nodes, providing a migration_countdown_ and slot_offset_.
    4. Migration: Nodes transition to the NETWORK_MERGING state. They listen for the primary network's timing, and at their assigned migration slot, they send a JOIN_REQUEST to the primary Network Manager. Upon receiving a JOIN_RESPONSE with ACCEPTED, they transition to NORMAL_OPERATION in the new network.
  5. Understand the TDMA Superframe and Network Synchronization

    main

    Network synchronization is managed via a TDMA (Time Division Multiple Access) superframe system. This system ensures coordinated communication through slot-based scheduling.

    Synchronization Components:

    • Superframe System: Configurable slot durations with automatic slot transitions.
    • Time Sync: Achieved via the Network Manager through routing messages.
    • Role Management: Includes network manager election and role management.
    • Scheduling: A slot-based message scheduling system.

    Current Limitations:

    • Sync drift thresholds are currently hardcoded (not yet configurable).
    • Lacks adaptive slot sizing and clock drift compensation algorithms.
  6. Understand the LoRaMesher Frame Structure

    main

    All LoRaMesher messages are encapsulated within a LoRa Physical Layer frame. The structure consists of the LoRa PHY Header, the LoRaMesher Frame, and the LoRa PHY CRC.

    Every LoRaMesher frame begins with a BaseHeader (6 bytes), which is common to all message types. Following the BaseHeader, messages include Message-Specific Extensions depending on the Message Type.

    ┌─────────────────────────────────────────────────────────────────┐
    │                        LoRa PHY Header                         │
    ├─────────────────────────────────────────────────────────────────┤
    │                     LoRaMesher Frame                           │
    ├─────────────────────────────────────────────────────────────────┤
    │                        LoRa PHY CRC                            │
    └─────────────────────────────────────────────────────────────────┘
  7. How Unidirectional Link Detection works

    main

    In LoRa mesh networks, physical obstructions or antenna orientation can create 'unidirectional links' where Node A can hear Node B, but Node B cannot hear Node A. This is critical because the distributed TDMA algorithm requires both nodes to recognize each other as direct neighbors to align DATA TX slots. If a link is unidirectional, the sender will transmit during a slot that the receiver has marked as SLEEP, leading to total data loss.

    To prevent this, the library implements a detection algorithm that monitors the reception_quality reported by peers in their routing table broadcasts.

    Detection Mechanism

    1. Direct Neighbor Check: When Node A receives a routing table from Node B, it checks if B lists A as a direct neighbor (hop_count == 1).
    2. Reception Quality: It specifically looks at the reception_quality field (the sender's raw EWMA reception rate). Multi-hop entries are ignored for this check.
    3. Grace Period: The library tolerates remote_link_quality == 0 for the first 2 routing table exchanges to allow for initial discovery.
    4. Confirmation: If after messages_expected >= 3 exchanges the peer still does not list the local node as a direct neighbor, the link is confirmed as unidirectional.
    5. Penalty Application: The link quality is penalized by dividing it by 4 (local_quality / 4). This increases the route cost, making a multi-hop bidirectional relay path more attractive than the failing direct 1-hop path.

    Recovery

    Recovery is automatic. If the link becomes bidirectional again (e.g., interference clears) and the peer starts listing the local node with remote_link_quality > 0, the penalty is removed and the quality returns to the standard weighted bottleneck formula.

  8. Implement the recommended Queued Receive pattern

    main

    To safely receive LoRa messages without starving the mesher's internal tasks or causing stack overflows, use a queue-based pattern. Instead of performing application logic inside the SetDataCallback, use the callback only to enqueue a message pointer into a dedicated queue. A separate application-owned task (ReceiveTask) should then dequeue and process these messages.

    Pattern Workflow:

    1. SetDataCallback: Allocates an AppMessage and enqueues it (non-blocking, 0 timeout). If the queue is full, the packet is dropped to ensure the mesher's internal tasks are never blocked.
    2. ReceiveTask: Dequeues and processes the message (blocks on portMAX_DELAY).

    This decoupling ensures that heavy processing occurs on a task you control, rather than on the mesher's internal radio/protocol stack.

    // Callback: Minimal and non-blocking
    mesher->SetDataCallback([](const AppMessage& msg) {
        AppMessage* new_msg = new AppMessage(msg);
        if (!receive_queue.push(new_msg, 0)) {
            delete new_msg;
            // Log drop
        }
    });
    
    // ReceiveTask: Dedicated processing
    void ReceiveTask(void* params) {
        while (true) {
            AppMessage* msg;
            if (receive_queue.pop(&msg, portMAX_DELAY) == os::QueueResult::kOk) {
                // Process msg...
                delete msg;
            }
        }
    }
  9. Understand the TDMA Superframe Structure

    main

    The LoRaMesher protocol uses a Time Division Multiple Access (TDMA) system organized into power-optimized superframes. Time is divided into specific slots assigned to different network functions. The allocation order is strictly: sync_beacon $\rightarrow$ control $\rightarrow$ data $\rightarrow$ sleep (elastic) $\rightarrow$ discovery (tail).

    Slot Types and Purposes

    Slot TypePurposePower State
    SYNC_BEACON_TXNetwork Manager (NM) original beacon or 1/2-hop forwardingTX (Specific nodes)
    CONTROL_TXNM routing table or specific node routingTX (Target), RX (Others)
    DATA_TXNode data transmissionTX (Target), RX (Neighbors)
    SLEEPElastic buffer to allow for timing marginsSLEEP
    DISCOVERY_RXNew-node discovery or NM_CLAIM receptionRX (All nodes, always at tail)

    Allocation Rules

    • Discovery Tail: DISCOVERY_RX slots are always at the end of the superframe to ensure mesh recovery.
    • Elastic Sleep: The SLEEP phase acts as a buffer between data and discovery. It shrinks (potentially to zero) to ensure the discovery tail is preserved.
    • Scaling:
      • Discovery count: (max_hops + 1) * 2
      • Sync count: (max_hops + 1)
      • Control count: Equals the sync beacon's node_count
      • Data count: Sum of active routing-table entries' allocated_data_slots.
    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                    POWER-OPTIMIZED SUPERFRAME STRUCTURE                      │
    │                              (Example: 20 slots)                           │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │                                                                             │
    │ Slot │ Type           │ Purpose              │ Power State                   │
    │ ──────────────────────────────────────────────────────────────────────────  │
    │  0   │ SYNC_BEACON_TX │ NM original beacon   │ TX (Network Manager only)    │
    │  1   │ SYNC_BEACON_TX │ 1-hop forwarding     │ TX (1-hop nodes only)         │
    │  2   │ SYNC_BEACON_TX │ 2-hop forwarding     │ TX (2-hop nodes only)         │
    │  3   │ CONTROL_TX     │ NM routing table     │ TX (Network Manager)          │
    │  4   │ CONTROL_TX     │ Node A routing       │ TX (Node A), RX (others)      │
    │  5   │ CONTROL_TX     │ Node B routing       │ TX (Node B), RX (others)      │
    │  6   │ DATA_TX        │ Node A data          │ TX (Node A), RX (neighbors)   │
    │  7   │ DATA_TX        │ Node B data          │ TX (Node B), RX (neighbors)   │
    │  8-17│ SLEEP          │ Elastic buffer       │ SLEEP (shrinks to honour tail) │
    │ 18-19│ DISCOVERY_RX   │ New-node / NM_CLAIM  │ RX (all nodes, always at tail)│
    │                                                                             │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ POWER CHARACTERISTICS:                                                      │
    │ • Active Slots: 10 (sync: 3, control: 3, data: 2, discovery: 2)           │
    │ • Sleep Slots: elastic — fill between data and discovery                   │
    │ • DISCOVERY_RX is always placed at the tail of the superframe              │
    │ • Configurable via TX duty cycle, min_sleep_fraction, churn_margin_slots   │
    └─────────────────────────────────────────────────────────────────────────────┘
  10. Understand LoRaMesher scalability and performance limits

    main

    When designing your network, consider the following architectural limits and performance characteristics:

    Scalability Limits:

    • Network Size: Up to 65,535 nodes (limited by 16-bit node IDs), though current implementation targets ~50 nodes.
    • Routes per Node: 40 (current) to 255 (design limit).
    • Hop Count Limit: 10 (current) to 255 (design limit) to prevent loops.
    • Message Queue Depth: 10 (current) to 255 (design limit).

    Memory Footprint (Approximate):

    • Static Memory: ~800 bytes per node.
    • Dynamic Memory: ~4KB per node (includes routing table and message queues).
    • Target Hardware: Fits comfortably on ESP32 with >100KB RAM available.
  11. Understand LoRaMesher routing cost and stability mechanisms

    main

    The protocol uses a composite cost formula combining hop count and link quality for route selection. Several mechanisms are implemented to ensure network stability:

    • Weighted Bottleneck Quality: Instead of using the minimum quality of a bidirectional link, it uses (min × 7 + avg × 3) / 10. This helps preserve direct links over 3-hop alternatives while still penalizing asymmetry.
    • Hard Unidirectional Penalty: If a link is confirmed to be unidirectional, its quality is set to 1 (minimum), resulting in a maximum cost of 65535. This ensures bidirectional alternatives are always preferred.
    • EWMA Link Quality: Uses an Exponentially Weighted Moving Average to track link quality.
    • Re-activation Hysteresis: Uses min_consecutive_for_reactivation to prevent rapid flapping of routes.
  12. How message flow works in LoRaMesher

    main

    When an application sends a message, it follows this sequence:

    1. Application calls SendMessage(data, destination) on the LoRaMesher API.
    2. LoRaMesher passes the message to the NetworkService to RouteMessage(message).
    3. NetworkService performs FindNextHop(destination) to determine the next node.
    4. NetworkService enqueues the message into the MessageQueueService using EnqueueMessage(message, slot).
    5. SuperframeService triggers a SlotTimer callback.
    6. SuperframeService instructs MessageQueueService to ProcessSlotQueue(currentSlot).
    7. MessageQueueService calls TransmitMessage(message) on the HardwareManager.
    8. HardwareManager sends the data via the radio hardware.