A self-healing mesh network for cheap 433 MHz ASK radio modules (the ~$2 FS1000A / XY-MK-5V pair), built on Arduino Uno/Nano.
Every board runs the same sketch and speaks the same standard packet. Nodes with PIR motion sensors send alerts to a master, which shows them on a 16x2 LCD and sounds a buzzer. If a node can't reach the master directly, its packets go through other nodes. When a relay drops out, the node finds a new one by itself.
[node 3] ))) [node 2] ))) [node 1] ))) [MASTER 0] LCD: "Motion! node 3"
\ /
))) [node 5] ))) [node 4] ))) ------ <- used automatically if node 2 dies
433 MHz ASK modules are about as cheap and simple as radios get: no Wi-Fi, no internet, no infrastructure. On their own, though, they're one-way and short-range. mesh-433 turns a handful of them into a sensor network that covers more ground than any one radio can, and keeps working when individual nodes fail.
Every board uses one standard packet, so any node can relay for any other:
| Field | Purpose |
|---|---|
| to / from / dest / origin | next hop, this hop's sender, final destination, original sender |
| type | data, ack, relay request, relay advert (the reserved control IDs 999 / 998 in the original sketches) |
| dataType | what the receiver should do with the data, e.g. 1 = show the payload on the LCD |
| sens1-4 | sensor slots, e.g. sens1 = 1 = PIR tripped, so the master sounds the buzzer |
| payload | 32 characters = both lines of a 16x2 LCD |
| ack flag | ask the next hop to acknowledge |
| hops | how many hops the sender is from the master (in relay adverts) |
| boot, seq | boot id (changes every power-up) and sequence number: together they identify a packet |
| ttl | hop limit |
Addressing: 0 is the master and every other node has its own ID.
Data frames are 56 bytes. Control frames (ack, relay request, relay advert) send only the 16-byte header, which cuts their time on air from about 0.4 s to 0.16 s at 2000 bps.
- Relay discovery: a node that needs a route broadcasts a relay request. Nodes that can reach the master answer after a random delay (so they don't all transmit at once) with their hop count. The node keeps the neighbour with the fewest hops as its relay.
- Better routes spread: when a node hears of a relay with fewer hops, it switches to it and re-advertises, so changes in the network spread outward.
- Failover: packets are acknowledged at each hop. If the ack doesn't come back, the node works through a retry sequence: resend, try the other route, ask for a new relay, then send via the new relay. Only if all of that fails does it raise a local alarm.
- Collision avoidance: ASK radios can't tell when someone else is transmitting, so timing does the work. Retries wait a random backoff that doubles each try, replies to a relay request are spread over 5 s, and a node drops its own reply if it hears a neighbour answer as well or better.
- Loop protection: a node never takes one of its own children as its relay. Packets carry a hop limit, and repeated packets are dropped, so a lost ack can't repeat an alert. Each node keeps a boot counter in EEPROM, so packets sent after a reboot are never mistaken for old ones.
- Commands down the mesh: each packet travelling up teaches the nodes on its path how to reach its sender, so the master can send commands back down (arm/disarm from the master's button).
Everything runs on millis() timers rather than delay(), so a node keeps listening while it waits.
firmware/mesh433/mesh.h the protocol: plain C++ with no Arduino headers, so it also runs on a PC
firmware/mesh433/mesh433.ino the board: radio, PIR, LCD, buzzer, arm/disarm button
test/sim.cpp desktop test harness: runs mesh.h over simulated lossy links
original/ earlier versions: 5RX/5TX (sensor -> master) and Node001 (first mesh version)
On an Uno it uses 16 KB of 32 KB flash and 1.4 KB of 2 KB RAM.
Libraries: RadioHead and LiquidCrystal I2C (Arduino Library Manager).
- Set
#define NODE_IDinmesh433.ino:0for the master,1, 2, 3...for the sensor nodes. - Wire it up (Uno/Nano):
| Part | Pin |
|---|---|
| 433 MHz receiver data | D11 |
| 433 MHz transmitter data | D12 |
| PIR output | D2 |
| Button (to GND) | A1 / D15 |
| TX LED, alert LED | D5, D6 |
| Buzzer | D7 |
| 16x2 LCD (I2C, 0x27) | A4 SDA, A5 SCL |
arduino-cli compile -b arduino:avr:uno firmware/mesh433 && arduino-cli upload -p /dev/ttyUSB0 -b arduino:avr:uno firmware/mesh433
A 17 cm wire antenna on each module makes a big difference to range.
make -C test test runs the protocol code on a PC against a radio model of RH_ASK at 2000 bps: real
time on air per frame, half duplex (a node is deaf while it transmits), and collisions (two frames
overlapping at a receiver are both lost), plus optional random loss.
| Test | Checks |
|---|---|
| line | routes form along a 3-hop chain, a packet arrives once |
| failover | kill the active relay, the node finds the other path and still delivers |
| lossy grid | 3x3 grid, collisions + 15% loss per link, 10 runs x 20 packets: no duplicates, nothing lost silently |
| storm | all 8 nodes trip at the same moment |
| isolated | no route at all: the packet is reported failed, nothing loops |
| downlink | the master sends a command down 2 hops |
| reboot | a power-cycled node's packets aren't mistaken for duplicates |
| millis wrap | routing and delivery across the 49.7-day millis() rollover |
| queue full | sends past the 4-slot queue are refused; every accepted one arrives |
| late ack | an ack that arrives during route discovery still completes the send |
| fan-out | the master arms 8 nodes through the 4-slot queue |
Typical results: lossy grid 96% delivered, storm 39/40, failover in about 40 s. The tests also pass under AddressSanitizer and UBSan.
Started in 2022 and revisited in 2023. The earliest surviving copies are the Arduino Cloud sketches
from March 2023 (original/), and the commit history starts there.
- 5RX / 5TX: PIR sensor nodes reporting to an LCD master, with acks and an alarm when the master doesn't answer.
- Node001: one sketch for every board. Adds relaying, relay discovery by hop count, and relay failover.
- firmware/ (current): the same design with binary packets instead of comma-separated text, no blocking delays, repeated packets dropped, collision avoidance, and commands sent down from the master.
The same problems show up anywhere sensors have to report without infrastructure:
- Perimeter and area sensors: unattended PIR, seismic or acoustic sensors reporting to one operator, like the PPSS-style sensor suites used to watch an area.
- Comms that degrade gracefully: no node is essential. When one is destroyed, jammed or out of battery, the network routes around it without anyone reconfiguring it.
- Cheap, disposable nodes: each node is a few dollars of parts, so losing some is acceptable.
- Authentication: an 8-byte message authentication code plus a counter on every packet, so alarms and arm/disarm commands can't be forged or replayed.
- Health monitoring: heartbeats every few minutes, with "node lost" and "jamming suspected" alerts on the master.
- Battery nodes: a 3.3 V Pro Mini, with sensor nodes sleeping until the PIR wakes them.
- Better radios: the
Radiointerface keeps the protocol separate from the hardware, so it can move to RFM69 or LoRa (SX127x), which can sense a busy channel and reach much further. - Field testing: delivery rate against distance, failover time and battery life, on real hardware.
- 433 MHz ASK can't tell when another node is already transmitting, so two nodes sending at once can collide. Random backoff and short control frames keep that rare.
- Frames are CRC-checked by RadioHead but not yet authenticated (see next steps).
- Packets are sent as raw little-endian structs. AVR, ARM, ESP and x86 are all little-endian.
- In Australia, 433.05-434.79 MHz falls under the ACMA LIPD class licence, which caps power at 25 mW EIRP.
MIT