← Home Few Bits
Cover image for TinyMesh — Stateless, Low‑Power Peer Discovery for Battery Sensors

TinyMesh — Stateless, Low‑Power Peer Discovery for Battery Sensors

Alex Solis Alex Solis ·

Problem

When you have dozens of tiny battery-powered sensors scattered around a house or workshop, discovery and maintenance become the annoying middle‑man. Full mesh stacks, pairing apps, or long-lived radios cost power and complexity. What if you just need the devices to find each other occasionally to exchange short status beacons or accept a configuration packet? TinyMesh is a minimal, stateless peer discovery pattern that fits in a few dozen bytes of code and keeps radios off most of the time.

Design Goals

  • Work on resource‑constrained MCUs with ~2–32 KB flash and single‑byte millisecond timers.
  • Keep average radio duty cycle below 1% for multi‑year battery life on coin cells.
  • No persistent neighbor tables or routing — discovery is ephemeral, enough for a brief exchange.
  • Tolerant of clock drift and radio collisions common on cheap hardware.

How It Works (High Level)

TinyMesh relies on periodic, short listen windows and randomized, short beacons. A node spends most time sleeping. Every T seconds it wakes and opens a listen slot of duration L. During this slot it listens for beacons and also transmits its own beacon at a random offset inside L. If it receives a beacon from a peer it immediately switches into a brief session mode to exchange a small payload. No addressing, no sessions kept after exchange — stateless.

Message Format

Keep packets tiny. Minimal format (all fields 1 byte unless noted):

  • 0x01 — protocol version
  • flags — 1 byte: request/response bits
  • id — 1–4 bytes: short node ID (use 2 bytes)
  • seq — 1 byte: sequence number to avoid processing duplicates
  • payload — 0–16 bytes depending on your use

Example: [0x01, 0x00, 0x12, 0x34, 0x05, 'T', 'E', 'M', 'P']

Timing Parameters

Pick T and L to balance latency and power. Typical values:

  • T = 30…300 s — discovery interval
  • L = 50…500 ms — listen window
  • tx_len ≈ 10…100 ms — time to transmit and brief listen for ACK

Duty cycle roughly equals (L + tx_len) / T. For example, T=300 s, L=100 ms, tx_len=20 ms gives ~0.04% duty cycle.

Dealing with Clock Drift

Cheap MCUs drift. TinyMesh approaches this by keeping listen windows wide enough to tolerate drift between intervals and by sending beacons at a random offset inside the listen window. If two nodes miss each other's slot, the next interval tries again. For deployments where nodes sleep for hours, optionally add a short (100–500 ms) active chamber every N intervals where the node stays awake to improve rendezvous probability.

Anti‑Collision and Backoff

Multiple radios transmitting in the same small area will collide. Use these simple mitigations:

  • Randomize the beacon offset uniformly inside L.
  • If you detect an incomplete packet (CRC fail), wait a random backoff 0…R ms and retry in the same listen slot if time remains.
  • Limit number of retries to avoid staying on too long.

Session Exchange

When a beacon is received, switch to a short session exchange. Keep exchanges tiny (1–8 bytes) and bounded in time. A standard pattern:

  1. Beacon received. If payload requests data, send small response packet within tx_len.
  2. If an ACK is requested, send a one‑byte ACK and stop.
  3. If more data is needed, use a simple stop‑and‑wait sequence with seq numbers and a max of 4 packets, then tear down.

Because the system is stateless, both sides drop session state after the exchange completes.

Minimal Pseudocode

Core loop can fit in a few dozen lines. Pseudocode:

loop:
sleep(T - jitter)
radio.on()
start = now()
tx_time = start + random(0, L - tx_len)
while(now() - start < L):
if now() >= tx_time and not tx_done:
radio.send(beacon)
tx_done = true
if radio.recv(pkt):
handle(pkt)
if need_response: radio.send(response)
radio.off()

Tuning and Troubleshooting

Start with conservative parameters: a long L and larger T, then reduce L and T once you confirm reliability.

  • If discovery rate is low: increase L or reduce T. Verify radios are actually waking (use a current meter).
  • If collisions are frequent: increase L (more room for random offsets), reduce tx_len (shorter packets), or add SIP‑style random backoffs.
  • If one node never sees others: check ID uniqueness, sequence wrapping, and make sure its sleep timer isn't stuck (common on low‑power tickless implementations).
  • If battery life is poor: measure duty cycle, then tune T up. Consider increasing T and using an occasional maintenance window where a node stays awake longer for firmware updates.

Why Stateless Works

For many sensor networks you don't need routing or persistent neighbor lists — just occasional discovery and tiny payloads. Stateless design is simple, fits tiny MCUs, and avoids the complexity and memory overhead of full network stacks. It also gracefully handles mobile nodes: if a device moves, you don't need to update tables, you just rediscover it next interval.

When Not to Use TinyMesh

If you need guaranteed low-latency two‑way communication, multi-hop routing, or high throughput, TinyMesh isn't the right choice. It's optimized for sparse, low‑rate exchanges where occasional latency is acceptable and power matters more than immediacy.

Wrap‑Up

TinyMesh is a pragmatic pattern: randomized beacons, short listening windows, and stateless exchanges. It gives you discoverable peers with tiny code and tiny batteries. For many home automation and sensor projects, that’s exactly the sweet spot — less fuss, more uptime, and fewer trips to replace coin cells. If you want, next post we can drop some real code for nRF24L01 or LoRa modules and show power measurements from a CR2032 on a minimal MCU.