Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
07b20f0
ota: .mota container format, merkle tree, EndF self-identity, signatu…
vk496 Jun 29, 2026
6b4f235
ota: vendor detools 0.53.0 in-place/CRLE decoder (NONE+CRLE config)
vk496 Jun 29, 2026
5da7830
ota: LoRa transfer protocol + session manager (discovery, block fetch…
vk496 Jun 29, 2026
9b61169
ota: platform apply + flash staging (ESP32 A/B, nRF52 in-place via bo…
vk496 Jun 29, 2026
9beaf68
ota: relay an external folder of .mota over USB serial (MotaSource)
vk496 Jun 29, 2026
543c901
ota: CLI commands + node/CommonCLI integration + example hooks
vk496 Jun 29, 2026
4680d10
ota: build/board enablement (platformio, build.sh, variants) + Python…
vk496 Jun 29, 2026
2092500
ota: motatool β€” portable C++ .mota packager + relay (build/verify/ins…
vk496 Jun 29, 2026
e3dd6b4
ota: native unit tests (format/merkle/detools/apply vectors)
vk496 Jun 29, 2026
54c8b58
ota: protocol spec + user guide
vk496 Jun 29, 2026
280b9b1
ota: serve & manage OTA over WiFi (motatool --tcp + ESP32 companion s…
vk496 Jun 29, 2026
4cb118b
ota: faster post-boot beacon + periodic re-announce + clearer `ota ls`
vk496 Jun 29, 2026
0a1cf2b
ota: enable OTA on the nRF52 variants the OTAFIX bootloader supports
vk496 Jun 29, 2026
9ff1c88
ota: configurable advertise interval (default 24h) + advertise on ser…
vk496 Jun 30, 2026
259802b
ota: configurable hop limit (`ota config hops`) + accept-gate + forwa…
vk496 Jun 30, 2026
bd2ecb8
ota: seeder storage protocol (pull-to-folder) + motatool serve storag…
vk496 Jul 2, 2026
10fa434
ota: pull a fetched .mota to a host folder (`ota pull <#> folder`) β€” …
vk496 Jul 2, 2026
28e3df0
ota: pause a folder pull on link loss, resume + rescan on reconnect
vk496 Jul 2, 2026
49820b5
docs: pull a firmware .mota off the mesh into a motatool folder
vk496 Jul 2, 2026
4a60148
ota: nRF52 mOTA flash-layout safety net + tests
vk496 Jul 2, 2026
8953214
ota: fragment-level want_mask requests (anti-burst) + warm-start leaf…
vk496 Jul 2, 2026
b159737
ota: admin OTA statistics β€” `ota stats` reply
vk496 Jul 3, 2026
0319a81
ota: stamp the real FIRMWARE_VERSION into the EndF trailer (was v0.0.0)
vk496 Jul 3, 2026
cbdc629
docs: clarify the warm-start capture workflow (--seed flag + validate…
vk496 Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ compile_commands.json
.venv/
venv/
platformio.local.ini

__pycache__/
7 changes: 7 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,15 @@ build_firmware() {
# e.g: RAK_4631_Repeater-v1.0.0-SHA
FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}"

# OTA target id = sha2-256:4(env_name) as a little-endian uint32 (matches tools/mota target_id_for_env
# and the device's MainBoard::getOtaTargetId()). Harmless when OTA is disabled.
MOTA_TARGET_ID=$(python3 -c "import hashlib,sys;print('0x%08x'%int.from_bytes(hashlib.sha256(sys.argv[1].encode()).digest()[:4],'little'))" "$1" 2>/dev/null || echo "")

# add firmware version info to end of existing platformio build flags in environment vars
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'"
if [ -n "$MOTA_TARGET_ID" ]; then
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMOTA_TARGET_ID=${MOTA_TARGET_ID}"
fi

# disable debug flags if requested
disable_debug_flags
Expand Down
634 changes: 634 additions & 0 deletions docs/ota_protocol.md

Large diffs are not rendered by default.

277 changes: 277 additions & 0 deletions docs/ota_user_guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
# Updating your node over the air (OTA) β€” user guide

This guide is for **node operators**: how to update your MeshCore device's firmware over the radio, in
plain language. No cables, no programmer β€” your node can download a new firmware from a neighbour and
install it. (For the technical wire format, see [the OTA protocol spec](ota_protocol.md).)

> **Is my node supported?** OTA works on **ESP32** boards (e.g. Heltec V3) and on the **RAK4631** (nRF52,
> which needs the special MeshCore bootloader). Other boards build fine but can't self-update yet.

---

## The important part first: it's safe

- **Nothing installs by itself.** Your node can *discover* and *download* an update in the background, but
it only **installs** when you say so (unless you deliberately turn on auto-install β€” see below).
- **Bad downloads can't sneak in.** Every piece of the firmware is checked against a cryptographic
fingerprint as it arrives, and the whole image is verified again before install. A corrupt or tampered
download is rejected, not installed.
- **You choose who to trust.** Updates can be *signed* by their author. You can tell your node to only
auto-install firmware signed by keys you've added.
- **It won't disrupt your mesh.** OTA traffic is always the **lowest priority** β€” your node only spends
spare airtime on it. Messages and routing always come first; a busy node simply updates later. Think of
it as *"eventually upgradable."*
- **It can recover.** If an install ever fails, the node falls back to a safe recovery mode (you can
re-flash a known-good firmware over USB) β€” it won't be left bricked.

---

## How to talk to your node

Connect to your node's **console** β€” usually a USB serial terminal at **115200 baud** (or whatever tool
you already use to manage the node). You type `ota ...` commands and the node replies in plain words.

The commands have short, friendly names (and most accept aliases, so you don't have to remember exact
spelling): type **`ota help`** any time to see the list, or just **`ota`** for a status summary.

---

## Common tasks

### 1. See what I'm running and whether anything is going on

```
ota status
```

Shows your current firmware version, your node's update "target" (its hardware/role id), and whether a
download is in progress.

For a denser admin view β€” your firmware's content id (`mid`) **and** its body hash, the fingerprint of the
set you're serving, live download progress, and the current policy β€” use:

```
ota stats
```

On a remote node this is **admin-only** (the remote command console requires the admin password) β€” send it
from the app's repeater command screen, or the WiFi/serial OTA console.

### 2. Find updates available near me

```
ota ls
```

Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words β€”
each with a **number**, its version, whether it's a full image or a small delta, how many nodes have it,
and how recently it was seen. For example:

```
Updates nearby (2 src) β€” `ota get <#>` to download:
1) v1.2.3 delta [yours] 3n 5s
2) v1.2.0 full [other hw] 1n 12s [downloading]
```

Each row shows the version, full-vs-delta, **whether it fits your node**, how many nodes have it, and how
long ago it was seen. The fit marker:

- **[yours]** β€” built for your exact hardware **and** role; safe to install.
- **[other hw]** β€” a different board or role (e.g. a companion image, or another board). Don't install it.
- **[?]** β€” can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build).

Run it again after a few seconds β€” discovery happens in the background, so the list fills in. Nothing is
downloaded yet; this is just looking around. (`ota neighbors` / `ota updates` also work.)

### 3. Download an update

Pick one from the list by its **number**, and say **where** to put it:

```
ota pull 1 flash # stage it in this node's flash, to install here
ota pull 1 folder # capture it onto a connected motatool folder as <id>.mota (don't install here)
ota pull 1 folder validate # same capture, warm-started from a motatool --seed build (much faster; below)
```

The destination is required β€” `ota pull 1` on its own just shows the choices. **`flash`** is always
available (stage here, then `ota install`). **`folder`** appears only while a `motatool serve` link is
attached (it shows the link, e.g. `folder: tcp 192.168.4.5`); it streams the firmware straight onto the
host folder β€” nothing is staged on this node. That's how you grab an **exact copy of another device's
firmware** off the mesh (to a `.mota` file) so you can later build a *delta* against firmware you don't
otherwise have. (`ota get` is an alias.)

**`validate` (warm-start, advanced).** Capturing a full image over the radio is slow. If you have a
*similar* build on the computer (e.g. a fresh recompile of the same firmware), run motatool with
`--seed <that.mota>` and add **`validate`**: the node fetches just the target's block fingerprints, keeps
every block your seed already matches, and pulls over the radio only the handful that actually differ β€”
turning a ~30-minute capture into seconds. The result is still a byte-exact, verified copy of the target.

*Where the seed comes from:* it is the **`--seed <file>` you pass to `motatool serve`** β€” **not** a file you
drop into the capture (`--dir`) folder, which is only the destination and starts empty. There is exactly one
configured seed. When you run `... folder validate`, the node asks motatool to begin the capture and motatool
stamps that seed's payload into the fresh `.part` in the same step β€” so it is always the file you named, with
no guessing. **`validate` is the switch:** a plain `folder` pull ignores any seed and fetches from scratch;
re-running a `validate` pull re-begins fresh (it never resumes a stale partial). Nothing about the seed is
trusted β€” every kept block is checked against the target's own fingerprints, so a mismatched or missing seed
just means those blocks are fetched over the radio (correct result, only slower).

The node fetches in the background, **at low priority**, a piece at a time β€” possibly from several
neighbours at once. Check progress with `ota status`. You can keep using your node normally meanwhile.

If a `folder` pull loses its link mid-transfer, `ota status` shows **paused** β€” the host keeps the
partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never
falls back to flash. To **stop** a download you no longer want:

```
ota cancel
```

### 4. Install a downloaded update

Once `ota status` shows the download is **ready to install**:

```
ota install
```

The node verifies the firmware one last time, and if everything checks out it installs it and **reboots
into the new version**. If the check fails, it tells you why and does **not** install. (If you haven't
added the signer's key, an unsigned/untrusted image will only install with this explicit command β€” never
automatically.)

After it reboots, run `ota status` to confirm the new version.

### 5. If something goes wrong

- A download that stalls or gets interrupted just **resumes** later, or you can `ota cancel` and try again.
- If an **install** fails, the node won't boot a broken image β€” it lands in **recovery mode**:
- **RAK4631 / nRF52:** it appears as a USB drive; drag a known-good firmware `.uf2` onto it to recover.
- **ESP32:** it keeps the previous firmware in the other slot and rolls back.
- When in doubt, you can always re-flash over USB the normal way.

---

## Optional: let it update automatically

By default your node only *discovers* updates β€” it won't download or install on its own. If you want more
automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved.

```
ota config autofetch any # auto-DOWNLOAD any compatible update for this node (still won't install)
ota config autofetch signed # auto-download only signed updates
ota config autofetch off # back to manual (default)

ota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust
ota config autoinstall off # never auto-install (default)

ota config advert 1440 # re-advertise this node every N minutes (default 1440 = 24h)
ota config advert 0 # disable periodic re-advertise (still advertises briefly at boot)

ota config hops 3 # how far OTA travels: accept from / relay up to N repeater hops (default 3)
ota config hops 0 # only exchange OTA with directly-connected nodes (never relay)

ota config # show the current settings
```

Recommended for most people: leave both **off** and update by hand. Use `autoinstall trusted` only once
you've added the signer's key (next section) and you trust them to push updates unattended.

---

## Optional: only trust updates from specific people

If you'll use auto-install, tell your node which signing keys to trust. The firmware author shares their
**public** key (a hex string); you add it:

```
ota key add <public-key-hex> # trust this signer
ota key list # show trusted signers
ota key rm <public-key-hex> # stop trusting one
```

Only updates signed by a trusted key are eligible for auto-install. Manual `ota install` still lets you
install anything yourself, on your own responsibility.

---

## Sharing updates with others (advanced)

### Relay a folder of firmware from a computer

If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can **hand out** a whole
folder of firmware files to the mesh β€” without storing them itself. Useful for seeding a new release to a
remote area.

1. Put the firmware files (`.mota` files β€” see below) in a folder on the computer.
2. Build the helper tool once (`tools/motatool/`), then point it at your node and the folder β€” over the
node's **USB serial**, or over **WiFi** if it's an ESP32 companion on your network:
```
cmake -S tools/motatool -B tools/motatool/build && cmake --build tools/motatool/build
# over USB serial:
./tools/motatool/build/motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v
# …or over WiFi (ESP32 companion): the seeder is on a DEDICATED port (5001), separate from the
# phone-app port (5000), so a phone can stay connected while you serve:
./tools/motatool/build/motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v
```
It answers the node's requests; your node then advertises those updates to neighbours, who can
`ota get` them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect.
Details: [tools/motatool/README.md](../tools/motatool/README.md).)

To stop, just stop the daemon β€” over WiFi the node auto-detaches when the connection closes; over USB you
can also run `ota folder off` on the node. `ota folder` on its own lists what your node is offering.

### Everyone helps share

You don't have to be a gateway to help. Once **any** node finishes downloading an update, it automatically
offers it to *its* neighbours too. So a new firmware spreads outward node-to-node, instead of everyone
hammering the one node that had it first β€” and no node is ever overloaded, because all of this stays
lowest-priority.

---

## Where firmware files come from

OTA distributes **`.mota`** files β€” a packaged, verifiable firmware image (full image or a small "delta"
that only contains what changed). You get them by:

- **Downloading a build.** This fork publishes a rolling **`dev-latest`** release on GitHub with the
current firmware for many boards, each accompanied by a `.full.mota` and a tiny `.delta.mota`. Grab the
one for your board to test.
- **Building your own** with the `mota` packaging tool β€” see [tools/mota/README.md](../tools/mota/README.md)
(this is for people distributing updates, not everyday operators).

---

## Quick reference

| I want to… | Command |
|---|---|
| List all commands | `ota help` |
| See my firmware + any download | `ota status` (or just `ota`) |
| Admin: ids/hashes + serving + policy | `ota stats` (admin-only remotely) |
| Find updates nearby | `ota ls` |
| Download update #1 | `ota get 1` |
| Cancel a download | `ota cancel` |
| Install a finished download | `ota install` |
| Turn on auto-download | `ota config autofetch any` |
| Turn on auto-install (trusted only) | `ota config autoinstall trusted` |
| Trust a signer | `ota key add <hex>` |
| Relay a folder (gateway) | `ota folder on` + the seeder daemon |
| List what I'm offering | `ota folder` |

(Older names still work too: `neighbors`/`updates` = `ls`, `pull` = `get`, `applydelta`/`apply` = `install`, `drop`/`stop` = `cancel`.)

---

## A few terms

- **Firmware** β€” the software running your node. Updating it can add features or fix bugs.
- **`.mota`** β€” a packaged firmware update file, with built-in integrity checks.
- **Target** β€” your node's hardware + role identity. Your node only auto-fetches updates built for the
same target, so it won't grab firmware meant for a different board.
- **Delta** β€” a small update containing only the changes from your current firmware (faster to send than a
full image). Your node rebuilds the complete firmware from it and verifies the result before installing.
- **Signed** β€” the update carries the author's cryptographic signature, so you can verify who made it.

For the full technical details (the file format and the radio protocol), see
[the OTA protocol spec](ota_protocol.md).
2 changes: 1 addition & 1 deletion examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#endif

#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.16.0"
#define FIRMWARE_VERSION "v1.17.0"
#endif

#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
Expand Down
Loading