mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-07-28 09:29:23 +00:00
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:
@@ -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
|
||||
|
||||
@@ -499,7 +499,7 @@ static int cmd_serve(const Args& a) {
|
||||
if (enable) { usleep(500000); t->write_str("ota folder on\r\n"); std::cout << "sent `ota folder on`\n"; }
|
||||
std::cout << "serving on " << target << " — Ctrl-C to stop\n";
|
||||
|
||||
SeederCore core(folder);
|
||||
SeederCore core(folder, a.get("dir")); // same folder doubles as "pull to folder" storage
|
||||
serve_loop(*t, core, verbose,
|
||||
[](const std::string& l) { std::cout << " [dev] " << l << "\n"; }, &g_stop);
|
||||
|
||||
|
||||
@@ -84,9 +84,15 @@ static constexpr uint8_t MS_RSP_MAGIC0 = 'm', MS_RSP_MAGIC1 = 's';
|
||||
static constexpr uint8_t MS_OP_COUNT = 0x01; // -> count(1)
|
||||
static constexpr uint8_t MS_OP_DESCRIBE = 0x02; // idx(1) -> MotaDesc(38)
|
||||
static constexpr uint8_t MS_OP_READ = 0x03; // idx(1) off(4) len(2) -> bytes
|
||||
static constexpr uint8_t MS_OP_STAT = 0x04; // storage: mid(4) -> present(1) total(4)
|
||||
static constexpr uint8_t MS_OP_BEGIN = 0x05; // storage: mid(4) total(4) -> OK (create 0xFF-filled)
|
||||
static constexpr uint8_t MS_OP_WRITE = 0x06; // storage: mid(4) off(4) len(2) data(len) -> OK
|
||||
static constexpr uint8_t MS_OP_SREAD = 0x07; // storage: mid(4) off(4) len(2) -> bytes (0xFF=unwritten)
|
||||
static constexpr uint8_t MS_OP_FIN = 0x08; // storage: mid(4) -> OK (validate + make servable)
|
||||
static constexpr uint8_t MS_STATUS_OK = 0x00;
|
||||
static constexpr uint8_t MS_STATUS_ERR = 0x01;
|
||||
static constexpr uint16_t MOTA_DESC_WIRE = 38;
|
||||
static constexpr uint16_t MOTA_SEEDER_WRITE_MAX = 512; // max data bytes per OP_WRITE/OP_SREAD
|
||||
// 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) [+2 reserved]
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <termios.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
@@ -64,6 +65,14 @@ std::array<uint8_t, MOTA_DESC_WIRE> SeederCore::describe(const ServedMota& s) {
|
||||
return w;
|
||||
}
|
||||
|
||||
std::string SeederCore::store_path(const uint8_t mid[4], bool part) const {
|
||||
static const char* H = "0123456789abcdef";
|
||||
std::string name;
|
||||
for (int i = 0; i < 4; i++) { name += H[mid[i] >> 4]; name += H[mid[i] & 0xF]; }
|
||||
name += part ? ".mota.part" : ".mota";
|
||||
return store_dir_ + "/" + name;
|
||||
}
|
||||
|
||||
bool SeederCore::handle(uint8_t op, const uint8_t* args, size_t arglen,
|
||||
uint8_t& status, std::vector<uint8_t>& payload) const {
|
||||
payload.clear();
|
||||
@@ -89,6 +98,70 @@ bool SeederCore::handle(uint8_t op, const uint8_t* args, size_t arglen,
|
||||
payload.assign(s->bytes.begin() + off, s->bytes.begin() + off + len);
|
||||
return true;
|
||||
}
|
||||
// --- STORAGE ops: "pull to folder" — capture a .mota the device is fetching off-mesh into <store_dir>.
|
||||
// All keyed by mid[4]; a partial pull is <midhex>.mota.part, published to <midhex>.mota on OP_FIN. ---
|
||||
if (op == MS_OP_STAT || op == MS_OP_BEGIN || op == MS_OP_WRITE || op == MS_OP_SREAD || op == MS_OP_FIN) {
|
||||
if (store_dir_.empty() || arglen < 4) { status = MS_STATUS_ERR; return true; }
|
||||
const std::string part = store_path(args, true), done = store_path(args, false);
|
||||
|
||||
if (op == MS_OP_STAT) { // present + size (completed wins over partial)
|
||||
struct stat sx{}; uint8_t present = 0; uint32_t total = 0;
|
||||
if (::stat(done.c_str(), &sx) == 0) { present = 1; total = (uint32_t)sx.st_size; }
|
||||
else if (::stat(part.c_str(), &sx) == 0) { present = 1; total = (uint32_t)sx.st_size; }
|
||||
payload.push_back(present);
|
||||
uint8_t tb[4]; wr_u32(tb, total); payload.insert(payload.end(), tb, tb + 4);
|
||||
return true;
|
||||
}
|
||||
if (op == MS_OP_BEGIN) { // fresh 0xFF-filled partial (start from 0)
|
||||
if (arglen < 8) { status = MS_STATUS_ERR; return true; }
|
||||
uint32_t total = rd_u32(args + 4);
|
||||
int fd = ::open(part.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) { status = MS_STATUS_ERR; return true; }
|
||||
uint8_t ff[4096]; memset(ff, 0xFF, sizeof ff);
|
||||
uint32_t left = total; bool ok = true;
|
||||
while (left && ok) { uint32_t c = left < sizeof ff ? left : (uint32_t)sizeof ff; ok = ::write(fd, ff, c) == (ssize_t)c; left -= c; }
|
||||
::close(fd);
|
||||
status = ok ? MS_STATUS_OK : MS_STATUS_ERR;
|
||||
return true;
|
||||
}
|
||||
if (op == MS_OP_WRITE) {
|
||||
if (arglen < 10) { status = MS_STATUS_ERR; return true; }
|
||||
uint32_t off = rd_u32(args + 4);
|
||||
uint16_t len = (uint16_t)(args[8] | (args[9] << 8));
|
||||
if (arglen < (size_t)10 + len) { status = MS_STATUS_ERR; return true; }
|
||||
int fd = ::open(part.c_str(), O_WRONLY);
|
||||
if (fd < 0) { status = MS_STATUS_ERR; return true; }
|
||||
bool ok = ::pwrite(fd, args + 10, len, off) == (ssize_t)len;
|
||||
::close(fd);
|
||||
status = ok ? MS_STATUS_OK : MS_STATUS_ERR;
|
||||
return true;
|
||||
}
|
||||
if (op == MS_OP_SREAD) { // read back (resume: recompute missing blocks)
|
||||
if (arglen < 10) { status = MS_STATUS_ERR; return true; }
|
||||
uint32_t off = rd_u32(args + 4);
|
||||
uint16_t len = (uint16_t)(args[8] | (args[9] << 8));
|
||||
const std::string src = (::access(part.c_str(), F_OK) == 0) ? part : done;
|
||||
int fd = ::open(src.c_str(), O_RDONLY);
|
||||
if (fd < 0) { status = MS_STATUS_ERR; return true; }
|
||||
payload.resize(len);
|
||||
bool ok = ::pread(fd, payload.data(), len, off) == (ssize_t)len;
|
||||
::close(fd);
|
||||
if (!ok) { payload.clear(); status = MS_STATUS_ERR; }
|
||||
return true;
|
||||
}
|
||||
if (op == MS_OP_FIN) { // light-validate (MAGIC + size) then publish
|
||||
struct stat sx{};
|
||||
if (::stat(part.c_str(), &sx) != 0) { status = MS_STATUS_ERR; return true; }
|
||||
uint8_t hd[8] = {0}; int fd = ::open(part.c_str(), O_RDONLY);
|
||||
bool got = fd >= 0 && ::read(fd, hd, 8) == 8;
|
||||
if (fd >= 0) ::close(fd);
|
||||
bool magic = got && hd[0] == 'm' && hd[1] == 'O' && hd[2] == 'T' && hd[3] == 'A';
|
||||
if (!magic || rd_u32(hd + 4) != (uint32_t)sx.st_size) { status = MS_STATUS_ERR; return true; }
|
||||
std::error_code ec; fs::rename(part, done, ec);
|
||||
status = ec ? MS_STATUS_ERR : MS_STATUS_OK;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // unknown op -> ignore (device retries)
|
||||
}
|
||||
|
||||
@@ -222,10 +295,20 @@ void serve_loop(Transport& t, const SeederCore& core, bool verbose,
|
||||
prev = -1;
|
||||
uint8_t op;
|
||||
if (!read_exact(t, &op, 1)) continue;
|
||||
size_t arglen = (op == MS_OP_DESCRIBE) ? 1 : (op == MS_OP_READ ? 7 : (op == MS_OP_COUNT ? 0 : SIZE_MAX));
|
||||
if (arglen == SIZE_MAX) continue; // unknown op
|
||||
uint8_t args[7];
|
||||
if (arglen && !read_exact(t, args, arglen)) continue;
|
||||
// fixed header bytes per op; OP_WRITE additionally reads `len` data bytes after its 10-byte header
|
||||
size_t hdr = (op == MS_OP_COUNT) ? 0 : (op == MS_OP_DESCRIBE) ? 1 : (op == MS_OP_READ) ? 7
|
||||
: (op == MS_OP_STAT || op == MS_OP_FIN) ? 4 : (op == MS_OP_BEGIN) ? 8
|
||||
: (op == MS_OP_SREAD || op == MS_OP_WRITE) ? 10 : SIZE_MAX;
|
||||
if (hdr == SIZE_MAX) continue; // unknown op
|
||||
uint8_t args[10 + MOTA_SEEDER_WRITE_MAX];
|
||||
if (hdr && !read_exact(t, args, hdr)) continue;
|
||||
size_t arglen = hdr;
|
||||
if (op == MS_OP_WRITE) { // variable payload: header(10) + data(len)
|
||||
uint16_t dlen = (uint16_t)(args[8] | (args[9] << 8));
|
||||
if (dlen > MOTA_SEEDER_WRITE_MAX) continue; // guard a runaway frame
|
||||
if (dlen && !read_exact(t, args + 10, dlen)) continue;
|
||||
arglen = (size_t)10 + dlen;
|
||||
}
|
||||
uint8_t xs;
|
||||
if (!read_exact(t, &xs, 1)) continue;
|
||||
if (xs != xor_bytes(args, arglen, op)) continue; // bad checksum -> ignore; device retries
|
||||
|
||||
@@ -33,12 +33,18 @@ private:
|
||||
// would call this directly from a characteristic-write handler and notify the reply — no framing needed.
|
||||
class SeederCore {
|
||||
public:
|
||||
explicit SeederCore(const Folder& f) : folder_(f) {}
|
||||
// `store_dir` (optional): folder where the "pull to folder" storage ops capture a `.mota` a device is
|
||||
// fetching off-mesh — the SAME --dir as serving. A partial pull is `<midhex>.mota.part`; OP_FIN publishes
|
||||
// it as `<midhex>.mota`. Empty store_dir = storage ops are refused (serve-only).
|
||||
explicit SeederCore(const Folder& f, std::string store_dir = "")
|
||||
: folder_(f), store_dir_(std::move(store_dir)) {}
|
||||
bool handle(uint8_t op, const uint8_t* args, size_t arglen,
|
||||
uint8_t& status, std::vector<uint8_t>& payload) const;
|
||||
static std::array<uint8_t, MOTA_DESC_WIRE> describe(const ServedMota& s);
|
||||
private:
|
||||
std::string store_path(const uint8_t mid[4], bool part) const; // <store_dir>/<midhex>.mota[.part]
|
||||
const Folder& folder_;
|
||||
std::string store_dir_;
|
||||
};
|
||||
|
||||
// A bidirectional byte link (the serial seeder needs this; BLE would reuse SeederCore directly).
|
||||
|
||||
@@ -282,6 +282,38 @@ static void t_folder_and_seeder() {
|
||||
uint8_t reof[7]; reof[0] = 0; wr_u32(reof + 1, (uint32_t)s0->bytes.size()); reof[5] = 16; reof[6] = 0;
|
||||
CHECK(core.handle(MS_OP_READ, reof, 7, status, pl) && status == MS_STATUS_ERR);
|
||||
|
||||
// --- STORAGE ops ("pull to folder"): STAT -> BEGIN -> WRITE -> SREAD -> FIN round-trip ---
|
||||
fs::path sdir = dir / "store"; fs::create_directories(sdir);
|
||||
SeederCore store(folder, sdir.string());
|
||||
uint8_t mid[4] = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
auto midpath = [&](bool part) {
|
||||
static const char* H = "0123456789abcdef"; std::string nm;
|
||||
for (int i = 0; i < 4; i++) { nm += H[mid[i] >> 4]; nm += H[mid[i] & 0xF]; }
|
||||
nm += part ? ".mota.part" : ".mota"; return (sdir / nm).string();
|
||||
};
|
||||
// STAT before anything -> present=0
|
||||
CHECK(store.handle(MS_OP_STAT, mid, 4, status, pl) && status == MS_STATUS_OK && pl.size() == 5 && pl[0] == 0);
|
||||
// BEGIN total=64 -> 0xFF-filled partial
|
||||
uint8_t beg[8]; std::memcpy(beg, mid, 4); wr_u32(beg + 4, 64);
|
||||
CHECK(store.handle(MS_OP_BEGIN, beg, 8, status, pl) && status == MS_STATUS_OK);
|
||||
CHECK(store.handle(MS_OP_STAT, mid, 4, status, pl) && pl[0] == 1 && rd_u32(pl.data() + 1) == 64);
|
||||
// WRITE the header (MAGIC + total) + a blob at offset 0
|
||||
const uint8_t blob[16] = {'m','O','T','A', 64,0,0,0, 1,2,3,4,5,6,7,8};
|
||||
uint8_t wr[10 + 16]; std::memcpy(wr, mid, 4); wr_u32(wr + 4, 0); wr[8] = 16; wr[9] = 0; std::memcpy(wr + 10, blob, 16);
|
||||
CHECK(store.handle(MS_OP_WRITE, wr, 10 + 16, status, pl) && status == MS_STATUS_OK);
|
||||
// SREAD back the 16 written bytes; an unwritten region reads 0xFF
|
||||
uint8_t srd[10]; std::memcpy(srd, mid, 4); wr_u32(srd + 4, 0); srd[8] = 16; srd[9] = 0;
|
||||
CHECK(store.handle(MS_OP_SREAD, srd, 10, status, pl) && pl.size() == 16 && std::memcmp(pl.data(), blob, 16) == 0);
|
||||
wr_u32(srd + 4, 32); srd[8] = 8; srd[9] = 0;
|
||||
CHECK(store.handle(MS_OP_SREAD, srd, 10, status, pl) && pl.size() == 8 && pl[0] == 0xFF && pl[7] == 0xFF);
|
||||
// FIN -> validates + publishes <mid>.mota
|
||||
CHECK(fs::exists(midpath(true)) && !fs::exists(midpath(false)));
|
||||
CHECK(store.handle(MS_OP_FIN, mid, 4, status, pl) && status == MS_STATUS_OK);
|
||||
CHECK(!fs::exists(midpath(true)) && fs::exists(midpath(false)) && fs::file_size(midpath(false)) == 64);
|
||||
CHECK(store.handle(MS_OP_STAT, mid, 4, status, pl) && pl[0] == 1 && rd_u32(pl.data() + 1) == 64);
|
||||
// storage refused on a serve-only core (empty store_dir)
|
||||
CHECK(core.handle(MS_OP_STAT, mid, 4, status, pl) && status == MS_STATUS_ERR);
|
||||
|
||||
fs::remove_all(dir);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user