MeshCore: Where Clients Donβt Repeat and Thatβs the Point
How a messaging-first mesh firmware for LoRa built infrastructure-grade networking by breaking the rule that every node should forward everything β and why it reminds me of embedded radio work from 2007
Iβm just getting into MeshCore in early 2026, and something about it immediately felt familiar. Not the LoRa modulation β I covered that in the LoRa post. Something deeper. The architecture.
Back in 2007-2009, I worked on an early IoT project with a company called Quality Thermistor. They made precision thermistors β temperature sensors with tight calibration curves, the kind of components youβd use when accuracy actually mattered. We were building wireless sensor networks using Jennic JN5139 radio modules β 2.4 GHz IEEE 802.15.4 chips running 6LoWPAN that predated the βInternet of Thingsβ marketing by several years.
The Jennic systems had a similar problem to LoRa mesh networks today: limited bandwidth, battery constraints, and the need to cover large areas with minimal infrastructure. You couldnβt just have every node blast every message to every other node. Youβd saturate the network instantly. So you designed hierarchies. Coordinator nodes. Router nodes. End devices that only talked when spoken to.
MeshCore does something remarkably similar. In a world where Meshtasticβs βevery node repeats everythingβ approach dominates the conversation, MeshCore takes the opposite stance: clients donβt repeat. That constraint is the design. And that design enables a different kind of network.
MeshCore packet structure β a 1-byte header encodes route type and payload type, followed by optional transport codes, a variable-length path, and up to 184 bytes of payload
What MeshCore Actually Is
MeshCore is messaging-first mesh firmware for LoRa radios, created by Scott at Ripple Radios. The core firmware is MIT licensed. It runs on the same ESP32+SX1262 hardware that Meshtastic uses β Heltec V3, LilyGo T-Beam, RAK WisBlock β but with a fundamentally different philosophy.
Where Meshtastic treats every node as equal (any device can be a client, any device can repeat), MeshCore introduces a deliberate hierarchy:
Companion Radio β The client firmware. Connects to your phone via BLE or USB. Sends and receives messages. Does not repeat. This is your handheld, your mobile device, your personal node.
Repeater β Infrastructure firmware. Receives packets, forwards them according to routing rules, manages mesh topology. No display, no user interface. This runs on a solar-powered node on a hilltop, or a device plugged into power in your attic.
Room Server β A repeater that also stores message history. Like a BBS for your mesh β you can request the last 32 unseen messages when you come into range. Group conversations persist even when participants are offline.
This separation of concerns is the architectural insight that makes MeshCore different. Clients donβt repeat because clients shouldnβt repeat. Theyβre mobile, battery-constrained, and not positioned for optimal coverage. Repeaters repeat because thatβs their job β theyβre placed deliberately, powered reliably, and antenna-optimized for the task.
The Jennic networks we built in 2007 had the same pattern. End devices were sensors: low-power, sleep-most-of-the-time, wake-up-measure-transmit-sleep. Routers were always-on nodes positioned for coverage. Coordinators managed the network. Different firmware for different roles, because role specialization enables better engineering.
The Routing: Flood First, Then Direct
MeshCoreβs routing approach is elegant in its pragmatism. It doesnβt require manual path configuration. It doesnβt rely on complex link-state tables. It learns the network by using it.
First message to a new destination: Flood.
Your companion radio has never talked to Bob before. It doesnβt know where Bob is. So it sends the message as a flood packet β every repeater that hears it forwards it onward, recursively, until it reaches Bob (or the hop limit, or all paths are exhausted).
Bob receives the message. Bob knows how it got there.
Each packet carries a path β up to 64 bytes recording the node hashes of every repeater it traversed. When Bobβs device receives the flood, it knows the exact sequence of hops the message took.
Bob sends a delivery report. It includes the return path.
Bobβs acknowledgment travels back along the discovered path. Now both sides know a working route.
Subsequent messages: Direct.
Future messages between you and Bob use direct routing β they follow the discovered path without flooding. Less airtime. Less network congestion. More efficient.
The path can change. If a repeater goes down, direct messages fail, and the system falls back to flooding to discover new routes. Itβs adaptive without being complicated.
FLOOD: Message broadcast to all repeaters
βββββββββββββββββββββββββββββββββββββββββ
β β
[You] βββββ [Repeater A] βββββ [Repeater B] βββββ [Bob]
β β² β
β β² β
β β [Repeater C] ββββ
β β
βββββββββββββββββββββββββββββββββββββββββ
Path recorded: A β B β destination
DIRECT: Message follows known path
[You] βββββ [Repeater A] βββββ [Repeater B] βββββ [Bob]
No flooding, no extra copies
SNR-Based Receive Delay: The Clever Bit
Hereβs where MeshCore gets technically interesting.
When a flood packet arrives at a repeater, the obvious thing to do is forward it immediately. But MeshCore does something counterintuitive: it delays forwarding based on signal quality.
Strong signal? Wait longer. Weak signal? Forward immediately.
// From Dispatcher.cpp - simplified
int calcRxDelay(float score, uint32_t air_time) const {
return (int) ((pow(10, 0.85f - score) - 1.0) * air_time);
}
The math produces delays like this:
| Signal Score | Quality | Delay (relative to airtime) |
|---|---|---|
| 0.85 | Weak (distant) | 0 ms β process immediately |
| 1.0 | Medium | ~0.4Γ airtime |
| 1.5 | Strong (nearby) | ~3.5Γ airtime |
Why?
Imagine you send a flood message. Repeater A (nearby, strong signal) and Repeater B (distant, weak signal) both receive it. If they both forward immediately, you get two copies flooding outward β redundant.
But if distant repeaters forward first, their retransmissions reach the network edges before the nearby repeaters even start. The nearby repeaters, hearing the packet arrive from the far side, can deduplicate and not bother forwarding.
The result: flood packets naturally propagate outward rather than echoing back. Better coverage with less redundancy. The weak signals β the ones from far-off repeaters at the edge of coverage β get priority because theyβre covering ground the strong signals havenβt reached yet.
This is exactly the kind of optimization that comes from understanding radio at a deep level. Itβs not something youβd think of from a pure networking perspective. Itβs RF thinking applied to mesh topology.
The Packet Format
MeshCore packets are compact by necessity. With LoRa data rates measured in kilobits per second at best, every byte matters.
Wire Format
Offset Size Field
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
0 1 Header byte
1 0-4 Transport codes (if route type requires)
next 1 Path length
next 0-64 Path (node hashes)
next 0-184 Payload
Maximum packet size: 255 bytes Maximum payload: 184 bytes (after header, path, transport codes)
Header Byte Breakdown
The header packs a lot into 8 bits:
Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
βββββ βββββ βββββ βββββ βββββ βββββ βββββ βββββ
ββββββββ ββββββββββββββββ ββββββββββββ
Payload Payload Route
Version Type Type
Route Types (bits 0-1):
| Value | Type | Description |
|---|---|---|
| 0x00 | TRANSPORT_FLOOD | Flood with transport layer addressing |
| 0x01 | FLOOD | Simple flood (no transport codes) |
| 0x02 | DIRECT | Follow recorded path |
| 0x03 | TRANSPORT_DIRECT | Direct with transport layer addressing |
Payload Types (bits 2-5):
| Value | Type | Description |
|---|---|---|
| 0x00 | REQ | Request (initiate conversation) |
| 0x01 | RESPONSE | Response to request |
| 0x02 | TXT_MSG | Private text message |
| 0x03 | ACK | Acknowledgment/delivery report |
| 0x04 | ADVERT | Node advertisement (Iβm here, this is my key) |
| 0x05 | GRP_TXT | Group text message |
| 0x06 | GRP_DATA | Group data (non-text) |
| 0x07 | ANON_REQ | Anonymous request |
| 0x08 | PATH | Path discovery response |
| 0x09 | TRACE | Network diagnostic trace |
| 0x0A | MULTIPART | Large message fragment |
| 0x0B | CONTROL | Control/management message |
| 0x0C | RAW_CUSTOM | Raw custom payload |
Node Hashes
Routing uses 1-byte node hashes β the first byte of each nodeβs Ed25519 public key. With 256 possible values, collisions happen, but the path structure provides enough context to disambiguate in practice. Itβs a pragmatic tradeoff: 1 byte per hop instead of 32 means paths can fit 64 hops instead of 8.
Cryptography: Ed25519 Everywhere
MeshCore uses Ed25519 for identity and signing. Every node has a keypair. Your public key is your identity.
Node advertisements include:
- 32-byte Ed25519 public key
- 4-byte timestamp
- 64-byte signature
- Flags (is_chat, is_repeater, is_room_server, has_location, has_name)
- Optional name and GPS coordinates
The signature covers the entire advertisement. You canβt impersonate a node without its private key.
Private messages are encrypted to the recipientβs public key with a 2-byte MAC (cipher authentication tag) included in the packet.
Group messages use a shared group key, with the channel identified by a 1-byte hash of the key.
The cryptographic model is similar to Signalβs β identity-based, forward-secret handshakes available, no central certificate authority. Your key is your identity. Verification happens out-of-band (QR codes, shared secrets, word lists).
Airtime Budget: The Discipline of Shared Spectrum
LoRa operates in license-free ISM bands. There are legal duty cycle limits in some regions (Europeβs 1% on certain sub-bands) and practical limits everywhere β if everyone transmits constantly, no one gets through.
MeshCore enforces an airtime budget in the Dispatcher:
// After each transmission, wait 2Γ the airtime consumed
next_tx_time = futureMillis(t * getAirtimeBudgetFactor());
The default budget factor is 2.0. If you transmit for 100ms, you stay silent for 200ms. This yields roughly 33% maximum duty cycle β plenty for messaging, conservative enough to share the spectrum.
The Dispatcher also implements Listen Before Talk (LBT):
- Before transmitting, check if the radio detects activity (preamble detection, RSSI above noise floor)
- If channel is busy, back off 200ms and try again
- If channel stays busy for 4+ seconds, transmit anyway (prevent deadlock from stuck radios)
This is good citizenship on shared spectrum. Itβs also required in some jurisdictions for ISM band operation.
The Seven-Layer Stack
MeshCoreβs architecture is a model of clean separation:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MyMesh (application logic) β examples/companion_radio/
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β BaseChatMesh β Contacts, messages, encryption
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Mesh β Routing, flood/direct, dedup
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Dispatcher β TX/RX queues, LBT, airtime
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β RadioLibWrapper β State machine (IDLE/RX/TX)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β CustomSX1262 / SX1268 / LR1110 β Chip-specific init, TCXO
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β RadioLib (external library) β Raw SPI register I/O
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The Mesh layer has zero knowledge of SX1262 registers. The RadioLib layer has zero knowledge of routing tables. Each layer only talks to the one directly below it. This is why MeshCore runs on 50+ board variants β porting to new hardware means writing a thin target file, not touching the routing logic.
The Dispatcher is particularly elegant. It implements the abstract Radio interface:
class Radio {
virtual int recvRaw(uint8_t* bytes, int sz) = 0; // non-blocking poll
virtual bool startSendRaw(const uint8_t* bytes, int len) = 0;
virtual bool isSendComplete() = 0;
virtual void onSendFinished() = 0;
virtual uint32_t getEstAirtimeFor(int len_bytes) = 0;
virtual float packetScore(float snr, int packet_len) = 0;
virtual bool isReceiving() { return false; }
virtual int getNoiseFloor() const { return 0; }
};
Everything is non-blocking. recvRaw() returns 0 if nothing is available. startSendRaw() kicks off transmission and returns immediately. The main loop polls state each iteration. No busy-waiting, no blocking calls, no threads. Just a clean state machine that handles radio I/O in roughly 330 lines of code.
Room Servers: The BBS Returns
Room servers are repeaters with message storage. They hold the last 32 unseen messages for group channels, serving them on request when clients come into range.
This is where the BBS parallel gets literal. A room server is:
- Persistent β Always on, always listening
- Store-and-forward β Holds messages for later retrieval
- Community-forming β Creates a βplaceβ in the mesh where conversations accumulate
When your companion radio connects to a room server, it can request history. Messages you missed while out of range come down in order. Itβs asynchronous communication that doesnβt require both parties to be online simultaneously β exactly what BBSes provided in the dial-up era.
Default credentials for room servers:
- Admin password:
password - Guest password:
hello
(Yes, you should change these. Yes, most people donβt. The 80s were the same way.)
Configuration: Compile-Time by Design
Unlike Meshtasticβs runtime configuration menus, MeshCore sets radio parameters at compile time via build flags:
# platformio.ini
-D LORA_FREQ=910.525 # Carrier frequency (MHz)
-D LORA_BW=62.5 # Bandwidth (kHz)
-D LORA_SF=7 # Spreading factor
-D LORA_CR=5 # Coding rate (4/N)
-D LORA_TX_POWER=22 # TX power (dBm)
This means all devices in a mesh must be compiled with matching parameters. Different SF? Different BW? They canβt talk. Itβs intentionally inflexible.
The upside: no accidental misconfiguration. No UI for users to break their mesh. Flash the firmware, join the network. The parameters are what they are.
Configuration Profiles
| Profile | SF | BW (kHz) | Data Rate | Link Budget | Range |
|---|---|---|---|---|---|
| Default | 7 | 62.5 | ~11 kbps | ~154 dB | 2-5 km urban, 10+ km LOS |
| Long Range | 11 | 125 | ~1.5 kbps | ~168 dB | 10+ km urban, 30+ km LOS |
| Medium Fast | 9 | 250 | ~5 kbps | ~155 dB | 3-8 km typical |
| Short Range Fast | 7 | 500 | ~21 kbps | ~145 dB | 1-3 km |
The 64-Hop Limit
MeshCoreβs internal routing supports paths up to 64 hops. The path field in each packet is variable-length, using 1-byte node hashes, allowing 64 bytes of path data.
In practice, youβll rarely see paths longer than 10-15 hops. Radio propagation and airtime accumulation make very long paths impractical. But the capability exists for extreme deployments β think mountain-to-mountain relay chains across wilderness areas.
For comparison: Meshtasticβs default hop limit is 3 (configurable up to 7). MeshCoreβs architecture handles longer paths naturally because it doesnβt flood indefinitely β once direct routing is established, hop count doesnβt affect network load.
MeshCore vs Meshtastic: The Design Philosophy Difference
Both run on the same hardware. Both build LoRa mesh networks. The differences are philosophical:
| Aspect | Meshtastic | MeshCore |
|---|---|---|
| Repeating | Every node repeats | Only repeaters repeat |
| Configuration | Runtime menus | Compile-time flags |
| Routing | Managed flood always | Flood β direct discovery |
| Infrastructure | Ad-hoc, all nodes equal | Hierarchical roles |
| Message storage | None (real-time only) | Room servers hold history |
| Target use case | Portable mesh, events | Persistent infrastructure |
| Learning curve | Flash and go | Plan your network |
Neither is βbetter.β Theyβre different tools for different problems.
Meshtastic is the walkie-talkie network. Grab a T-Beam, flash it, join the mesh. If youβre at a festival, a hiking group, or an emergency response, you want the mesh to form spontaneously and work immediately. The fact that your phone might relay messages when youβre not looking is a feature β it increases mesh connectivity without requiring infrastructure planning.
MeshCore is the network you build. You plan repeater placement. You consider antenna lines. You deploy room servers at key locations. Clients connect to infrastructure rather than being infrastructure. Itβs more work upfront, but the result is a network that works the same whether you have 3 users or 300.
The Jennic systems I worked with in 2007 were firmly in the βplanned infrastructureβ camp. You couldnβt just scatter end devices and hope for the best. You designed the topology. You calculated link budgets. You placed routers deliberately. MeshCore feels like that β engineering discipline applied to mesh networking.
Open Source Status
The MeshCore firmware is MIT licensed and available on GitHub. You can read every line of the Dispatcher, Mesh, and RadioLib wrapper code.
The T-Deck firmware (Lilygo T-Deck specific UI) is maintained separately and not currently open source.
The mobile apps (Android/iOS) are created by Liam Cottle and are also not open source.
This creates a split ecosystem: the core radio and mesh logic is fully auditable and modifiable, but the end-user applications are closed. If youβre building infrastructure and programming companion radios directly, you have full source access. If youβre using the phone apps, youβre trusting Liamβs implementation.
The public channel key for MeshCoreβs default group channel:
8b3387e9c5cdea6ac9e5edbaa115cd72
This isnβt secret β itβs the well-known default that lets new nodes join the public conversation. Real private communication uses per-contact or per-group keys.
Technical Reference
Packet Structure Detail
| Field | Size | Description |
|---|---|---|
| Header | 1 byte | Route type (2 bits) + Payload type (4 bits) + Version (2 bits) |
| Transport Codes | 0-4 bytes | Source/destination hashes for transport routing |
| Path Length | 1 byte | Number of node hashes in path |
| Path | 0-64 bytes | Array of 1-byte node hashes |
| Payload | 0-184 bytes | Payload-type-specific data |
Advertisement Payload
| Field | Size | Description |
|---|---|---|
| Public Key | 32 bytes | Ed25519 public key |
| Timestamp | 4 bytes | Unix timestamp (for replay protection) |
| Signature | 64 bytes | Ed25519 signature over advert data |
| App Data | Variable | Flags + optional name + optional GPS |
App Data Flags
| Bit | Meaning |
|---|---|
| 0x01 | is_chat β Node supports messaging |
| 0x02 | is_repeater β Node forwards packets |
| 0x03 | is_room_server β Node stores message history |
| 0x10 | has_location β GPS coordinates included |
| 0x80 | has_name β Node name string included |
Default LoRa Parameters (US/Canada)
| Parameter | Value |
|---|---|
| Frequency | 910.525 MHz |
| Bandwidth | 62.5 kHz |
| Spreading Factor | 7 |
| Coding Rate | 4/5 |
| TX Power | 22 dBm |
| Preamble | 8 symbols |
| Sync Word | 0x12 |
The Closing Thought
MeshCoreβs design β clients donβt repeat, infrastructure is intentional, routing adapts from flood to direct β isnβt novel in the broader history of wireless networking. The Jennic systems from 2007 had similar patterns. ZigBee standardized coordinator/router/end-device roles. Enterprise WiFi has always separated access points from clients.
Whatβs remarkable is seeing these mature patterns applied to LoRa mesh in a way thatβs accessible to hobbyists. A $25 Heltec V3 running MeshCore firmware becomes a capable mesh repeater. A handful of them, positioned thoughtfully, creates network infrastructure that covers a small town.
The constraint β clients donβt repeat β forces you to think about your network. Where will the repeaters go? What terrain do they need to cover? How will clients connect to them? This is engineering rather than hope-based deployment.
And room servers bring back the BBS model directly. A persistent node that holds messages. A place in the mesh where conversations accumulate. Log in, check what you missed, participate asynchronously. The protocols and radio modulation are 21st century; the social model is 1985.
Iβve been setting up my first MeshCore nodes this month. Flashing repeater firmware to a Heltec on the roof. Configuring a companion radio for my phone. Thinking about where to place a room server for the neighborhood.
It reminds me of building those Jennic sensor networks in 2007 β the same deliberate infrastructure thinking, the same role separation, the same satisfaction of designing a topology that works. Different chips, different frequencies, same discipline.
Clients donβt repeat. Thatβs not a limitation. Itβs a design decision.
And itβs the right one for the networks MeshCore is meant to build.
This post is part of a series on the LoRa ecosystem. See LoRa for the physical layer foundation. Coming soon: Meshtastic (accessible mesh networking), RNode (open hardware transceiver), and Reticulum (cryptographic networking stack). Browse our complete protocol collection.