The local makerspace has a project for an upcoming light parade that will be held in the old ex-industrial area where we have our space. The idea is to have a bunch of ESP32 devices meshed together with RGB LEDs creating synchronized effects across the parade route. To make this work, I needed some nodes to always be root nodes (connected to the router) and other nodes to always be non-root nodes (connected to the mesh, never becoming root). The ESP-IDF mesh API has functions like esp_mesh_fix_root() and esp_mesh_set_type() that seem like they should do exactly this, but as it turns out, they don’t work reliably. Here’s what I learned the hard way.
The Problem
This seemed straightforward, the API has functions for this, right? Well, not quite. The ESP-WIFI-MESH implementation is fundamentally designed for self-organizing networks, and trying to force static node status conflicts with this design at a deep level.
Understanding the Mesh Networking IE
Before diving into what I tried, it’s important to understand how mesh networks share state. The mesh networking Information Element (IE) is a data structure embedded in Wi-Fi beacons that carries network state information. All mesh nodes broadcast this IE, allowing other nodes to discover the network and understand its current state.
Key flags in this IE include things like MESH_ASSOC_FLAG_ROOT_FIXED (is fixed root enabled?), MESH_ASSOC_FLAG_VOTE_IN_PROGRESS (is root election happening?), and MESH_ASSOC_FLAG_NETWORK_FREE (is there a root at all?).
Here’s the kicker: when a device joins a mesh network, it reads the parent’s mesh networking IE and updates its own settings to match. So even if you call esp_mesh_fix_root(true) locally, if you join a network where fixed root is disabled, your device will update to match. The mesh networking IE can override your local API settings.
There’s also a timing issue – there’s a delay between calling mesh APIs and the mesh networking IE being updated and broadcast. This creates race conditions where your API call might succeed, but the network state hasn’t propagated yet.
Trying to Force Root Nodes
First Attempt: Just Fix the Root
My first approach was simple, just call esp_mesh_fix_root(true) before starting the mesh:
esp_mesh_fix_root(true);
esp_mesh_start();This enables the “Fixed Root Setting” which disables automatic root node election via voting. The setting gets propagated through the mesh network via the mesh networking IE in beacons. When fixed root is enabled, the mesh stack won’t initiate root election voting.
But here’s the problem: I also needed child nodes to connect to this root. And that’s where things got complicated.
The Self-Organization Problem
The critical challenge is managing self-organization. Self-organized networking is a feature where nodes autonomously scan, select, connect, and reconnect to other nodes and routers. It has three main functions:
- Selection or election of the root node (based primarily on RSSI from the router)
- Selection of a preferred parent node
- Automatic re-connection upon detecting disconnection
When self-organized networking is enabled, the ESP-WIFI-MESH stack internally makes calls to Wi-Fi APIs (scan, connect, disconnect). This is why you can’t call Wi-Fi APIs while self-organization is enabled – it would interfere with the mesh stack’s internal operations.
My initial attempt was to disable self-organization:
esp_mesh_set_self_organized(false, false);But this prevented child nodes from connecting to the root. The mesh soft-AP needs self-organization enabled to properly handle association requests from child nodes. Without it, the mesh stack doesn’t actively manage the AP’s connection state.
Enabling Self-Organization After Router Connection
So I tried a different approach – disable self-organization initially, then enable it after the router connected:
// Before mesh start
esp_mesh_set_type(MESH_ROOT);
esp_mesh_fix_root(true);
esp_mesh_set_self_organized(false, false);
esp_mesh_start();
// After router connection (IP_EVENT_STA_GOT_IP)
bool is_root_fixed = esp_mesh_is_root_fixed();
if (!is_root_fixed) {
esp_mesh_fix_root(true); // Re-fix if lost
}
vTaskDelay(pdMS_TO_TICKS(100)); // Delay for stability
esp_mesh_set_self_organized(true, false); // Enable with select_parent=falseThe select_parent=false parameter is supposed to maintain the current Wi-Fi state, for a root node already connected to router, it should stay connected. This mode should allow the mesh AP to accept child connections without causing the root to search for a new parent.
But root fixing kept getting lost when self-organization was enabled. The mesh stack’s internal state management would clear the fixed root flag during the transition. There’s a race condition between enabling self-organization and the mesh stack updating its internal state and broadcasting the updated IE.
I ended up having to constantly check and re-fix the root status in multiple event handlers, which was getting ridiculous.
Monitoring Root Status Everywhere
I had to add checks in multiple event handlers to monitor and enforce root status:
// In MESH_EVENT_STARTED handler
if (is_root_node_forced && !esp_mesh_is_root()) {
esp_restart(); // Violation detected - reboot
}
// In MESH_EVENT_PARENT_DISCONNECTED handler
if (is_root_node_forced && is_router_connected) {
bool is_root_fixed = esp_mesh_is_root_fixed();
if (!is_root_fixed) {
esp_mesh_fix_root(true); // Re-fix
}
}
// In MESH_EVENT_ROOT_SWITCH_ACK handler
if (is_root_node_forced && !esp_mesh_is_root()) {
esp_restart(); // Violation detected - reboot
}This was getting complex, and it still wasn’t reliable. The root status could be lost during network events like router disconnection, parent disconnection, or root switch events.
Trying to Force Non-Root Nodes
Forcing non-root nodes had its own set of problems.
First Attempt: Disable Self-Organization
My first thought was to disable self-organization to prevent root election:
esp_mesh_set_self_organized(false, false);But this also prevented connection to parent nodes. With self-organization disabled, the mesh stack stops all automatic scanning and connection attempts. You’d have to manually set a parent using esp_mesh_set_parent(), which requires knowing the exact parent’s SSID, channel, and BSSID – not practical for dynamic mesh networks.
Enable Self-Organization Without Router Connection
Next I tried enabling self-organization but preventing router connection:
esp_mesh_set_self_organized(true, false); // Enable self-org, disable router
esp_mesh_set_type(MESH_LEAF); // Set as leaf node
esp_mesh_fix_root(false); // Release root fixingBut this still allowed root election under certain network conditions. The root election algorithm runs independently of device type settings. If the router becomes unavailable or RSSI conditions change, the mesh stack may initiate root election even with these settings.
Full Self-Organization with Leaf Type
My final attempt was to enable full self-organization but set device type to leaf:
esp_mesh_set_self_organized(true, true); // Full self-organization
esp_mesh_set_type(MESH_LEAF); // Prevent root election
esp_mesh_fix_root(false); // Release root fixingBut the mesh stack could still override the leaf type during root election. Root election happens at the mesh protocol level, which can override application-level type settings. If no other suitable root candidate exists and the device has good router RSSI, it may be elected root despite being set as MESH_LEAF.
Why Root Election Overrides Everything
Understanding root election is key to understanding why forcing non-root status fails.
Root election uses a voting mechanism where devices vote for root candidates. Devices vote primarily based on RSSI from the external router. A device can only become root when it obtains a vote percentage that reaches the threshold (default 0.9).
Root election can be triggered by:
- Network startup when no root exists
- Root node failure or disconnection
- Router disconnection
- Network topology changes
During root election, the mesh stack evaluates all nodes as potential root candidates. Device type settings (MESH_LEAF, MESH_NODE) can be overridden if a device wins the vote. The election algorithm prioritizes network connectivity over manual type designations.
The MESH_LEAF type is more of a “preference” than a hard constraint – it indicates the device should not forward packets, but doesn’t prevent it from becoming root.
Why It All Failed
Root Node Issues
Self-Organization Conflict:
- If you enable self-organization with
select_parent=true, the root will search for a parent and disconnect from the router (because a root must give up its role to connect to a parent) - If you use
select_parent=false, it doesn’t reliably allow child connections – there’s a timing issue where the mesh networking IE may not immediately reflect that the root is ready to accept children - The mesh AP may not accept connections when self-organization is disabled
- If you enable self-organization with
Root Fixing Not Persistent:
esp_mesh_fix_root(true)can returnESP_OKbut the root status may not persist- Root fixing can be lost when
esp_mesh_set_self_organized()is called due to internal state changes - Network events can cause root to become unfixed as the mesh stack re-evaluates the fixed root setting
API Behaviour Inconsistency:
esp_mesh_is_root_fixed()may return false even after successfulesp_mesh_fix_root(true)- Root status verification requires constant polling and re-fixing
Non-Root Node Issues
Root Election Override:
esp_mesh_set_type(MESH_LEAF)doesn’t guarantee the device won’t become root- The mesh stack’s root election algorithm can override leaf type
- Network topology changes can trigger root election
Self-Organization Requirement:
- Disabling self-organization prevents parent connection
- Enabling self-organization allows root election
- There’s no configuration that allows parent connection while preventing root election
Fundamental Limitations
The ESP-IDF mesh implementation is designed for self-organizing networks. The mesh stack internally manages network state, making Wi-Fi API calls to scan, connect, and reconnect as needed. Self-organizing behaviour is the default and expected mode of operation.
Manual status forcing conflicts with this self-organizing nature. When self-organization is enabled, the mesh stack makes decisions based on network conditions (RSSI, topology, connectivity) rather than manual settings. You’re trying to impose static configuration on a dynamic system.
Network events trigger the mesh stack to re-evaluate network state and make autonomous decisions. Events like parent disconnection or router disconnection cause the mesh stack to re-scan, re-evaluate root candidates, and update network topology – potentially overriding your manual type or fixed root settings to maintain network connectivity.
The mesh networking IE carries network state information that can override local settings when devices join or network conditions change.
What I Learned
The ESP-IDF mesh API does not reliably support forcing node status. The fundamental issue is that the mesh stack is designed for self-organization, and manual forcing conflicts with this design. While the API provides functions like esp_mesh_fix_root() and esp_mesh_set_type(), their behaviour is not guaranteed to persist through network events.
I tried working around these limitations by:
- Enabling/disabling self-organization at different times
- Re-fixing root status in event handlers
- Monitoring status in multiple event handlers
- Rebooting on status violations
All of it proved unreliable and created complex, hard-to-maintain code that still didn’t guarantee the desired behaviour.
In the end, I removed all this functionality. Sometimes the best solution is to work with the system’s design rather than against it. If you need specific nodes to be root or non-root, you might need to design your network topology differently, or use application-level logic to influence (but not force) node behaviour.
The mesh networking IE and root election mechanisms operate at a level that’s designed to ensure network connectivity above all else. Trying to override this for static configuration just doesn’t work reliably.
Do you know how?
I eventually gave up on this approach and removed the functionality entirely. If you’ve managed to get forced mesh node status working reliably, I’d love to hear how you did it. My contact information is in the footer.