embedded-nano-mesh
A tiny mesh networking protocol for small embedded devices.
embedded-nano-mesh allows you to build a self-healing mesh network
using very cheap microcontrollers and simple serial radio modules.
It is designed to run on extremely small devices such as:
- Arduino Nano / Uno / Mini
- small STM32 boards
- ESP8266 / ESP32
- Raspberry Pi Pico
- Linux devices (Raspberry Pi, Orange Pi, PCs)
The only thing required is a serial connection to a radio module.
No WiFi stack.
No heavy networking libraries.
Just a lightweight mesh protocol.
Why this library exists
Many mesh solutions require:
- powerful hardware
- complex configuration
- large memory footprint
embedded-nano-mesh is different.
It was built with the goal of running on very small and cheap boards.
You can create a mesh network using:
Microcontroller + Serial Radio Module
Example:
+--------------+ +--------------+
| Arduino Nano | | HC-12 |
| STM32 | + | JDY-41 | = Mesh node
| ESP8266 | | LoRa module |
| Linux PC | | etc. |
+--------------+ +--------------+
Connect multiple nodes together and they will automatically form a mesh network.
What you can build
This protocol works well for small distributed systems:
- Home automation
- Agricultural sensor networks
- Remote telemetry
- Industrial data collection
- Device-to-device messaging
- Remote control systems
Because nodes forward messages to each other, the network automatically extends its range.
Key features
no_stdcompatible- Works on very low-memory devices
- Runs on Linux and microcontrollers
- Works with any serial radio module
- Self-healing mesh network
- Configurable packet lifetime (hop limit)
- Duplicate packet filtering
- Very small protocol overhead
- Easy to port to new hardware
Quick start (5 minutes)
Add the library to your Cargo.toml:
embedded-nano-mesh = "3.1.0"
Then provide a serial interface using embedded-io.
Example for Linux:
embedded-nano-mesh-linux-io = "0.0.1"
Example usage:
let mut interface = new;
let mut mesh_node = ...;
mesh_node.send_to_exact.unwrap;
loop
That’s it.
Your device is now part of a mesh network.
How routing works
The protocol intentionally uses a very simple routing model.
When a node sends a packet:
- Nearby nodes receive it
- Each node decides whether to forward it
- The packet spreads through the network
This behaves similar to ripples in water.
To prevent infinite spreading, packets contain a lifetime (hop limit).
Each hop decreases the lifetime.
Example:
lifetime = 10
The packet can travel up to 10 nodes before it disappears.
Examples:
lifetime = 1 → only nearest devices
lifetime = 5 → medium network range
lifetime = 10 → large mesh
Avoiding duplicate packets
Mesh networks often suffer from packet echo.
embedded-nano-mesh prevents this using a duplicate filter.
Each node remembers recently seen packets:
(sender_id, packet_id)
If the same packet appears again within a short time window, it is automatically ignored.
This keeps the network from being flooded by echoes.
When a duplicate of an already-known packet is detected, the default
strategy (double_period) doubles the time window during which echoes are
ignored — a simple anti-flood backoff: the busier the network, the longer
echoes are suppressed. The alternative ignore strategy leaves the period
unchanged.
The filter remembers a limited number of packets at once. When its table is
full, the default overflow strategy (evict_oldest) drops the oldest entry
and accepts the new packet, so legitimate traffic is never silently lost.
Both the table size and the time window are configurable at compile time —
see "Compile-time configuration" below.
Version compatibility
Different versions of embedded-nano-mesh use the same wire protocol, so
nodes with different versions can coexist in one network.
The table below is generated by the test
tests/test_version_compatibility.rs and reflects the actual state of
cross-version compatibility:
| Scenario | Topology | Status |
|---|---|---|
| send_to_exact | v3 -> v2 | pass |
| send_to_exact | v2 -> v3 | pass |
| broadcast | v3 -> v2 | pass |
| broadcast | v2 -> v3 | pass |
| send_to_exact through relay | v3 -> v2 -> v3 | pass |
| ping-pong through relay | v2 -> v3 -> v2 | pass |
| transaction through relay | v2 -> v3 -> v2 | fail |
Known limitation: v2 send_with_transaction does not work when the
transaction packets have to transit through a v3 relay node, because the
v3 duplicate filter drops the FinishTransaction step (it reuses the
unique id of AcceptTransaction). Ping-pong and plain packet forwarding
are unaffected.
Examples
Example projects:
Arduino Nano example:
https://github.com/boshtannik/embedded-nano-mesh-arduino-nano-example
Linux example:
https://github.com/boshtannik/embedded-nano-mesh-linux-example
CLI tool:
https://github.com/boshtannik/embedded-nano-mesh-cli-tool
Platform support
Currently tested on:
- Arduino Nano
- Linux (PC / Raspberry Pi)
Expected to work on:
- STM32
- ESP8266
- ESP32
- Raspberry Pi Pico
- Windows
- macOS
Porting to new platforms is easy.
Porting to new hardware
The protocol communicates with radios through the embedded-io trait.
To port the library to a new platform you only need to implement:
embedded_io::Read
embedded_io::Write
for your serial interface.
That’s all.
Once your serial driver implements embedded-io, the mesh protocol will
work.
Managing packet collisions
To reduce packet collisions, it is recommended to use different
listen_period values for different nodes.
Example:
device 1 → 230 ms
device 2 → 240 ms
device 3 → 250 ms
This prevents devices from synchronizing their transmissions.
Protocol limits (default configuration)
- Payload size: 32 bytes
- Max addresses: 255 (configurable, see below)
- Packet queues: 5 packets (received, send and transit, configurable)
- Duplicate filter size: 8 packets
- Duplicate ignore period: 1000 ms
Compile-time configuration
The protocol is configurable at compile time, either via environment
variables or a .env file in the project root (see .env.example). Real
environment variables take precedence over .env.
| Variable | Meaning | Default |
|---|---|---|
ENM_RECEIVER_FILTER_DUPLICATE_IGNORE_PERIOD |
how long (ms) a packet id is remembered | 1000 |
ENM_RECEIVER_FILTER_REGISTRATION_SIZE |
how many packet ids can be remembered at once | 8 |
ENM_RECEIVER_FILTER_OVERFLOW_STRATEGY |
what to do when the table is full: evict_oldest or drop_new |
evict_oldest |
ENM_RECEIVER_FILTER_DUPLICATE_STRATEGY |
what to do on a duplicate: double_period or ignore |
double_period |
ENM_PACKET_QUEUE_SIZE |
size of the packet queues (received, send and transit) | 5 |
ENM_ADDRESS_BYTES |
wire size of the device address / lifetime field: 1 (u8), 2 (u16) or 4 (u32) |
1 |
When the table is full, evict_oldest (default) removes the oldest entry
and accepts the new packet, so legitimate new packets are never silently
dropped in busy networks. drop_new rejects the new packet instead.
When a duplicate of an already-known packet is detected, double_period
(default) doubles the ignore period (anti-flood backoff); ignore leaves it
unchanged.
ENM_ADDRESS_BYTES controls the size of the device address and the packet
lifetime on the wire:
1→u8: 255 addresses, max 255 hops (default)2→u16: 65535 addresses, max 65535 hops4→u32: 4 billion addresses, max 4 billion hops
Warning: any value other than 1 changes the wire format, so all nodes
in a network must be built with the same setting and cannot interoperate
with default (u8) builds.
Example:
ENM_RECEIVER_FILTER_REGISTRATION_SIZE=16 cargo build
ENM_PACKET_QUEUE_SIZE=10 cargo build
ENM_ADDRESS_BYTES=2 cargo build
The duplicate ignore period, the registration size, the queue size and the
address size are also configurable through a .env file (see .env.example).
Security
The protocol does not implement encryption.
If encryption is required, it should be implemented in the application layer.
Status
Current version: 3.1.0
Features:
- sending messages to an exact node (
send_to_exact) - broadcasting (
broadcast) - receiving messages (
receive) - hop-limited routing (
lifetime) - duplicate filtering
- intermediate node forwarding
- fully non-blocking: never stalls the user program loop
Delivery guarantees (transactions, ping-pong, acknowledgements, retries)
are intentionally not part of the protocol core. They are the
responsibility of the application layer and can be built on top of
send_to_exact / broadcast / receive.
Community
Telegram channel:
https://t.me/embedded_nano_mesh
GitHub:
Support the project
If this project helped you, you can support development.
Bitcoin:
bc1qc50tm0ppj3hh7fecd6d0rv8tdygy8uhe2cemzt
Buy me a coffee:
https://www.buymeacoffee.com/boshtannik
License
This project is licensed under:
- MIT
- Apache 2.0
- GPLv3
You may choose the license that best fits your needs.
Contributing
Contributions are welcome.
Please include in your pull request:
- Issue reference
- Short explanation of the fix
- Description of the solution