fluxcore docs
Firmware built from upstream MeshCore's dev branch plus a handful of fixes, for a short list of boards.
Latest build#
2026-11-04 (b115223) · tag beala-b115223
Changes since beala-4d912c9:
- Clone takeover: count unanswered messages from the takeover, two-radio bench results
How it differs from the official flasher#
| flasher.meshcore.io | This site | |
|---|---|---|
| Firmware | Official stable releases | Upstream dev (unreleased) plus the changes listed below |
| Updates | When MeshCore cuts a release | Automatically, within about 45 minutes of a change upstream or here |
| Boards | Every supported board | The 7 boards listed below |
| Roles | Companion, repeater, room server | Companion (USB and BLE); repeater for the RAK 1W only |
| Version shown | e.g. v1.18.0 | e.g. v1.18.0-dev-beala (the last upstream release it's newer than) |
| Older versions | All past releases | The last 5 builds |
The flasher itself is the official one, unchanged. Erase images and bootloaders are copied from flasher.meshcore.io, so those are the same files.
Because these builds track dev, they get upstream's newest work before it's released, including its bugs. If something breaks, flash an older build from the version list, or the official firmware from flasher.meshcore.io.
Boards#
| Board | Radio | Firmware |
|---|---|---|
| Heltec Mesh Node T1 | SX1262 | Companion USB / BLE |
| Seeed SenseCAP MeshTracker X1 | LR2021 | Companion USB / BLE |
| RAK WisMesh Tag | SX1262 | Companion USB / BLE |
| Seeed SenseCAP T1000-E | LR1110 | Companion USB / BLE |
| Heltec V3 | SX1262 | Companion USB / BLE |
| Heltec V4 | SX1262 | Companion USB / BLE |
| RAK WisMesh 1W Booster (3401) | SX1262 | Repeater |
What's different from mainline firmware#
All boards#
Packet parser security fixes. Neil Alexander's fixes from meshcore-dev/MeshCore#3521, not yet merged upstream. Malformed or truncated packets from the air could make the firmware read past the end of buffers or use stale memory; the parser now rejects them. Covers empty and short frames, partial encrypted payloads, oversized advert data, bad TRACE and CONTROL packets, and ACK overflows. One addition on top: packets with a newer, unsupported protocol version are dropped instead of being handled as version 1. beala/pr-3521
Safer randomness for new keys. A radio now only keeps a new private key if it gathered enough randomness from sources it can trust, and the Heltec V3 and V4 use their chip's random generator properly (see below). beala/entropy
INA219 shunt setting. The INA219 current sensor's readings assumed a 0.1 ohm shunt resistor.
Set yours with sensor set ina219_shunt <ohms> (for example 0.01 for a board marked R010).
It's saved and survives a reboot. sensor get ina219_shunt shows it; it only appears in
sensor list if an INA219 was found at boot. The most it can measure is 0.32 V divided by the shunt:
3.2 A with 0.1 ohm, 32 A with 0.01 ohm.
beala/ina219-shunt
Resend messages that don't get out. After sending a message, the radio listens for the next repeater passing it on. If it hears none, it sends the exact same packet once more. Radios that already have it ignore the copy, so nobody sees it twice. Works for channel messages and for direct messages along a path. On by default on companions, off by default on repeaters (see below). beala/flood-retry
Several companions with one key. Pick one radio as the main radio; the others with the same key still show the messages they hear but never answer them, so senders get one reply and one path, and "delivered" means the main radio has it. A Radio console contact in the app runs radio commands and tells you when another radio with your key is heard (see below). beala/main
Companion boards#
Buzzer, vibration and LED switches. Commands to turn the buzzer, the vibration motor and the LEDs on and off, each on its own (see below). Mainline only has a buzzer command, and the X1's button cycles buzzer and vibration together. beala/alert-toggles
Stronger encryption (experimental, opt-in). A new encryption format that fixes real weaknesses in the current one, and works alongside radios that don't have it (see below). beala/crypto-v2
Find a lost radio. Make a radio beep for a couple of minutes, from itself or from another radio you've allowed, and measure the distance between two LR2021 radios such as X1s (see below). beala/find-me
T1, X1, WisMesh Tag and T1000-E#
Stay off when plugged in. On mainline, plugging a switched-off node into a charger turns it on.
These builds stay off and just charge; press the button to turn on. Change it with
set charger.wake on|off. A node that switched itself off for low battery still comes back when charged.
beala/charger-stay-off, beala/custom-defaults
Heltec T1 and T1000-E#
Buzzer starts off on both, and the status LED starts off on the T1000-E.
Turn them back on with set buzzer on / set led on.
beala/custom
MeshTracker X1 (LR2021 radio)#
These change the LR2021 driver, so they only affect the X1 on this site.
- Full transmit power. RadioLib's power table for the LR2021 stops rising at about 14 dBm, whatever you set. This uses Semtech's reference table, so settings up to 22 dBm give the power they say. beala/fix-lr2021-pa-table
- Semtech firmware patch. Semtech says to load a patch into the radio after every reset; RadioLib never does. It's now loaded at boot. beala/lr2021-pram-patch
- AGC reset fix. With
agc.reset.intervalturned on, the radio was left asleep after each reset: packets were missed and queued messages didn't send. It now wakes and recalibrates properly. beala/fix-lr2021-agc-reset - Correct boosted-gain status. The boot log always said RX boosted gain was off, even when it was on. beala/fix-lr2021-rx-boost-status
- SIMO power supply. The radio runs from its built-in DC-DC converter, which uses less current. beala/lr2021-simo-flag, beala/x1-simo
- Radio diagnostics. The two commands below, plus transmit fault logging in debug builds. beala/lr2021-tx-fault-log, beala/lr2021-rx-gain-step, beala/lr2021-diagnostics
Buzzer, vibration, LED and charging commands#
Send these from the companion app's command line. Each takes effect immediately and is saved. A board without
that part replies Unknown command.
| Command | What it controls | Boards |
|---|---|---|
get buzzerset buzzer on|off | The buzzer | T1, T1000-E, X1, WisMesh Tag |
get vibeset vibe on|off | The vibration motor, separately from the buzzer. Like the buzzer, it only goes off for new messages while the app isn't connected | X1 |
get charger.wakeset charger.wake on|off | Whether plugging in a charger turns the node on (off on this site: it stays off and charges) | T1, X1, WisMesh Tag, T1000-E |
get ledset led on|off | The status LED (heartbeat, unread messages, charging) and the flash of the TX LED when it transmits | All companion boards on this site |
Replies: > on or > off for get, OK for set.
The X1's triple-press still cycles buzzer and vibration modes, and the commands show the result.
Find a lost radio#
Make a radio beep so you can find it by ear, from the radio itself or from another one. Between two radios with an LR2021 (such as two X1s) you can also measure how far apart they are.
How to use it#
- Set it up once, before anything gets lost. You need two radios that have each other as contacts.
On the radio you might lose, run
set find.allow <other radio's name>. Do the same the other way round if either could get lost. - Lost, but you can still reach it from the app (for example over Bluetooth): run
find meon it. It beeps for 2 minutes, even with the buzzer turned off, and vibrates on the X1. Press its button or runfind me stopto end it. - Lost for real: on the other radio, run
find beep <name>, wait a few seconds, thenget find. You should seebeeping for 120 s.find stop <name>silences it. - Both radios have an LR2021 (X1, AB Labs XIAO): run
find range <name>, thenget find, for something like42 m (+/- 4 m, 30/32 answers, -97 dBm). Move and repeat: if the number goes down, you're getting closer.
Commands#
From the companion app's command line.
| Command | What it does | Boards |
|---|---|---|
find mefind me <seconds> | Beep for 2 minutes, or 1 to 600 seconds | T1, X1, WisMesh Tag, T1000-E |
find me stop | Stop beeping (the button does too) | T1, X1, WisMesh Tag, T1000-E |
set find.allow <name>set find.deny <name>get find.allow | Let a contact make this radio beep and range to it, take that away, or list who's allowed (up to 8). They can't do anything else | All companion boards |
find beep <name>find stop <name> | Make another radio beep for 2 minutes, or stop it | All companion boards; the other radio needs a buzzer or vibration motor |
find range <name> | Measure the distance to another radio | LR2021 to LR2021 (X1, AB Labs XIAO) |
set find.extended onset find.extended offget find.extended | Use extended ranging for find range (off by default). The distance stays right while the radios move, and it adds a rough relative speed: closing at ~20 m/s, moving apart at ~40 m/s, or not moving fast. Speed comes in steps of about 20 m/s, so it shows vehicles, not walking. Both radios need this build | LR2021 to LR2021 |
get find | The latest answer, and how long ago it came | All companion boards |
If it doesn't work#
nothing yet: the lost radio didn't hear you. It has to be in range (through repeaters is fine for beeping; ranging needs a direct path). Get closer and try again.find: not allowed: step 1 wasn't done on the lost radio.old request ignored (is your clock set?): the radio you're using has the wrong time. Connect it to the app so its clock gets set.ranging didn't start: this radio was busy transmitting: runfind rangeagain. (Builds before 4 Nov saidradio busyhere even when the radio was free; update both radios.)ranging needs 902-928 MHz: ranging uses a 500 kHz wide signal, which only fits the US band (also Canada, Australia, New Zealand).
Technical details#
- Requests are encrypted CLI messages to the target, like a direct message. The target only acts
on them from contacts on its allow list (or with remote CLI access), and only if the request is newer than the
last one it accepted from that contact, so recorded requests can't be replayed. The list is saved in the
findsection of/prefs.json. - Ranging uses the LR2021's round-trip time-of-flight mode: SF9, 500 kHz, up to 32 exchanges in 2.5 s, reporting the median. Command sequence and bench results from beala/lr2021-re. The distance offset isn't calibrated, so expect it to be a few metres off; walls and bodies make it read longer.
- While ranging, the lost radio is busy for 5 s and the finder for up to 2.5 s. Neither hears mesh traffic in that time.
- Each ranging session uses a random address, sent only inside the encrypted reply, so other radios can't easily answer in its place or range the lost radio while it listens. The ranging exchange itself isn't encrypted, so a determined attacker could still fake the distance.
- Extended ranging runs two exchanges per measurement whose errors from relative motion are opposite: their average gives the distance, their difference the speed. In normal mode, motion can throw the distance off by tens of metres.
Full write-up: docs/find_me.md.
Source: beala/find-me.
Flood retry#
When you send a message, your radio listens for the next repeater passing it on. If it hears nothing within about 2.5 seconds, it sends the same packet again, byte for byte, down to the timestamp. That covers channel messages, messages to contacts it doesn't have a path to yet, and direct messages that follow a path through repeaters. For a direct message, a lost hop is fixed in a few seconds instead of waiting for the app to time out and retry.
That's safe because every radio and repeater remembers the messages it has seen and drops repeats of them. A repeater that got the first copy ignores the resend, and so do the radios that showed you the message. A repeater that missed the first copy treats the resend as new and passes it on. So the resend only fixes the case where the first hop was lost, and costs one extra transmission when it wasn't needed.
On a repeater it works the same way for messages it forwards: if it doesn't hear the next hop pass a message on, it sends its copy again. That helps on every hop of a direct route, and where one repeater is the only link between two areas. It's off by default. Adverts are never resent, and a resend is skipped while the repeater has other packets waiting. A repeater at the edge of the mesh, with nobody further out, would resend every channel message once, so leave it off there.
Commands#
From the companion app's command line, or a repeater's command line (USB or remote admin).
| Command | What it does |
|---|---|
get flood.retry | How many resends, and the wait |
set flood.retry 0 to 3set flood.retry off | How many times to resend if nothing is heard. Companions start at 1, repeaters at 0 (off) |
set flood.retry.wait <ms>set flood.retry.wait auto | How long to wait for a repeat, 500 to 30000 ms. auto (the default) is 1 second plus 8 times the message's airtime, about 2.5 s at SF7 / 62.5 kHz |
get flood.retry.stats | Counts since boot, for example watched 18, echoed 15 (9 after a resend), resends 12, no echo 3, skipped 0. "After a resend" means the first send didn't get through and the resend did |
Adverts, traces, and the last hand-off to the recipient are never resent: nobody passes those on, so there's no repeat to listen for. More detail and bench results are in docs/flood_retry.md.
One key, several radios#
You can load the same private key onto more than one companion, say one at home and one you carry, and both show your messages. With more than one switched on, a message that arrives by flood reaches each of them and each replies. The sender gets two replies and two paths, keeps the last, and sends its direct messages to that one radio from then on.
With this firmware, one radio can take over as the main radio. The others become backups: they still show the messages they hear, but send no acknowledgement, path or response. Senders only ever hear from the main radio, however far apart your radios are. If the main radio is off or out of range, nothing answers and the sender sees the message fail, so take over on the radio you have with you. If you send a message from a backup, its console offers to make it the main radio.
The Radio console contact#
Every radio with this firmware adds a contact called Radio console to the app. It isn't a
real node, and messages to it never go on air: the radio runs them as commands and replies in the same chat.
Send help to start. Commands aren't case sensitive. Other radio commands work there too, such as get flood.retry, so
you can use them from an app that has no command line.
The console also tells you things. When your radio hears another radio with your key, it posts: "Another radio with this key is active: it sent to Bob. Make this the main radio, so the others stay quiet? Reply "yes"."
Commands#
Send these to the Radio console contact, or from the companion app's command line.
| Command | What it does |
|---|---|
takeover | Make this the main radio. It floods a short claim only radios with your key can read; each of them becomes a backup and says so in its console |
yes | Accept the takeover the console just offered |
clone | This radio's role, how many messages it left unanswered as a backup, and when another radio with the key was last heard |
clone off | Answer as normal again |
The role is saved and survives a reboot. A radio that was off during the takeover doesn't know about it; the main radio offers to send the takeover again when it hears that radio reply. Senders keep one path for your key, to the main radio; a backup elsewhere only gets messages that come by flood. Messages you send from one radio don't show up on the others. More detail and bench results are in docs/clone_takeover.md.
Encryption v2 (experimental)#
Opt-in, per contact and per channel. It's new and hasn't been reviewed independently yet, so don't rely on it for anything sensitive.
In plain terms#
A private message's encryption has two jobs: scramble it so listeners can't read it, and seal it so the receiver knows it really came from you and wasn't changed or made up.
Why MeshCore's current encryption is weak:
- It leaks patterns. Messages are scrambled in 16-character pieces, and the same piece always scrambles the same way. A listener can't read your words, but can tell when you repeat them, like a code where each common word always gets the same codeword.
- The seal is far too short. It's 2 bytes, so there are only 65,536 possible seals. Someone without the key can keep sending fakes until about 1 in 65,536 is accepted, and because pieces are scrambled separately, they can build the fake by cutting and pasting pieces of your earlier messages, so it reads as real text.
What v2 changes: it uses AES-SIV, a published standard (RFC 5297), not a home-made scheme.
- No patterns. The whole message is scrambled together, so repeated words no longer look alike.
- A seal that can't be guessed. 16 bytes instead of 2: guessing one is less likely than winning a 1-in-300-million lottery four times in a row.
- No cut and paste. The seal covers the whole message plus who it's from and to. Change any part and the message is thrown away.
Working with radios that don't have it: repeaters relay v2 like any other message, and radios without
v2 quietly ignore it. Everything starts on the old format. You switch a contact or channel to v2 once the other side
has it. When a contact sends you v2, your radio starts replying in v2 by itself, but nothing ever switches back to the
old format automatically. v2only refuses the old format from that contact or channel entirely.
Cost: about 7 more bytes per message on average. 160-character texts still fit.
What it doesn't fix: channel members can still read everything and post under any name; public and hashtag channels aren't private; someone who records messages and later steals your radio's key can read them; recorded messages can be replayed; and who's talking, how often and how much is still visible.
Commands#
From the companion app's command line. They only work on the device itself, not from a remote admin.
| Command | What it does |
|---|---|
set crypto v2 Alice | Send v2 to the contact Alice, still accept both formats (name, or the start of the public key in hex) |
set crypto v2only Alice | Send v2 and refuse old-format messages from Alice |
set crypto legacy Alice | Back to the old format (the default) |
get crypto Alice | Show Alice's setting |
set crypto.ch v2 1get crypto.ch 1 | The same for a channel, by number or name. Channels never switch by themselves |
get crypto | Channels not on legacy, and how many contacts are on v2 |
set crypto.recover | Only needed if saved settings were lost (see below) |
get crypto.bench | Times decryption on this radio with a test key, and says whether AES runs on hardware (aes=hw) or software |
Keep repeaters and room servers on legacy unless they run this firmware: they can't read v2, so logins
would fail.
Technical details#
- Cipher: RFC 5297 AES-SIV with AES-128 and a 16-byte synthetic IV/tag. Key = HKDF-SHA256 of the
existing 32-byte shared or channel secret. Associated data: the label
"MeshCore AEAD v2", then the payload type and the fixed addressing fields (destination/source hashes, sender key for anonymous requests, or the channel hash). Fields repeaters change (path, hop count) aren't covered. - Telling formats apart: an old-format payload is always 2 + 16·n bytes. v2 never uses that length
(one
0x00byte is added when it would), so the receiver picks the format from the length alone, and firmware from v1.18.0 on always rejects v2. The packet header is unchanged, so old repeaters keep forwarding. - Downgrades: automatic changes only ever raise a contact to v2; channels never change on their own;
a missing ACK never falls back. Policies are saved to two alternating checksummed files, and prefs record the newest
save. If it can't be loaded at boot, everything is treated as
v2onlyuntilset crypto.recover, instead of silently accepting the old format again. - Payload safety: the receive buffer past the decrypted message is zeroed. PATH payloads whose path doesn't fit are rejected (an over-read that also affects mainline). Tests run under AddressSanitizer with random payloads of every type.
- Timing: on nRF52, AES runs on the CC310 hardware engine, which has no secret-dependent lookup tables and is faster. It's checked against an RFC test vector on first use, with software AES as the fallback. ESP32 already uses its AES hardware. The tag comparison is constant-time (checked in the ARM compiler output). A packet built to trigger the padding retry costs two decryption attempts instead of one, which is CPU time only and leaks nothing. An automatic upgrade is written to flash 5 seconds later, so it doesn't delay the first v2 message's ACK.
- Overhead: 16 bytes (+1 in 1 of 16 lengths) against v1's 2 + 0–15 bytes of padding: about 6.6 bytes more on average. Longest direct message 165 bytes (v1: 167).
Full specification, test vectors and limitations: docs/crypto_v2.md.
Key randomness#
In plain terms#
The first time a radio starts, it makes its own private key: a secret number that everything private hangs off. Every direct message to or from that radio is protected with it. The key is only as good as the randomness used to pick it. If that randomness were predictable, someone could work out the key and read those messages.
What was wrong:
- On the Heltec V3 and V4 (ESP32), the chip's good random generator was switched off by a quirk in how the firmware starts up. The key came only from the LoRa radio's noise reading, which nobody has checked.
- On every board, nothing checked whether the sources were working. If they all failed, the radio would still make a key and keep it forever.
What changed:
- Only proven sources count. The chip's generator counts only when it reports success and is known to be truly random at that moment. Radio noise counts only if it passes standard checks for a stuck or lopsided signal, and then at a fraction of what it probably holds. Timing is mixed in but counts for nothing, because start-up timing can repeat from boot to boot.
- No key is kept without enough. A key is saved only once 256 bits of trusted randomness have been gathered. Otherwise the radio uses a temporary key for this session and tries again at the next boot. It never keeps a key that might be weak.
- ESP32 turns its noise source on. Right at power-on, before anything else starts, the chip briefly switches on the internal noise source its random generator needs.
- A standard generator. Keys come from HMAC-DRBG, the NIST-standard random generator, checked against NIST's official test vectors.
Which radios it affects: it's a real fix on the Heltec V3 and V4. The T1, X1, T1000-E, WisMesh Tag and RAK3401 already used their chip's hardware generator, and now also check that it worked.
Your existing key doesn't change. Updating the firmware keeps it. To get a key made the new way,
do a full erase when flashing. That gives the radio a new identity, so your contacts will need to add it again.
Run get entropy afterwards to check there was enough randomness.
The random 6-digit Bluetooth PIN, shown on boards with a screen, comes from the same generator, and every PIN is equally likely. Channel keys aren't made on the radio: the phone app picks them. Hashtag channels aren't private at all.
Technical details#
- Before: each key byte was the radio's
randomByte()XOR Arduinorandom(), plus the CC310 TRNG on nRF52. On ESP32,randomSeed()at boot switchesrandom()to the newlib PRNG, seeded from 31 bits of the same radio. Failures were ignored and a key was always saved. - Now: inputs are conditioned into a SHA-256 pool, which seeds an HMAC-DRBG (SP 800-90A, SHA-256,
tested against 20 NIST CAVP vectors). Keys are kept only when at least 256 bits have been credited:
- CC310: 256 bits, only if
generate()reports success. - ESP32 RNG: 256 bits, only in
collectEarly(), the first call in every firmware'ssetup(). It runs with the SAR ADC entropy source on (bootloader_random_enable()), before the ADC or RF start, as ESP-IDF requires. At any other time the ESP32 RNG counts for nothing. - Radio noise: 1 bit per sample, for 512-sample batches that pass the SP 800-90B repetition count and adaptive proportion tests. These catch a stuck or biased radio, not a predictable one.
- Timings, seeds and anything else: 0.
- CC310: 256 bits, only if
- Fail closed: the companion, repeater, room server, sensor, KISS modem and terminal chat firmware all save a new identity only if there was enough. The terminal chat example used to make keys from the Arduino PRNG seeded at a keypress.
- Other details: the BLE PIN uses rejection sampling, temporaries are wiped with a
volatilewrite, and the pool is documented as single-threaded. - Cost: about 0.6 s on first boot with trusted hardware. Without it (RP2040, STM32, ESP32-C6), about 5 s per 512-sample batch on an SX126x, and up to 3 batches.
- Limits: no hardware has been measured yet. The ESP32 credit relies on ESP-IDF's statement about the SAR ADC source. RP2040 and STM32 depend on radio noise alone.
Full write-up: docs/entropy.md.
Source: beala/entropy.
New commands (MeshTracker X1)#
Send these from the companion app's command line (or any client that can run companion CLI commands). Other radios reply Unknown command.
get radio.faults#
Transmits that went wrong since boot, newest first (up to 8), with how long ago each was.
> 3 since boot: 12s OCP; 95s ERR=0004; 300s NODONE
| Flag | Meaning |
|---|---|
NODONE | The transmit didn't finish |
OCP | The power amplifier hit its over-current or over-voltage limit |
ERR=xxxx | The chip reported an error (its error register in hex; 0004 is a PLL lock failure) |
CMD | The chip rejected a command |
TIMEOUT, EOL | The chip's timeout and end-of-life signals |
With no faults: > no TX faults since boot
get radio.agc#
The receiver gain step the radio picked for each of the last packets, with their signal strength, SNR and age, newest first.
> G13 -112dBm -6.5dB 4s; G9 -87dBm 8.0dB 31s
Higher gain steps mean the radio amplified a weaker signal. Useful for spotting a receiver that's being overloaded by a nearby transmitter (low gain steps on weak packets). Before anything is received: > no packets received yet
How the builds are made#
The builds come from beala/main, the
fork's own main line. Every 30 minutes, and whenever beala/main changes, a GitHub workflow in
beala/MeshCore merges in the newest upstream dev, builds
all boards in parallel, and publishes a release tagged
beala-<commit>. This site checks for new releases every 15 minutes and republishes.
The release notes list what changed since the previous release, with a link to the full diff against upstream. The branch names next to each feature above are where that change is kept for sending upstream.