ota: seeder storage protocol (pull-to-folder) + motatool serve storage handlers

Add the device->host WRITE half of the mota-seeder link so a device can capture
a .mota it is fetching off-mesh into the host folder (the same --dir used for
serving), stored as <mid>.mota — e.g. to grab an exact copy of a device's
firmware to build a delta against firmware you don't otherwise have.

- MotaSeederProto: OP_STAT / OP_BEGIN / OP_WRITE / OP_SREAD / OP_FIN (keyed by
  mid). Resume needs no host bookkeeping: BEGIN 0xFF-fills the file; on reconnect
  the device SREADs the leaves and re-requests only missing blocks (same as flash
  resume). Partial = <midhex>.mota.part, published to <midhex>.mota on FIN.
- motatool `serve --dir <folder>` now also handles the storage ops on the same
  folder/connection (SeederCore gains the store dir; serve_loop frames the
  variable-length WRITE). Host round-trip test added.

Firmware side (FolderMotaStore + `ota pull <#> <dest>` + pause/resume) follows.
This commit is contained in:
Valentin Kivachuk Burda
2026-07-02 08:32:00 +02:00
parent 259802b706
commit bd2ecb80a7
6 changed files with 153 additions and 8 deletions
+20 -2
View File
@@ -21,8 +21,19 @@
// MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1) total_size(4)
// leaves_off(4) block_count(4) payload_off(4) payload_size(4)
//
// status: 0 = OK, non-zero = error (idx out of range, read past EOF, ...). On error the response carries
// no payload (just magic+op+status+xsum).
// --- STORAGE ops (device -> host WRITE): "pull to folder". The device is fetching a `.mota` off the mesh
// (e.g. a neighbour's self-served firmware it has no local copy of) and streams it into the host folder as
// `<mid>.mota`, so the exact image can be captured for later delta-building. The device still initiates.
// Resume needs NO host bookkeeping: OP_BEGIN 0xFF-fills the file and, on reconnect, the device OP_SREADs the
// merkle leaves and re-requests only the missing blocks (identical to flash-resume). All keyed by mid[4].
// OP_STAT 0x04 args: mid(4) resp payload: present(1) total_size(4)
// OP_BEGIN 0x05 args: mid(4) total_size(4) resp: status OK (create/truncate, 0xFF-filled)
// OP_WRITE 0x06 args: mid(4) off(4) len(2) data(len) resp: status OK
// OP_SREAD 0x07 args: mid(4) off(4) len(2) resp payload: len bytes (0xFF = never written)
// OP_FIN 0x08 args: mid(4) resp: status OK (validate + make servable)
//
// status: 0 = OK, non-zero = error (idx out of range, read past EOF, no such stored file, ...). On error the
// response carries no payload (just magic+op+status+xsum).
namespace mesh {
namespace ota {
@@ -35,10 +46,17 @@ static const uint8_t MOTA_SEEDER_RSP_MAGIC1 = 's';
static const uint8_t MS_OP_COUNT = 0x01;
static const uint8_t MS_OP_DESCRIBE = 0x02;
static const uint8_t MS_OP_READ = 0x03;
static const uint8_t MS_OP_STAT = 0x04; // storage: does <mid>.mota exist on the host? + its size
static const uint8_t MS_OP_BEGIN = 0x05; // storage: create/truncate <mid>.mota (0xFF-filled)
static const uint8_t MS_OP_WRITE = 0x06; // storage: write bytes at offset
static const uint8_t MS_OP_SREAD = 0x07; // storage: read bytes back (resume: recompute missing blocks)
static const uint8_t MS_OP_FIN = 0x08; // storage: transfer complete — validate + make servable
static const uint8_t MS_STATUS_OK = 0x00;
static const uint8_t MS_STATUS_ERR = 0x01;
static const uint16_t MOTA_DESC_WIRE = 38; // bytes of a MotaDesc on the wire (see layout above)
static const uint16_t MOTA_SEEDER_WRITE_MAX = 512; // max data bytes per OP_WRITE/OP_SREAD (bounds frames)
} // namespace ota
} // namespace mesh