Production-ready mesh networking for ESP32 MicroPython devices.
Complete 5-layer protocol stack over ESP-NOW. Zero dependencies. ~18 KB RAM.
Like the underground fungal networks that connect entire forests, Mycelium creates resilient, self-healing mesh networks between ESP32 devices.
- Complete Protocol Stack — 5 layers (Physical through Application) in a single library
- AODV-lite Routing — On-demand route discovery with expanding ring search
- Reliable Delivery — Fragmentation, ACK/NACK, and automatic retransmission
- Built-in Services — Pub/Sub, RPC, File Transfer, and Diagnostics out of the box
- AES-128 CTR Encryption — Per-packet nonce, pre-shared key, zero-alloc encrypt/decrypt
- Viper Optimized — Critical paths hit 125k pkt/s encode/decode on ESP32
- Tiny Footprint — ~18 KB RAM, zero external dependencies
- Wide Hardware Support — ESP32, S2, S3, C3 with optional Long Range mode
| Layer | Name | Modules | Responsibility |
|---|---|---|---|
| 5 | Application | pubsub.py rpc.py file.py diag.py |
Pub/Sub, RPC, File Transfer, Diagnostics |
| 4 | Transport | transport.py packet.py crypto.py |
Reliable delivery, fragmentation, encryption |
| 3 | Routing | aodv.py routing.py forward.py |
AODV-lite route discovery, forwarding engine |
| 2 | Infrastructure | peer.py neighbor.py duplicate.py |
Peer management, neighbor tracking, dedup |
| 1 | Physical | adapter.py _fast.py |
ESP-NOW I/O, viper-optimized codec |
mesh.py orchestrates all layers and provides the unified API.
Copy the umesh/ directory to your ESP32:
# Using mpremote
mpremote connect /dev/ttyUSB0 cp -r umesh/ :umesh/
# Using Thonny
# Drag and drop the umesh/ folder to your deviceimport asyncio
from umesh.mesh import Mesh
from umesh.config import MeshConfig
async def main():
mesh = Mesh(MeshConfig(channel=6))
await mesh.start()
# Send a message to node 0x0002
await mesh.send(0x0002, b"Hello mesh!")
# Keep running
while True:
await asyncio.sleep(1)
asyncio.run(main())import asyncio
from umesh.mesh import Mesh
from umesh.config import MeshConfig
async def main():
mesh = Mesh(MeshConfig(channel=6))
# Register callback for incoming data
def on_message(src_id, payload, rssi):
print(f"From 0x{src_id:04X}: {payload} (RSSI: {rssi})")
mesh.on_data(on_message)
await mesh.start()
while True:
await asyncio.sleep(1)
asyncio.run(main())from umesh.config import MeshConfig
config = MeshConfig(
channel=6,
enable_encryption=True,
psk=b"16-byte-key!!!!!" # Exactly 16 bytes
)All nodes must share the same PSK. Encryption is transparent — the API stays identical.
# Subscriber node
mesh.subscribe("sensors/temp", lambda topic, data, src:
print(f"{topic}: {data}"))
# Publisher node
mesh.publish("sensors/temp", b"23.5")# Server node
mesh.rpc_register("get_temp", lambda: b"23.5")
# Client node
result = await mesh.rpc_call(0x0002, "get_temp")
print(result) # b"23.5"# Sender
await mesh.send_file(0x0002, "config.json", file_data)
# Receiver
mesh.on_file(lambda name, data, src:
print(f"Received {name} ({len(data)} bytes)"))# Ping a node
rtt = await mesh.ping(0x0002)
print(f"RTT: {rtt} ms")
# Network statistics
stats = mesh.stats()14-byte header with up to 236 bytes payload (250 bytes total — ESP-NOW limit):
0 1 2 3 4 5 6 7 8 9 10 11 12 13
+---------+---------+----+----+----+----+----+----+----+----+----+----+----+----+
| Ver|Type| Flags | Packet ID |TTL |Hops| Source ID | Dest ID | Frag Info |CRC8|
+---------+---------+-----------+----+----+-----------+-----------+--------+---+----+
4b 4b 8b 16b 8b 8b 16b 16b 12b+4b+4b+4b 8b
16 packet types: DATA, DATA_FRAG, ACK, NACK, RREQ, RREP, RERR, HELLO, PING, PONG, TOPO, SYNC, PUB, SUB, RPC, CTRL.
All parameters are set via MeshConfig:
| Parameter | Type | Default | Description |
|---|---|---|---|
node_id |
int |
auto | 16-bit node ID (derived from MAC if None) |
channel |
int |
1 |
WiFi channel (1-14) |
lr_mode |
bool |
False |
Enable ESP-NOW Long Range mode |
hello_interval |
int |
10000 |
HELLO beacon interval (ms) |
route_timeout |
int |
120000 |
Route entry expiration (ms) |
max_ttl |
int |
7 |
Maximum Time-To-Live (1-15) |
peer_slots |
int |
19 |
Maximum ESP-NOW peer slots (1-19) |
rx_buf |
int |
2048 |
Receive buffer size (bytes) |
duplicate_window |
int |
32 |
Duplicate detection window size |
enable_encryption |
bool |
False |
Enable AES-128 CTR encryption |
psk |
bytes |
None |
Pre-Shared Key (16 bytes) |
| Metric | Value |
|---|---|
| Packet encode/decode | ~8 us / 125k pkt/s |
| Duplicate check | ~5 us / 200k checks/s |
| RAM footprint | ~18 KB (default config) |
| Max message size | 3,540 bytes (15 fragments) |
| Max TTL | 15 hops |
| Addressable nodes | 65,534 (16-bit) |
| Board | Standard | Long Range | Notes |
|---|---|---|---|
| ESP32 | Yes | No | Original silicon |
| ESP32-S2 | Yes | Yes | |
| ESP32-S3 | Yes | Yes | Recommended |
| ESP32-C3 | Yes | Yes | RISC-V core |
Requirements: MicroPython >= 1.20
mycelium/
umesh/ # Core library
__init__.py
mesh.py # Main orchestrator
config.py # MeshConfig with validation
const.py # Protocol constants
adapter.py # ESP-NOW adapter (Layer 1)
_fast.py # Viper/native optimized codec
peer.py # Peer manager (Layer 2)
neighbor.py # Neighbor table (Layer 2)
duplicate.py # Duplicate filter (Layer 2)
aodv.py # AODV-lite route discovery (Layer 3)
routing.py # Routing table (Layer 3)
forward.py # Forwarding engine (Layer 3)
packet.py # Packet codec (Layer 4)
transport.py # Reliable transport (Layer 4)
crypto.py # AES-128 CTR encryption (Layer 4)
pubsub.py # Pub/Sub messaging (Layer 5)
rpc.py # Remote Procedure Call (Layer 5)
file.py # File transfer (Layer 5)
diag.py # Diagnostics (Layer 5)
utils.py # Shared utilities
tests/ # Test suite (CPython)
examples/ # Example scripts
docs/ # Documentation
| Example | Description |
|---|---|
01_adapter_basic.py |
Broadcast communication between ESP32 nodes |
02_adapter_unicast.py |
Point-to-point unicast with peer registration |
| Example | Description |
|---|---|
neighbor_example.py |
Neighbor discovery and RSSI tracking |
duplicate_filter_example.py |
Duplicate packet detection |
forwarding_example.py |
Multi-hop packet forwarding |
| Example | Description |
|---|---|
transport_example.py |
Reliable delivery with ACK/retransmit |
crypto_example.py |
Encrypted mesh communication |
fast_example.py |
Viper-optimized codec benchmarks |
packet_demo.py |
Packet encoding/decoding demo |
| Example | Description |
|---|---|
example_pubsub.py |
Pub/Sub topic messaging |
rpc_example.py |
Remote Procedure Call |
file_transfer_example.py |
File transfer between nodes |
example_diagnostics.py |
Ping, stats, topology |
| Example | Description |
|---|---|
simple_mesh.py |
Minimal mesh setup |
complete_example.py |
Full-featured mesh node |
custom_config.py |
Custom configuration options |
- Packet Module — Packet format and codec API
- Pub/Sub Integration — Pub/Sub messaging guide
- File Transfer Flow — File transfer protocol details
Contributions are welcome! See CONTRIBUTING.md for guidelines.
For security concerns and vulnerability reporting, see SECURITY.md.
This project is licensed under the Apache License 2.0 — see the LICENSE file for details.
- ESP-NOW by Espressif Systems
- MicroPython project
- AODV (RFC 3561) — inspiration for the routing protocol