mirror of
https://github.com/vk496/MeshCore.git
synced 2026-09-16 06:12:42 +00:00
Rename cli + fixes
This commit is contained in:
+19
-10
@@ -5,6 +5,9 @@ written for developers who want to implement an interoperable peer (server, fetc
|
||||
in another codebase or project. Everything below is implemented and hardware-verified in this repository;
|
||||
where a section names a source file, that file is the authoritative reference for byte-level details.
|
||||
|
||||
> **Just want to update your node?** See the plain-language [OTA user guide](ota_user_guide.md) — this
|
||||
> document is the technical/wire specification.
|
||||
|
||||
**Design goals**
|
||||
|
||||
- Distribute firmware over LoRa as a **self-verifying, resumable, BitTorrent-style block transfer** that
|
||||
@@ -498,17 +501,23 @@ Heltec V3 over one USB cable, every block merkle-checked.
|
||||
User-facing OTA data should travel via `CMD_OTA_*` companion binary frames; the text CLI below is
|
||||
debug/operator oriented and replies are `snprintf`-bounded into a 160-byte buffer.
|
||||
|
||||
Commands take intuitive aliases (matched by the first word; see `is_cmd` in `OtaCli.cpp`) so they're easy
|
||||
to type and read — `status`/`neighbors`/`pull`/`drop`/`applydelta` are the canonical names, the aliases are
|
||||
the recommended user-facing forms. Output is plain-language (a user-facing guide lives at
|
||||
[ota_user_guide.md](ota_user_guide.md)).
|
||||
|
||||
```
|
||||
ota status session + self-fw summary
|
||||
ota neighbors discovered mOTAs (queries sources; rows arrive async via OTA_HAVE)
|
||||
ota announce serve self + send a beacon now
|
||||
ota pull <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch)
|
||||
ota drop drop the current fetch session (free the slot)
|
||||
ota folder on|off attach/detach an external .mota folder (host daemon) ; bare = list
|
||||
ota self print this firmware's EndF (body/image size, base_hash)
|
||||
ota applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader
|
||||
ota config [autofetch|autoinstall|checkpoint] ... show/set persisted policy
|
||||
ota key add|list|rm <hex> trusted signer allowlist
|
||||
ota help | ? list the commands
|
||||
ota status | st (or bare `ota`) plain-language: running fw, the one fetch session (state/%/id), serving, keys
|
||||
ota ls | neighbors | nbrs | updates | n discovered updates (queries sources; rows arrive async via OTA_HAVE)
|
||||
ota get | pull | download <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch)
|
||||
ota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader
|
||||
ota cancel | drop | stop drop the current fetch session (frees the slot; stops re-seeding)
|
||||
ota announce | adv serve self + send a beacon now
|
||||
ota self | id print this firmware's EndF (body/image size, base_hash)
|
||||
ota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list
|
||||
ota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy
|
||||
ota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list
|
||||
ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# 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.
|
||||
|
||||
### 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 3 nodes 5s
|
||||
2) v1.2.0 full 1 node 12s [downloading]
|
||||
```
|
||||
|
||||
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**:
|
||||
|
||||
```
|
||||
ota get 1
|
||||
```
|
||||
|
||||
The node starts fetching it in the background, **at low priority**, a piece at a time — possibly from
|
||||
several neighbours at once. Check progress any time with `ota status` (you'll see it climb, e.g.
|
||||
`download: downloading 120/525 (23%)`). You can keep using your node normally meanwhile.
|
||||
|
||||
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 # 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. Run the helper daemon, pointing it at your node's USB port and the folder:
|
||||
```
|
||||
pip install pyserial
|
||||
./tools/mota/mota_seeder.py --port /dev/ttyACM0 --dir ./my_firmware/ -v
|
||||
```
|
||||
It turns the relay on for you and then answers the node's requests. Your node now advertises those
|
||||
updates to neighbours, who can `ota get` them like any other. (Details: [tools/mota/README.md](../tools/mota/README.md).)
|
||||
|
||||
To turn it off, stop the daemon (or run `ota folder off` on the node). `ota folder` on its own lists what
|
||||
your node is currently 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`) |
|
||||
| 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).
|
||||
@@ -45,7 +45,8 @@ static bool parse_manifest_fields(ByteReader& r, uint32_t signed_off, MotaManife
|
||||
if (!r.ok) return false;
|
||||
if (out.block_size_log2 == 0 || out.block_size_log2 > 24 || out.payload_size == 0) return false;
|
||||
out.block_count = (out.payload_size + out.block_size() - 1) / out.block_size();
|
||||
return out.block_count != 0;
|
||||
// block_idx is uint16 on the wire; capping here also keeps block_count*4 (leaves length) from overflowing.
|
||||
return out.block_count != 0 && out.block_count <= 0xFFFFu;
|
||||
}
|
||||
|
||||
bool mota_parse(const uint8_t* buf, uint32_t len, MotaManifest& out) {
|
||||
|
||||
+102
-51
@@ -28,9 +28,42 @@ static char fstate_char(OtaManager::FetchState s) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char* codec_name(uint8_t c) {
|
||||
return c == CODEC_FULL ? "full" : (c == CODEC_DETOOLS_SEQUENTIAL ? "seq"
|
||||
: (c == CODEC_DETOOLS_INPLACE ? "inpl" : "?"));
|
||||
// For users, the only distinction that matters is full image vs. delta (which delta codec is internal).
|
||||
static const char* codec_kind(uint8_t c) { return c == CODEC_FULL ? "full" : "delta"; }
|
||||
|
||||
// A plain-language word for the fetch state (shown in `ota status`).
|
||||
static const char* state_word(OtaManager::FetchState s) {
|
||||
switch (s) {
|
||||
case OtaManager::IDLE: return "idle";
|
||||
case OtaManager::WANT_MANIFEST: return "starting";
|
||||
case OtaManager::FETCHING: return "downloading";
|
||||
case OtaManager::COMPLETE: return "ready to install";
|
||||
case OtaManager::FAILED: return "failed";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the packed fw_version (MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre) into "v1.2.3" (or ".pre").
|
||||
static void ver_str(char* out, size_t cap, uint32_t v) {
|
||||
unsigned maj = v >> 24, min = (v >> 16) & 0xFF, pat = (v >> 8) & 0xFF, pre = v & 0xFF;
|
||||
if (pre) snprintf(out, cap, "v%u.%u.%u.%u", maj, min, pat, pre);
|
||||
else snprintf(out, cap, "v%u.%u.%u", maj, min, pat);
|
||||
}
|
||||
|
||||
// Match the first word of `a` against any of the '|'-separated names (so commands have intuitive aliases
|
||||
// and short forms); on a match, point `*rest` at the argument text. Keeps the dispatch table readable.
|
||||
static bool is_cmd(const char* a, const char* names, const char** rest) {
|
||||
size_t tlen = 0; while (a[tlen] && a[tlen] != ' ') tlen++;
|
||||
for (const char* s = names; *s; ) {
|
||||
const char* d = s; while (*d && *d != '|') d++;
|
||||
if ((size_t)(d - s) == tlen && tlen && strncmp(a, s, tlen) == 0) {
|
||||
const char* r = a + tlen; while (*r == ' ') r++;
|
||||
if (rest) *rest = r;
|
||||
return true;
|
||||
}
|
||||
s = (*d == '|') ? d + 1 : d;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The everyday OTA surface is BitTorrent-shaped: `ota` shows what you're holding (your running firmware
|
||||
@@ -45,54 +78,68 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (*a != 0 && *a != ' ') return false;
|
||||
while (*a == ' ') a++;
|
||||
OtaContext& c = ota_ctx();
|
||||
const char* rest = a;
|
||||
|
||||
// ---- raw / internal primitives, tucked under `ota dev ...` ----
|
||||
if (strncmp(a, "dev", 3) == 0 && (a[3] == 0 || a[3] == ' ')) {
|
||||
const char* d = a + 3; while (*d == ' ') d++;
|
||||
return handle_dev(d, reply, c);
|
||||
if (is_cmd(a, "dev", &rest)) {
|
||||
return handle_dev(rest, reply, c);
|
||||
}
|
||||
|
||||
// ---- help: list the commands in plain words (aliases in parentheses) ----
|
||||
if (is_cmd(a, "help|?|h", &rest)) {
|
||||
snprintf(reply, 160,
|
||||
"OTA: status | ls=find updates | get <#>=download | install | cancel | announce | self | "
|
||||
"folder | config | key. Try `ota ls`.");
|
||||
|
||||
// ---- inventory dashboard: running fw (self), the one fetch session, serving state ----
|
||||
if (*a == 0 || strncmp(a, "status", 6) == 0) {
|
||||
} else if (*a == 0 || is_cmd(a, "status|st", &rest)) {
|
||||
SelfFwInfo fi; bool s = ota_self_firmware(fi);
|
||||
char selfhx[9]; if (s && fi.valid) mesh::Utils::toHex(selfhx, fi.body_hash, 4); else strcpy(selfhx, "?");
|
||||
OtaManager::FetchState fs = c.manager.fetchState();
|
||||
char midhx[9]; strcpy(midhx, "-");
|
||||
if (fs != OtaManager::IDLE) mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
||||
unsigned age = (fs != OtaManager::IDLE && c.session_started_ms) ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
|
||||
sprintf(reply, "OTA tgt=%08X fw=%s | self:%s%uK | sess:%c %u/%u mid=%s age=%us | serv:%s keys=%u",
|
||||
(unsigned)board.getOtaTargetId(), selfhx, s ? "full " : "?",
|
||||
(unsigned)((s ? fi.image_len : 0) / 1024), fstate_char(fs),
|
||||
(unsigned)c.manager.blocksHave(), (unsigned)c.manager.blocksTotal(),
|
||||
midhx, age, c.serving ? "on" : "off", (unsigned)c.allow.count());
|
||||
char dl[80];
|
||||
if (fs == OtaManager::IDLE) {
|
||||
strcpy(dl, "no download");
|
||||
} else {
|
||||
char midhx[9]; mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
||||
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
|
||||
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
|
||||
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
|
||||
snprintf(dl, sizeof dl, "download: %s %u/%u (%u%%) id=%s %us", state_word(fs), have, tot, pct, midhx, age);
|
||||
}
|
||||
snprintf(reply, 160, "OTA | this fw %s (%uK) | %s | serving:%s (%u) | trusted keys:%u | target %08X",
|
||||
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), dl,
|
||||
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
||||
(unsigned)c.allow.count(), (unsigned)board.getOtaTargetId());
|
||||
|
||||
// ---- what's available around me (catalogued from beacons + OTA_HAVE), best/most-recent first ----
|
||||
} else if (strncmp(a, "neighbors", 9) == 0 || strncmp(a, "nbrs", 4) == 0) {
|
||||
} else if (is_cmd(a, "neighbors|nbrs|updates|ls|n", &rest)) {
|
||||
// Kick a fresh round of catalog queries (async — rows arrive over the next seconds); render what we
|
||||
// have now. The reply buffer is 160 B (serial / one LoRa packet for remote-admin) so writes are bounded.
|
||||
// have now in plain words. The reply buffer is 160 B (serial / one LoRa packet for remote-admin), so
|
||||
// writes are bounded and extra rows collapse to "+N more".
|
||||
c.manager.queryAll();
|
||||
const int CAP = 160;
|
||||
int n = snprintf(reply, CAP, "nbrs #:mid t=tgt codec seed age *=cur (src=%u)", (unsigned)c.manager.sourceCount());
|
||||
int n = snprintf(reply, CAP, "Updates nearby (%u src) — `ota get <#>` to download:",
|
||||
(unsigned)c.manager.sourceCount());
|
||||
const uint8_t* cur = (c.manager.fetchState() != OtaManager::IDLE) ? c.manager.fetchManifestId() : nullptr;
|
||||
uint32_t now = millis(); int shown = 0, more = 0;
|
||||
for (uint8_t i = 0; i < c.manager.catalogCount(); i++) {
|
||||
const OtaManager::CatRow* h = c.manager.catalogRow(i);
|
||||
if (CAP - n < 60) { more++; continue; }
|
||||
char midhx[9]; mesh::Utils::toHex(midhx, h->mid, 4);
|
||||
if (CAP - n < 40) { more++; continue; }
|
||||
bool on = cur && memcmp(cur, h->mid, 4) == 0;
|
||||
uint32_t age = (now - h->last_ms) / 1000; if (age > 99999) age = 99999;
|
||||
n += snprintf(reply + n, CAP - n, "\n %d:%s t=%08X %s seed=%u %us%s", shown + 1, midhx,
|
||||
(unsigned)h->target_id, codec_name(h->codec), (unsigned)h->n_seeders,
|
||||
(unsigned)age, on ? "*" : "");
|
||||
char ver[20]; ver_str(ver, sizeof ver, h->fw_version);
|
||||
n += snprintf(reply + n, CAP - n, "\n %d) %s %s %u node%s %us%s", shown + 1, ver,
|
||||
codec_kind(h->codec), (unsigned)h->n_seeders, h->n_seeders == 1 ? "" : "s",
|
||||
(unsigned)age, on ? " [downloading]" : "");
|
||||
shown++;
|
||||
}
|
||||
if (more && n < CAP) snprintf(reply + n, CAP - n, "\n +%d more", more);
|
||||
if (shown == 0) strcpy(reply, "nbrs: none yet — sources beacon periodically; re-run in a few s (queried now)");
|
||||
if (shown == 0) strcpy(reply, "No updates seen yet — re-run `ota ls` in a few seconds (just asked around).");
|
||||
|
||||
// ---- start fetching a specific catalogued mOTA (by list index or manifest_id) ----
|
||||
} else if (strncmp(a, "pull", 4) == 0 && (a[4] == 0 || a[4] == ' ')) {
|
||||
const char* p = a + 4; while (*p == ' ') p++;
|
||||
if (*p == 0) { strcpy(reply, "usage: ota pull <#|mid8> (see `ota neighbors`)"); return true; }
|
||||
} else if (is_cmd(a, "pull|get|download", &rest)) {
|
||||
const char* p = rest;
|
||||
if (*p == 0) { strcpy(reply, "usage: ota get <#> (see the numbers in `ota ls`)"); return true; }
|
||||
const OtaManager::CatRow* sel = nullptr; uint8_t mid[4];
|
||||
if (*p == '#' || (p[0] >= '1' && p[0] <= '9' && (p[1] == 0 || p[1] == ' '))) { // index among catalogue
|
||||
int idx = atoi(*p == '#' ? p + 1 : p);
|
||||
@@ -101,7 +148,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
for (uint8_t i = 0; i < c.manager.catalogCount(); i++)
|
||||
if (memcmp(c.manager.catalogRow(i)->mid, mid, 4) == 0) { sel = c.manager.catalogRow(i); break; }
|
||||
}
|
||||
if (!sel) { strcpy(reply, "ERR no such neighbor (see `ota neighbors`)"); return true; }
|
||||
if (!sel) { strcpy(reply, "ERR no such update (see the numbers in `ota ls`)"); return true; }
|
||||
if (c.apply_pending) { strcpy(reply, "ERR busy applying"); return true; }
|
||||
uint8_t selmid[4]; uint32_t seltgt = sel->target_id; memcpy(selmid, sel->mid, 4); // sel may move on reset
|
||||
c.manager.reset_session(); c.fetch_store.clear();
|
||||
@@ -110,7 +157,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
sprintf(reply, "OK pulling mid=%s target=%08X (low priority)", midhx, (unsigned)seltgt);
|
||||
|
||||
// ---- discard the current session (e.g. a stalled old fetch) to free the slot ----
|
||||
} else if (strncmp(a, "drop", 4) == 0) {
|
||||
} else if (is_cmd(a, "drop|cancel|stop", &rest)) {
|
||||
OtaManager::FetchState fs = c.manager.fetchState();
|
||||
char midhx[9]; strcpy(midhx, "-");
|
||||
if (fs != OtaManager::IDLE) mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
||||
@@ -120,19 +167,19 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
|
||||
// ---- broadcast our tiny beacon so peers discover us. If not already serving, set up flash-backed
|
||||
// self-serve first (so we're a real, fetchable source of our own running firmware). ----
|
||||
} else if (strncmp(a, "announce", 8) == 0) {
|
||||
} else if (is_cmd(a, "announce|adv", &rest)) {
|
||||
if (!c.serving) c.serving = ota_serve_self(c, 0);
|
||||
c.manager.announce();
|
||||
sprintf(reply, "OK beacon sent (serving=%s)", c.serving ? "self fw" : "nothing");
|
||||
|
||||
// ---- running firmware identity (compare against a delta's base_hash) ----
|
||||
} else if (strncmp(a, "self", 4) == 0) {
|
||||
} else if (is_cmd(a, "self|id", &rest)) {
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid) { strcpy(reply, "ERR no EndF (firmware lacks the trailer?)"); return true; }
|
||||
char hx[17]; mesh::Utils::toHex(hx, fi.body_hash, 8);
|
||||
sprintf(reply, "self body=%u image=%u base_hash=%s", (unsigned)fi.body_len, (unsigned)fi.image_len, hx);
|
||||
|
||||
} else if (strncmp(a, "applydelta", 10) == 0) {
|
||||
} else if (is_cmd(a, "install|apply|applydelta", &rest)) {
|
||||
// Apply the fetched update. Destructive (reflashes + reboots) and GATED, not interactive (no "type
|
||||
// yes" round-trip — unreliable over LoRa): refuse unless the fetch is COMPLETE, then the apply path
|
||||
// validates in order (payload hash -> built-for-this-firmware -> signature/trust) and returns the
|
||||
@@ -151,8 +198,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
|
||||
// ---- external folder relay: advertise + serve `.mota` from a host daemon over the seeder UART, so the
|
||||
// node hosts MANY images (any architecture) it doesn't hold in flash. Trustless (fetchers verify). --
|
||||
} else if (strncmp(a, "folder", 6) == 0) {
|
||||
const char* p = a + 6; while (*p == ' ') p++;
|
||||
} else if (is_cmd(a, "folder|fold", &rest)) {
|
||||
const char* p = rest;
|
||||
if (strncmp(p, "on", 2) == 0) {
|
||||
#if defined(OTA_FOLDER_SERIAL)
|
||||
if (!c.serving) c.serving = ota_serve_self(c, 0); // keep serving our own fw alongside the folder
|
||||
@@ -176,8 +223,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
}
|
||||
|
||||
// ---- policy config (persisted via NodePrefs). conservative defaults: autofetch/autoinstall off ----
|
||||
} else if (strncmp(a, "config", 6) == 0) {
|
||||
const char* p = a + 6; while (*p == ' ') p++;
|
||||
} else if (is_cmd(a, "config|cfg|set", &rest)) {
|
||||
const char* p = rest;
|
||||
if (strncmp(p, "autofetch ", 10) == 0) {
|
||||
const char* v = p + 10;
|
||||
uint8_t pol = strncmp(v, "any", 3) == 0 ? OtaManager::AUTOFETCH_ANY
|
||||
@@ -204,24 +251,28 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
(unsigned)c.manager.checkpoint_blocks(), (unsigned)c.allow.count());
|
||||
}
|
||||
|
||||
// ---- trusted signer allowlist (security config; persisted) ----
|
||||
} else if (strncmp(a, "key add ", 8) == 0) {
|
||||
uint8_t pub[32];
|
||||
if (mesh::Utils::fromHex(pub, 32, a + 8) && c.allow.add(pub)) { c.config_dirty = true; strcpy(reply, "OK key added (saved)"); }
|
||||
else strcpy(reply, "ERR key");
|
||||
} else if (strncmp(a, "key list", 8) == 0) {
|
||||
int n = sprintf(reply, "keys=%u:", (unsigned)c.allow.count());
|
||||
for (uint8_t i = 0; i < c.allow.count() && n < 140; i++) {
|
||||
char hx[17]; mesh::Utils::toHex(hx, c.allow.get(i), 8);
|
||||
n += sprintf(reply + n, " %s", hx);
|
||||
// ---- trusted signer allowlist (security config; persisted): `ota key add|rm <hex>` / `ota key` lists ----
|
||||
} else if (is_cmd(a, "key|keys", &rest)) {
|
||||
const char* p = rest;
|
||||
if (strncmp(p, "add ", 4) == 0) {
|
||||
uint8_t pub[32];
|
||||
if (mesh::Utils::fromHex(pub, 32, p + 4) && c.allow.add(pub)) { c.config_dirty = true; strcpy(reply, "OK key added (saved)"); }
|
||||
else strcpy(reply, "ERR key");
|
||||
} else if (strncmp(p, "rm ", 3) == 0 || strncmp(p, "remove ", 7) == 0) {
|
||||
uint8_t pub[32]; const char* h = p + (p[0] == 'r' && p[1] == 'm' ? 3 : 7);
|
||||
if (mesh::Utils::fromHex(pub, 32, h) && c.allow.remove(pub)) { c.config_dirty = true; strcpy(reply, "OK removed (saved)"); }
|
||||
else strcpy(reply, "ERR");
|
||||
} else { // bare `ota key` (or `key list`) -> show them
|
||||
int n = snprintf(reply, 160, "trusted signer keys (%u):", (unsigned)c.allow.count());
|
||||
for (uint8_t i = 0; i < c.allow.count() && n < 140; i++) {
|
||||
char hx[17]; mesh::Utils::toHex(hx, c.allow.get(i), 8);
|
||||
n += snprintf(reply + n, 160 - n, " %s", hx);
|
||||
}
|
||||
if (c.allow.count() == 0) strcpy(reply, "no trusted signer keys yet (add one with `ota key add <hex>`)");
|
||||
}
|
||||
} else if (strncmp(a, "key rm ", 7) == 0) {
|
||||
uint8_t pub[32];
|
||||
if (mesh::Utils::fromHex(pub, 32, a + 7) && c.allow.remove(pub)) { c.config_dirty = true; strcpy(reply, "OK removed (saved)"); }
|
||||
else strcpy(reply, "ERR");
|
||||
|
||||
} else {
|
||||
strcpy(reply, "ota: status|neighbors|announce|pull <#|mid>|drop|folder|config|self|applydelta|key|dev");
|
||||
strcpy(reply, "Unknown OTA command. Type `ota help`.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -432,10 +432,15 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) {
|
||||
if (slot < 0) {
|
||||
slot = (_n_cat < OTA_MAX_CATALOG) ? _n_cat++ : lru;
|
||||
_catalog[slot] = CatRow{};
|
||||
memcpy(_catalog[slot].mid, mid, 4); memcpy(_catalog[slot].seeder0, hv.seeder_id, 4);
|
||||
memcpy(_catalog[slot].mid, mid, 4);
|
||||
memcpy(_catalog[slot].seeders[0], hv.seeder_id, 4);
|
||||
_catalog[slot].n_seeders = 1;
|
||||
} else if (memcmp(_catalog[slot].seeder0, hv.seeder_id, 4) != 0 && _catalog[slot].n_seeders < 255) {
|
||||
_catalog[slot].n_seeders++; // another distinct source has it
|
||||
} else {
|
||||
CatRow& cc = _catalog[slot]; // count DISTINCT sources (no double-count)
|
||||
bool known = false;
|
||||
for (uint8_t k = 0; k < cc.n_seeders; k++)
|
||||
if (memcmp(cc.seeders[k], hv.seeder_id, 4) == 0) { known = true; break; }
|
||||
if (!known && cc.n_seeders < OTA_CAT_SEEDERS) memcpy(cc.seeders[cc.n_seeders++], hv.seeder_id, 4);
|
||||
}
|
||||
CatRow& c = _catalog[slot];
|
||||
c.target_id = target; c.fw_version = fwver; c.codec = codec; c.flags = flags; c.last_ms = _now_ms;
|
||||
@@ -465,7 +470,7 @@ void OtaManager::startFetch(const uint8_t* mid, uint32_t target) {
|
||||
memcpy(_fid, mid, 4);
|
||||
_rng = (rd_u32(_seeder_id) ^ rd_u32(_fid)) | 1u; // per-node block-pick/jitter sequence (distinct per node)
|
||||
_fstate = WANT_MANIFEST;
|
||||
_mf_total = 0; _mf_mask = 0; _mf_len = 0; // fresh manifest reassembly
|
||||
_mf_total = 0; _mf_mask = 0; _mf_len = 0; _mf_retries = 0; // fresh manifest reassembly
|
||||
GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4);
|
||||
uint8_t b[16];
|
||||
emit(b, encode_get_manifest(b, sizeof(b), gm), false);
|
||||
@@ -497,6 +502,7 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
// a block must fit our reassembly buffer (and be non-empty) — reject an oversized block_size up front
|
||||
if (bs == 0 || bs > OTA_MAX_BLOCK || payload_size == 0) { _fstate = FAILED; return; }
|
||||
uint32_t bc = (payload_size + bs - 1) / bs;
|
||||
if (bc > 0xFFFFu) { _fstate = FAILED; return; } // block_idx is uint16 on the wire — can't address more
|
||||
memcpy(_froot, mf + 20, 4);
|
||||
|
||||
uint32_t leaves_off = 8 + mfl;
|
||||
@@ -576,7 +582,12 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
|
||||
return true;
|
||||
}
|
||||
_fstate = FETCHING; // resume fetching the holes
|
||||
requestMissing();
|
||||
// De-sync the first REQ exactly like a fresh fetch, so a coordinated reboot (whole site power-cycle)
|
||||
// doesn't make every resuming node REQ in lockstep. loop() fires the first REQ once the hold elapses.
|
||||
_rng = (rd_u32(_seeder_id) ^ rd_u32(_fid)) | 1u;
|
||||
_req_hold_at = _now_ms + (rngNext() % OTA_REQ_SPREAD_MS);
|
||||
_peer_req_block = 0xFFFFFFFFu; _peer_req_at = 0;
|
||||
_loop_last_have = _have; _loop_last_mask = _reasm_mask; // "no progress yet" -> loop will request after the hold
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -726,7 +737,9 @@ void OtaManager::loop() {
|
||||
sendQuery(_pq_seeder, _pq_digest, 0); // unfiltered: one broadcast HAVE serves everyone
|
||||
}
|
||||
if (_fstate == WANT_MANIFEST) {
|
||||
// the MANIFEST reply may have been lost on a marginal link — retry GET_MANIFEST
|
||||
// the MANIFEST reply may have been lost on a marginal link — retry GET_MANIFEST, but give up after a
|
||||
// cap so an unreachable mid doesn't pin the single fetch slot (or emit) forever.
|
||||
if (++_mf_retries > OTA_MANIFEST_MAX_RETRY) { _fstate = FAILED; return; }
|
||||
GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4);
|
||||
uint8_t b[16];
|
||||
emit(b, encode_get_manifest(b, sizeof(b), gm), false);
|
||||
|
||||
@@ -42,6 +42,9 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len)
|
||||
#ifndef OTA_MF_MAXFRAG
|
||||
#define OTA_MF_MAXFRAG 4 // max manifest fragments (a signed v2 manifest is ~2)
|
||||
#endif
|
||||
#ifndef OTA_MANIFEST_MAX_RETRY
|
||||
#define OTA_MANIFEST_MAX_RETRY 20 // give up (FAILED) after this many GET_MANIFEST retries — frees the slot
|
||||
#endif
|
||||
#ifndef OTA_MAX_SOURCES
|
||||
#define OTA_MAX_SOURCES 12 // heard OTA sources (beacon senders) tracked (LRU); ~12 B each
|
||||
#endif
|
||||
@@ -204,12 +207,15 @@ public:
|
||||
void on_message(const uint8_t* msg, uint16_t len); // feed one received OTA message
|
||||
void loop(); // drive fetch (re-request missing blocks)
|
||||
|
||||
// Drop the current fetch session back to IDLE (so a fresh `ota pull` / advert starts a new one).
|
||||
// Drop the current fetch session back to IDLE (so a fresh `ota pull` / advert starts a new one). Also
|
||||
// stops re-seeding a previously-completed download — callers clear the staging store right after, so the
|
||||
// re-seed view would otherwise advertise a mota we can no longer serve.
|
||||
void reset_session() {
|
||||
_fstate = IDLE; _have = 0; _req_count = 0;
|
||||
_fstate = IDLE; _have = 0; _req_count = 0; _mf_retries = 0;
|
||||
_reasm_block = 0xFFFFFFFFu; _reasm_mask = 0; _reasm_need = 0; _awaiting_proof = false;
|
||||
_loop_last_have = 0; _loop_last_mask = 0;
|
||||
_mf_total = 0; _mf_mask = 0; _mf_len = 0;
|
||||
unserveFetched();
|
||||
}
|
||||
|
||||
FetchState fetchState() const { return _fstate; }
|
||||
@@ -218,13 +224,14 @@ public:
|
||||
const uint8_t* fetchManifestId() const { return _fid; }
|
||||
|
||||
// --- discovery catalog (for `ota neighbors`): mOTAs heard around us via OTA_HAVE, deduped by mid ---
|
||||
static const uint8_t OTA_CAT_SEEDERS = 4; // distinct sources tracked per catalog row (for "N nodes have it")
|
||||
struct CatRow {
|
||||
uint8_t mid[4];
|
||||
uint32_t target_id, fw_version;
|
||||
uint8_t codec, flags;
|
||||
uint8_t seeder0[4]; // first source that advertised this mid
|
||||
uint8_t n_seeders; // distinct sources advertising it (saturates) — "N nodes have it"
|
||||
uint32_t have_max; // best block-count any source reported (== total when a full copy exists)
|
||||
uint8_t seeders[OTA_CAT_SEEDERS][4]; // distinct sources advertising this mid (deduped; capped)
|
||||
uint8_t n_seeders; // count of the above (capped at OTA_CAT_SEEDERS) — "N+ nodes have it"
|
||||
uint32_t have_max; // best block-count any source reported (== total when a full copy exists)
|
||||
uint32_t last_ms;
|
||||
};
|
||||
uint8_t catalogCount() const { return _n_cat; }
|
||||
@@ -319,7 +326,8 @@ private:
|
||||
uint16_t _loop_last_mask = 0; // fragment-level stall detection in loop()
|
||||
uint8_t _reasm_buf[OTA_MAX_BLOCK];
|
||||
// multi-fragment manifest reassembly (a signed v2 manifest exceeds one packet)
|
||||
uint8_t _mf_buf[256];
|
||||
uint8_t _mf_buf[OTA_MF_MAXFRAG * OTA_MF_FRAG]; // sized to the fragment cap so no valid manifest is silently dropped
|
||||
uint16_t _mf_retries = 0; // GET_MANIFEST retries while WANT_MANIFEST (give up after a cap)
|
||||
uint8_t _mf_total = 0; // frag_total of the manifest being reassembled (0 = none)
|
||||
uint16_t _mf_mask = 0; // received manifest-fragment bitmap
|
||||
uint32_t _mf_len = 0; // assembled manifest length (set by the last fragment)
|
||||
|
||||
@@ -70,6 +70,24 @@ TEST(OtaParse, RejectsTampering) {
|
||||
EXPECT_FALSE(mota_parse(b.data(), b.size(), m));
|
||||
}
|
||||
|
||||
// block_idx is a uint16 on the wire, so a manifest needing > 65535 blocks can't be addressed and must be
|
||||
// rejected at parse (this also keeps block_count*4 from overflowing the leaves-length computation).
|
||||
TEST(OtaParse, RejectsTooManyBlocks) {
|
||||
auto manifest = [](uint32_t payload_size, uint8_t bsl) { // minimal unsigned-full manifest (93 B)
|
||||
std::vector<uint8_t> m(93, 0);
|
||||
m[0] = MOTA_FORMAT_VER; m[1] = MFLAG_FULL; m[2] = 0x12;
|
||||
m[15] = payload_size; m[16] = payload_size >> 8; m[17] = payload_size >> 16; m[18] = payload_size >> 24;
|
||||
m[19] = bsl;
|
||||
return m;
|
||||
};
|
||||
MotaManifest mm;
|
||||
auto over = manifest(65536u * 1024u, 10); // 65536 blocks -> rejected
|
||||
EXPECT_FALSE(mota_parse_manifest(over.data(), over.size(), mm));
|
||||
auto ok = manifest(65535u * 1024u, 10); // 65535 blocks -> allowed
|
||||
EXPECT_TRUE(mota_parse_manifest(ok.data(), ok.size(), mm));
|
||||
EXPECT_EQ(mm.block_count, 65535u);
|
||||
}
|
||||
|
||||
TEST(OtaMerkle, RootMatchesVectorAndLeaves) {
|
||||
MotaManifest m;
|
||||
ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m));
|
||||
@@ -752,6 +770,71 @@ TEST(OtaTransfer, RejectsIncompatibleCodec) {
|
||||
g_q.clear();
|
||||
}
|
||||
|
||||
// Encode a 1-row OTA_HAVE from a specific seeder, carrying have_count (Phase-2 awareness).
|
||||
static uint16_t make_have_row(uint8_t* buf, uint16_t cap, const uint8_t mid[4], uint32_t target,
|
||||
uint32_t fwver, uint8_t codec, uint8_t flags,
|
||||
const uint8_t seeder[4], uint16_t have_count) {
|
||||
uint8_t row[OTA_HAVE_ROW_BYTES];
|
||||
memcpy(row, mid, 4);
|
||||
row[4]=target; row[5]=target>>8; row[6]=target>>16; row[7]=target>>24;
|
||||
row[8]=fwver; row[9]=fwver>>8; row[10]=fwver>>16; row[11]=fwver>>24;
|
||||
row[12]=codec; row[13]=flags;
|
||||
row[14]=(uint8_t)(have_count & 0xFF); row[15]=(uint8_t)(have_count >> 8);
|
||||
HaveMsg hv; memcpy(hv.seeder_id, seeder, 4); memset(hv.set_digest, 0, 4);
|
||||
hv.frag_idx=0; hv.frag_total=1; hv.n_rows=1; hv.rows=row;
|
||||
return encode_have(buf, cap, hv);
|
||||
}
|
||||
|
||||
// Catalog accounting: "N nodes have it" must count DISTINCT seeders (a repeated HAVE from one node must
|
||||
// not inflate it), and have_max tracks the best progress any source reported.
|
||||
TEST(OtaCatalog, DistinctSeederCountAndHaveCount) {
|
||||
OtaManager m; SendTo none{&m}; m.begin(SIM_TARGET_ID, sim_send, &none);
|
||||
uint8_t b[64]; uint8_t mid[4]={9,9,9,9};
|
||||
uint8_t s1[4]={1,0,0,0}, s2[4]={2,0,0,0};
|
||||
m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s1, 5));
|
||||
m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s1, 7)); // same seeder
|
||||
ASSERT_EQ(m.catalogCount(), 1);
|
||||
EXPECT_EQ(m.catalogRow(0)->n_seeders, 1); // counted once despite two HAVEs
|
||||
EXPECT_EQ(m.catalogRow(0)->have_max, 7u); // max progress seen
|
||||
m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s2, 3)); // new seeder
|
||||
EXPECT_EQ(m.catalogRow(0)->n_seeders, 2);
|
||||
EXPECT_EQ(m.catalogRow(0)->have_max, 7u); // still the max, not overwritten by the lower one
|
||||
g_q.clear();
|
||||
}
|
||||
|
||||
// An unanswered GET_MANIFEST must not pin the fetch slot forever: after OTA_MANIFEST_MAX_RETRY ticks with
|
||||
// no manifest, the session gives up (FAILED) so a new pull can take the slot. (Lowest-priority + bounded.)
|
||||
TEST(OtaTransfer, ManifestGiveUpAfterRetries) {
|
||||
g_q.clear();
|
||||
OtaManager client; OtaStoreRam<4096> store; SendTo to_none{&client};
|
||||
client.begin(SIM_TARGET_ID, sim_send, &to_none);
|
||||
client.set_fetch_store(&store);
|
||||
uint8_t mid[4]={7,7,7,7};
|
||||
client.pull(mid, SIM_TARGET_ID); // no server -> stuck WANT_MANIFEST
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
||||
for (int i = 0; i < OTA_MANIFEST_MAX_RETRY + 2; i++) { g_clk += 5000; client.set_clock(g_clk); client.loop(); g_q.clear(); }
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
||||
}
|
||||
|
||||
// Re-seeding a completed download must stop when the session is dropped (the staging store is cleared right
|
||||
// after), so the node never advertises a mota it can no longer serve.
|
||||
TEST(OtaSwarm, ReSeedStopsAfterDrop) {
|
||||
g_q.clear();
|
||||
OtaManager server, client; OtaStoreRam<4096> store;
|
||||
SendTo to_client{&client}, to_server{&server};
|
||||
server.begin(0, sim_send, &to_client);
|
||||
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
||||
client.set_fetch_store(&store);
|
||||
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
||||
server.announce();
|
||||
pump(client);
|
||||
ASSERT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
||||
EXPECT_EQ(client.servedCount(), 1); // re-seeding the completed download to peers
|
||||
client.reset_session();
|
||||
EXPECT_EQ(client.servedCount(), 0); // dropped -> stops advertising it
|
||||
}
|
||||
|
||||
// --- detools delta decode (vendored detools C decoder, CRLE-only build) ----------------------
|
||||
// Mirrors the device apply path (src/helpers/ota/OtaApply.cpp): base read via from_read/from_seek,
|
||||
// patch streamed via patch_read, output written via to_write. Proves the on-device delta apply uses
|
||||
|
||||
Reference in New Issue
Block a user