ota: pause a folder pull on link loss, resume + rescan on reconnect

If an `ota pull <#> folder` block-write fails mid-transfer (the motatool seeder
link dropped), the fetch enters a new PAUSED state instead of failing or falling
back to RAM/flash: progress stays on the host, the manager stops requesting, and
loop()/stall-detection leave it untouched (it waits indefinitely). On reconnect
the ESP32 seeder re-registers the folder destination and, if PAUSED, calls
resumeStaged(): OP_STAT re-attaches the host's partial, the leaves are re-read,
and only the missing blocks are re-requested (a brand-new/absent file restarts
from 0). `ota status` reports the paused state.
This commit is contained in:
Valentin Kivachuk Burda
2026-07-02 08:50:20 +02:00
parent 10fa434412
commit 28e3df02f9
4 changed files with 19 additions and 6 deletions
+3
View File
@@ -145,6 +145,9 @@ void halt() {
ota_seeder_attached = true;
char di[24]; snprintf(di, sizeof di, "tcp %s", ota_seeder_client.remoteIP().toString().c_str());
mesh::ota::ota_ctx().set_folder_dest(&ota_folder_store, di); // offer `ota pull <#> folder`
// if a folder pull PAUSED when the link dropped, the host still holds the partial: rescan + resume
if (mesh::ota::ota_ctx().manager.fetchState() == mesh::ota::OtaManager::PAUSED)
mesh::ota::ota_ctx().manager.resumeStaged(nullptr);
mesh::ota::ota_ctx().manager.announce(); // new served set -> advertise the folder's fw to peers
WIFI_DEBUG_PRINTLN("OTA seeder: client connected (%s) — relay + folder pull-dest ready", di);
} else {
+2
View File
@@ -29,6 +29,7 @@ static char fstate_char(OtaManager::FetchState s) {
case OtaManager::WANT_MANIFEST: return 'W';
case OtaManager::FETCHING: return 'F';
case OtaManager::COMPLETE: return 'C';
case OtaManager::PAUSED: return 'P';
default: return 'X';
}
}
@@ -44,6 +45,7 @@ static const char* state_word(OtaManager::FetchState s) {
case OtaManager::FETCHING: return "downloading";
case OtaManager::COMPLETE: return "ready to install";
case OtaManager::FAILED: return "failed";
case OtaManager::PAUSED: return "paused (folder link lost — reconnect to resume)";
default: return "?";
}
}
+10 -5
View File
@@ -446,7 +446,7 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) {
}
bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uint8_t flags) const {
if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST) return false; // busy with a session
if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST || _fstate == PAUSED) return false; // busy
if (_fstate == COMPLETE && memcmp(mid, _fid, 4) == 0) return false; // already have it
if (!codecOk(codec)) return false; // can't apply this codec
if (_have_desired_mid) // manual pull of a specific mid
@@ -475,7 +475,7 @@ void OtaManager::armFirstReqHold() {
// Begin (or resume) fetching a chosen mid: try a staged-partial resume first, else request the manifest.
void OtaManager::startFetch(const uint8_t* mid, uint32_t target) {
(void)target;
if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST) return;
if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST || _fstate == PAUSED) return;
if (resumeStaged(mid)) return; // resume a partial container left in flash
memcpy(_fid, mid, 4);
seedBlockRng(); // per-node block-pick/jitter sequence (distinct per node)
@@ -642,10 +642,15 @@ void OtaManager::handleProof(const uint8_t* m, uint16_t n) {
clearReassembly(); // bad -> drop, re-fetch the block
return;
}
// verified -> commit the payload block, then its leaf (the present marker)
if (!_fetch->write(_fpoff + (uint32_t)_reasm_block * _fbs, _reasm_buf, blen)) return;
// verified -> commit the payload block, then its leaf (the present marker). A write failure here means a
// FOLDER destination's seeder link dropped mid-transfer: PAUSE (hold progress on the host, stop
// requesting, do NOT fall back to RAM/flash). The block is left uncommitted (its leaf stays 0xFF), so on
// reconnect resumeStaged() re-requests exactly it. (A flash store never fails these writes.)
uint8_t leaf[4]; merkle_leaf(leaf, _reasm_buf, blen);
if (!_fetch->write(_floff + (uint32_t)_reasm_block * 4, leaf, 4)) return;
if (!_fetch->write(_fpoff + (uint32_t)_reasm_block * _fbs, _reasm_buf, blen) ||
!_fetch->write(_floff + (uint32_t)_reasm_block * 4, leaf, 4)) {
_fstate = PAUSED; clearReassembly(); return;
}
_have++;
OTA_DBG("OTA: block %u OK have=%u/%u\n", (unsigned)_reasm_block, (unsigned)_have, (unsigned)_fbc);
clearReassembly();
+4 -1
View File
@@ -96,7 +96,10 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len)
class OtaManager {
public:
enum FetchState : uint8_t { IDLE, WANT_MANIFEST, FETCHING, COMPLETE, FAILED };
// PAUSED: a folder-destination write failed mid-transfer (the seeder link dropped). Progress is held on
// the host; the manager stops requesting and does NOT fall back to RAM/flash. resumeStaged() (called on
// reconnect) re-STATs the host file, recomputes which blocks are missing, and resumes.
enum FetchState : uint8_t { IDLE, WANT_MANIFEST, FETCHING, COMPLETE, FAILED, PAUSED };
// Sentinel for "no block" in the reassembly / peer-REQ / recently-served slots (a real block index is
// a small uint16, so 0xFFFFFFFF is never valid).