ota: serve & manage OTA over WiFi (motatool --tcp + ESP32 companion seeder/console)

Extends the USB-serial folder relay to WiFi so an ESP32 companion can both
serve .mota and be operated headlessly:

- motatool `serve --tcp <host[:port]>`: a TcpTransport sibling of the serial
  transport (default port 5001). SeederCore/Folder are reused unchanged — the
  COUNT/DESCRIBE/READ protocol is transport-agnostic.
- ESP32 companion: a dedicated OTA seeder port (5001) for `serve --tcp`, plus
  an OTA text console on 5002 (`nc <ip> 5002` -> `ota status|ls|announce|...`,
  the same handle_ota_command CLI serial nodes have). Both run alongside the
  phone-app port (5000); all three coexist.
- WiFi.setSleep(false): ESP32 STA mode's modem power-save periodically sleeps
  the modem/CPU and stalls the SX1262 SPI+DIO servicing, leaving LoRa deaf
  while WiFi is associated. Disabling it restores the radio (HW-validated:
  a V3 WiFi companion is then discovered over LoRa and discovers its peers).
- docs: serving .mota over WiFi (protocol §10.2 + user guide).
This commit is contained in:
Valentin Kivachuk Burda committed 2026-06-29 20:24:24 +02:00
1 parent 54c8b586c1
commit 280b9b1555
7 files changed
+243 -57

No files matched your search

+20 -12
View File
@@ -496,11 +496,12 @@ blocks) and streams payload blocks from the source on demand; proofs are generat
### 10.2 The `mota-seeder` transport (`MotaSeederProto.h`)
A `MotaSource` is fed by a host that serves a folder over the device's **USB serial — the same console the
CLI uses** (no extra hardware). The host is the self-contained C++ tool `tools/motatool/` (`motatool serve`,
which also builds + validates `.mota` and runs on small hardware). The device only emits request frames
*while actively serving a fetch*, and reads the reply synchronously, so binary frames coexist with the text
CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:
A `MotaSource` is fed by a host that serves a folder over the device's **USB serial** (the same console the
CLI uses — no extra hardware) or, on an ESP32 WiFi companion, over **WiFi (TCP)**. The host is the
self-contained C++ tool `tools/motatool/` (`motatool serve --serial <port>` / `--tcp <host[:port]>`, which
also builds + validates `.mota` and runs on small hardware). The device only emits request frames *while
actively serving a fetch*, and reads the reply synchronously, so over the shared USB console binary frames
coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed:
```
request (device → host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args)
@@ -516,16 +517,23 @@ status: 0 = OK, non-zero = error (out of range / past EOF).
Device CLI: `ota folder on` (attach + announce), `ota folder` (list), `ota folder off`. Build flag
`OTA_FOLDER_SERIAL` (default stream = console `Serial`; override `OTA_FOLDER_SERIAL_STREAM` + define
`OTA_FOLDER_SERIAL_BEGIN` for a dedicated UART). Verified on hardware: a RAK4631 relays a host folder to a
Heltec V3 over one USB cable, every block merkle-checked.
`OTA_FOLDER_SERIAL_BEGIN` for a dedicated UART). On an ESP32 WiFi companion the node also runs a second
`WiFiServer` on a **dedicated seeder port** (`OTA_SEEDER_TCP_PORT`, default `5001`), separate from the
companion app port (`TCP_PORT`, default `5000`) — so `motatool serve --tcp` can feed updates while a phone
app stays connected. The node auto-attaches the source when a seeder client connects and detaches when it
closes (no `ota folder on` needed over TCP). Verified on hardware: a RAK4631 relays a host folder to a
Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (`:5001`) while the companion serves a
phone on `:5000` — every block merkle-checked.
**Transport-agnostic by design.** The request/response *semantics* (`COUNT` / `DESCRIBE(idx)` /
`READ(idx, off, len)` over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum +
resync framing above exists only because a shared USB-UART is an unreliable, unframed byte stream. Over a
framed/reliable link such as **BLE GATT** (e.g. an Android phone relaying a folder to a node), the same ops
carry over directly — a request characteristic write delivers `op + args` and the reply is a notification
of `status + payload`, with no magic/checksum needed. `motatool` reflects this split: a transport-free
`SeederCore` (the catalog logic) under a serial framing layer, so a BLE transport reuses the core verbatim.
resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a
reliable stream and the **WiFi (TCP)** transport reuses it as-is — both ends just treat the socket as a
byte stream (on-device, `SerialMotaSource` runs verbatim over an Arduino `Stream`-compatible `WiFiClient`;
`motatool`'s `TcpTransport` mirrors its `SerialTransport`). A future framed link such as **BLE GATT** (an
Android phone relaying a folder) could carry the same ops with no magic/checksum at all — a request
characteristic write delivers `op + args`, the reply notifies `status + payload`. `motatool` reflects this
split: a transport-free `SeederCore` (the catalog logic) under a swappable framing/transport layer.
---
+11 -5
View File
@@ -162,16 +162,22 @@ folder of firmware files to the mesh — without storing them itself. Useful for
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's USB port and the folder:
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 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/motatool/README.md](../tools/motatool/README.md).)
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 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.
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
+89 -1
View File
@@ -111,6 +111,79 @@ void halt() {
unsigned long last_wifi_reconnect_attempt = 0;
#endif
/* WIFI OTA SEEDER — relay a host folder of .mota over WiFi (motatool `serve --tcp`), on a DEDICATED port
separate from the companion (TCP_PORT), so a phone app stays connected while motatool feeds updates. */
#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
#include <helpers/ota/MotaSourceSerial.h>
#ifndef OTA_SEEDER_TCP_PORT
#define OTA_SEEDER_TCP_PORT 5001
#endif
static WiFiServer ota_seeder_server(OTA_SEEDER_TCP_PORT);
static WiFiClient ota_seeder_client; // the live seeder connection (reused)
static mesh::ota::SerialMotaSource ota_seeder_source(ota_seeder_client, 3000);
static bool ota_seeder_attached = false;
// Accept one motatool connection at a time; while connected, register its folder as a serve source so
// the node advertises + relays it over LoRa. Drop the source the moment the connection closes.
static void ota_seeder_loop() {
if (ota_seeder_client && ota_seeder_client.connected()) return; // still serving the current client
if (ota_seeder_attached) { // previous client just disconnected
mesh::ota::ota_ctx().detach_folder();
ota_seeder_attached = false;
WIFI_DEBUG_PRINTLN("OTA seeder: client disconnected, relay stopped");
}
WiFiClient c = ota_seeder_server.available();
if (c) {
ota_seeder_client = c; // rebind the persistent Stream to it
if (mesh::ota::ota_ctx().manager.add_source(&ota_seeder_source)) {
ota_seeder_attached = true;
WIFI_DEBUG_PRINTLN("OTA seeder: client connected, relaying its folder");
} else {
ota_seeder_client.stop(); // no free source slot
}
}
}
#endif
/* WIFI OTA CONSOLE — a tiny text CLI for OTA over WiFi. A WiFi companion has no serial text console, so
without this its OTA is only reachable through the phone app. Connect with e.g. `nc <ip> 5002` and type
`ota status` / `ota ls` / `ota announce` / ... — one client at a time, on a DEDICATED port separate from
the companion (5000) and the seeder (5001). */
#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA)
#include <helpers/ota/OtaCli.h> // mesh::ota::handle_ota_command(line, reply, board)
#ifndef OTA_CONSOLE_TCP_PORT
#define OTA_CONSOLE_TCP_PORT 5002
#endif
static WiFiServer ota_console_server(OTA_CONSOLE_TCP_PORT);
static WiFiClient ota_console_client;
static char ota_console_line[128];
static uint8_t ota_console_len = 0;
static void ota_console_loop() {
if (!ota_console_client || !ota_console_client.connected()) {
WiFiClient c = ota_console_server.available();
if (c) { ota_console_client = c; ota_console_len = 0;
ota_console_client.print("OTA console — type `ota ...` (e.g. ota status / ota ls / ota announce)\r\n> "); }
return;
}
while (ota_console_client.available()) {
char ch = (char)ota_console_client.read();
if (ch == '\r' || ch == '\n') {
if (ota_console_len == 0) continue; // ignore blanks / the CRLF pair
ota_console_line[ota_console_len] = 0;
char reply[160]; reply[0] = 0;
if (!mesh::ota::handle_ota_command(ota_console_line, reply, board))
strcpy(reply, "only `ota ...` commands are supported on this console");
ota_console_client.print(" -> "); ota_console_client.print(reply); ota_console_client.print("\r\n> ");
ota_console_len = 0;
} else if (ota_console_len < sizeof(ota_console_line) - 1) {
ota_console_line[ota_console_len++] = ch;
}
}
}
#endif
void setup() {
Serial.begin(115200);
@@ -208,13 +281,24 @@ void setup() {
WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect...");
wifi_needs_reconnect = true;
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
WIFI_DEBUG_PRINTLN("connected! IP %s (companion app on :%d)",
WiFi.localIP().toString().c_str(), TCP_PORT);
wifi_needs_reconnect = false;
}
});
WiFi.begin(WIFI_SSID, WIFI_PWD);
// Disable WiFi modem power-save: its periodic modem/light-sleep stalls the SX1262 SPI+DIO servicing,
// which makes the LoRa radio go deaf (no TX/RX) while WiFi is associated. Required for LoRa+WiFi to
// coexist on this ESP32 — the small extra idle current is well worth a working radio.
WiFi.setSleep(false);
serial_interface.begin(TCP_PORT);
#ifdef ENABLE_OTA
ota_seeder_server.begin(); // dedicated OTA seeder port for `motatool serve --tcp` (relay over LoRa)
WIFI_DEBUG_PRINTLN("OTA seeder listening on :%d (motatool serve --tcp)", OTA_SEEDER_TCP_PORT);
ota_console_server.begin(); // dedicated OTA text-console port (`nc <ip> 5002` -> `ota ...`)
WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc <ip> %d, type `ota ...`)", OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT);
#endif
#elif defined(BLE_PIN_CODE)
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#elif defined(SERIAL_RX)
@@ -257,6 +341,10 @@ void loop() {
}
#if defined(ESP32) && defined(WIFI_SSID)
#ifdef ENABLE_OTA
ota_seeder_loop(); // accept/drop a motatool `serve --tcp` connection on the dedicated seeder port
ota_console_loop(); // service the OTA text console (port 5002)
#endif
// Safely attempt to reconnect every 10 seconds if flagged
if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) {
WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect...");
+8 -5
View File
@@ -16,7 +16,7 @@ cross-checked byte-for-byte against the Python reference packager `tools/mota/mo
| `build` | Create a **full** or **delta** `.mota` from a firmware (local file **or** http(s) URL). |
| `verify` | Validate one or more `.mota` (merkle tree, leaves vs payload, image hash, Ed25519 signature). `--pub` requires a specific signer; `--base` confirms a sequential delta rebuilds its image. |
| `inspect`| Print every field of a `.mota`'s manifest (debugging). |
| `serve` | Serve a **folder** of `.mota` to a node over USB serial. Invalid files are warned about and skipped — one corrupt file never sinks the rest. |
| `serve` | Serve a **folder** of `.mota` to a node over USB serial (`--serial`) or WiFi (`--tcp`). Invalid files are warned about and skipped — one corrupt file never sinks the rest. |
| `keygen` | Generate an Ed25519 signing keypair (hex). |
Every command has detailed, example-rich help: `motatool <command> --help`.
@@ -71,8 +71,9 @@ $MT verify delta.mota --base old_firmware.bin
# 4. dump a single .mota's manifest fields
$MT inspect ./motas/RAK4631_04D413FD_v1.16.0_full_ABCD1234.mota
# 5. serve a folder to a node over its USB serial (recursive; skips non-.mota; warns on corrupt)
$MT serve --dir ./motas --serial /dev/ttyUSB0 --baud 115200 -v
# 5. serve a folder to a node (recursive; skips non-.mota; warns on corrupt)
$MT serve --dir ./motas --serial /dev/ttyUSB0 --baud 115200 -v # over USB serial
$MT serve --dir ./motas --tcp 192.168.1.50:5001 -v # …or over WiFi (ESP32 companion, port 5001)
```
`build` notes:
@@ -95,8 +96,10 @@ The protocol is split so the serving logic is reusable across links:
- **`SeederCore`** (`src/serve.{h,cpp}`) is transport-free: it maps a request `(op, args)` to a response
`(status, payload)` — `COUNT` / `DESCRIBE(idx)` / `READ(idx, off, len)` over the validated catalog.
- **`SerialTransport` + `serve_serial()`** wrap it with the byte-stream framing (magic + XOR checksum +
resync) for the unreliable USB-UART link (`MotaSeederProto.h`).
- **`Transport` + `serve_loop()`** wrap it with the byte-stream framing (magic + XOR checksum + resync,
`MotaSeederProto.h`). Two transports ship: **`SerialTransport`** (`--serial`, USB-UART) and
**`TcpTransport`** (`--tcp <host[:port]>`, WiFi — connects to the node's dedicated seeder port, default
`5001`, which the ESP32 companion runs alongside its phone-app port so both work at once).
To serve the same folder over **BLE** (e.g. an Android phone relaying to a MeshCore node), implement the
transport at the GATT layer — a request characteristic write hands `(op, args)` straight to
+50 -25
View File
@@ -81,7 +81,7 @@ static void help_top() {
" build Package a firmware as a .mota (a full image, or a small delta vs a previous build).\n"
" verify Check that .mota files are valid (block hashes, image hash, signature).\n"
" inspect Print every field of a .mota's manifest (debugging).\n"
" serve Serve a folder of .mota to a node over USB serial (corrupt files are skipped).\n"
" serve Serve a folder of .mota to a node over USB serial or WiFi (corrupt files are skipped).\n"
" keygen Generate an Ed25519 signing keypair.\n"
"\n"
"TYPICAL WORKFLOW\n"
@@ -194,29 +194,34 @@ static void help_inspect() {
static void help_serve() {
std::cout <<
"motatool serve — serve a folder of .mota to a MeshCore node over USB serial.\n"
"motatool serve — serve a folder of .mota to a MeshCore node over USB serial or WiFi (TCP).\n"
"\n"
"USAGE\n"
" motatool serve --dir <folder> --serial <port> [options]\n"
" motatool serve --dir <folder> --serial <port> [options] # over USB serial\n"
" motatool serve --dir <folder> --tcp <host[:port]> [options] # over WiFi (ESP32 companion)\n"
"\n"
"It scans the folder for .mota files, validates each, and serves the valid ones to the node.\n"
"Corrupt/invalid files are reported and skipped — one bad file never stops the rest. The node\n"
"advertises them to the mesh as if it held them, and any node whose hardware matches can fetch\n"
"them. The relay is trustless: fetchers verify every block, so this host never needs the keys.\n"
"\n"
"OPTIONS (--dir and --serial are required)\n"
" --dir <folder> folder of .mota to serve (searched recursively by default).\n"
" --serial <port> the node's USB serial port (e.g. /dev/ttyUSB0 or /dev/ttyACM0).\n"
" --baud <n> serial speed (default 115200).\n"
" --no-recursive serve only the top folder; don't descend into sub-folders.\n"
" --no-enable don't auto-send 'ota folder on'/'off' to the node (run them on its CLI yourself).\n"
" -v, --verbose log each request the node makes (COUNT / DESCRIBE / READ).\n"
"OPTIONS (--dir and one of --serial / --tcp are required)\n"
" --dir <folder> folder of .mota to serve (searched recursively by default).\n"
" --serial <port> the node's USB serial port (e.g. /dev/ttyUSB0 or /dev/ttyACM0).\n"
" --tcp <host[:port]> the node's WiFi seeder address (default port 5001). This is a DEDICATED\n"
" port, separate from the companion port (5000), so serving doesn't disturb a\n"
" phone app connected to the node. The node auto-enables relaying on connect.\n"
" --baud <n> serial speed (default 115200; --serial only).\n"
" --no-recursive serve only the top folder; don't descend into sub-folders.\n"
" --no-enable (--serial only) don't auto-send 'ota folder on'/'off' on the node's CLI.\n"
" -v, --verbose log each request the node makes (COUNT / DESCRIBE / READ).\n"
"\n"
"Leave it running; press Ctrl-C to stop (it tells the node to stop relaying first). It shares the\n"
"same USB cable as the node's text console — the node only pulls bytes while actively fetching.\n"
"Leave it running; press Ctrl-C to stop. Over serial it shares the USB cable with the node's text\n"
"console; over TCP it uses the node's dedicated seeder port. The node only pulls while fetching.\n"
"\n"
"EXAMPLE\n"
" motatool serve --dir ./motas --serial /dev/ttyUSB0 -v\n";
"EXAMPLES\n"
" motatool serve --dir ./motas --serial /dev/ttyUSB0 -v\n"
" motatool serve --dir ./motas --tcp 192.168.4.234 -v\n";
}
static void help_keygen() {
@@ -442,8 +447,9 @@ static int cmd_keygen(const Args& a) {
static int cmd_serve(const Args& a) {
if (a.has("help")) { help_serve(); return 0; }
if (!a.has("dir") || !a.has("serial")) {
std::cerr << "error: --dir and --serial are required\n\n"; help_serve(); return 2;
bool use_tcp = a.has("tcp");
if (!a.has("dir") || (!a.has("serial") && !use_tcp)) {
std::cerr << "error: --dir and one of --serial / --tcp are required\n\n"; help_serve(); return 2;
}
bool recursive = !a.has("no-recursive");
bool verbose = a.has("verbose");
@@ -465,20 +471,39 @@ static int cmd_serve(const Args& a) {
}
if (n == 0) std::cerr << " (nothing valid to serve)\n";
SerialTransport t;
std::string e = t.open(a.get("serial"), std::atoi(a.get("baud", "115200").c_str()));
if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; }
// Pick the transport: a serial port, or a TCP connection to the node's WiFi seeder port (host[:port],
// default port 5001). The node runs the seeder on a DEDICATED port, separate from its companion port,
// so serving over WiFi doesn't disturb a phone app connected to the companion.
SerialTransport st;
TcpTransport tt;
Transport* t = nullptr;
std::string target;
if (use_tcp) {
std::string hp = a.get("tcp");
size_t colon = hp.rfind(':');
std::string host = (colon == std::string::npos) ? hp : hp.substr(0, colon);
int port = (colon == std::string::npos) ? 5001 : std::atoi(hp.substr(colon + 1).c_str());
std::string e = tt.open(host, port);
if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; }
t = &tt; target = host + ":" + std::to_string(port);
} else {
std::string e = st.open(a.get("serial"), std::atoi(a.get("baud", "115200").c_str()));
if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; }
t = &st; target = a.get("serial") + " @ " + a.get("baud", "115200");
}
std::signal(SIGINT, on_sigint);
bool enable = !a.has("no-enable");
if (enable) { usleep(500000); t.write_str("ota folder on\r\n"); std::cout << "sent `ota folder on`\n"; }
std::cout << "serving on " << a.get("serial") << " @ " << a.get("baud", "115200") << " — Ctrl-C to stop\n";
// The CLI auto-enable only applies to the serial console; the TCP seeder port auto-enables relaying on
// the node side when this connection opens (and stops when it closes), so there's nothing to send.
bool enable = !use_tcp && !a.has("no-enable");
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);
serve_serial(t, core, verbose,
[](const std::string& l) { std::cout << " [dev] " << l << "\n"; }, &g_stop);
serve_loop(*t, core, verbose,
[](const std::string& l) { std::cout << " [dev] " << l << "\n"; }, &g_stop);
if (enable) { t.write_str("ota folder off\r\n"); usleep(200000); }
if (enable) { t->write_str("ota folder off\r\n"); usleep(200000); }
std::cout << "\nbye\n";
return 0;
}
+47 -4
View File
@@ -5,6 +5,8 @@
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netdb.h>
#include "util.h"
namespace fs = std::filesystem;
@@ -138,7 +140,48 @@ bool SerialTransport::write(const uint8_t* p, size_t n) {
return true;
}
// ---- serial framing loop --------------------------------------------------------------------------
// ---- TcpTransport ---------------------------------------------------------------------------------
std::string TcpTransport::open(const std::string& host, int port) {
addrinfo hints{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM;
addrinfo* res = nullptr;
std::string portstr = std::to_string(port);
if (getaddrinfo(host.c_str(), portstr.c_str(), &hints, &res) != 0 || !res)
return "cannot resolve host: " + host;
std::string err = "cannot connect to " + host + ":" + portstr;
for (addrinfo* p = res; p; p = p->ai_next) {
int fd = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0) continue;
if (::connect(fd, p->ai_addr, p->ai_addrlen) == 0) { fd_ = fd; err.clear(); break; }
::close(fd);
}
freeaddrinfo(res);
return err;
}
TcpTransport::~TcpTransport() { if (fd_ >= 0) ::close(fd_); }
int TcpTransport::read_byte(int timeout_ms) {
if (fd_ < 0) return -1;
fd_set rs; FD_ZERO(&rs); FD_SET(fd_, &rs);
timeval tv{ timeout_ms / 1000, (timeout_ms % 1000) * 1000 };
if (select(fd_ + 1, &rs, nullptr, nullptr, &tv) <= 0) return -1;
uint8_t b;
ssize_t n = ::recv(fd_, &b, 1, 0);
return n == 1 ? (int)b : -1; // 0 == peer closed -> treated as timeout/closed
}
bool TcpTransport::write(const uint8_t* p, size_t n) {
if (fd_ < 0) return false;
size_t off = 0;
while (off < n) {
ssize_t w = ::send(fd_, p + off, n - off, 0);
if (w < 0) return false;
off += (size_t)w;
}
return true;
}
// ---- seeder framing loop --------------------------------------------------------------------------
static uint8_t xor_bytes(const uint8_t* p, size_t n, uint8_t seed = 0) {
uint8_t x = seed; for (size_t i = 0; i < n; i++) x ^= p[i]; return x;
}
@@ -162,9 +205,9 @@ static void send_response(Transport& t, uint8_t op, uint8_t status, const std::v
t.write(frame.data(), frame.size());
}
void serve_serial(Transport& t, const SeederCore& core, bool verbose,
const std::function<void(const std::string&)>& devline,
const volatile bool* stop) {
void serve_loop(Transport& t, const SeederCore& core, bool verbose,
const std::function<void(const std::string&)>& devline,
const volatile bool* stop) {
std::string line;
int prev = -1;
auto flush_line = [&]() {
+18 -5
View File
@@ -59,10 +59,23 @@ private:
int fd_ = -1;
};
// Serial framing loop: resync on 'M''S', verify the request checksum, dispatch to `core`, frame the
// reply. Device text/log lines sharing the wire are surfaced via `devline`. Runs until *stop becomes true.
void serve_serial(Transport& t, const SeederCore& core, bool verbose,
const std::function<void(const std::string&)>& devline,
const volatile bool* stop);
// TCP client transport: connect to a node's WiFi seeder port (a dedicated port, NOT the companion port),
// then run the same seeder framing over the socket. Lets `serve` feed a WiFi node the same way as serial.
class TcpTransport : public Transport {
public:
std::string open(const std::string& host, int port); // "" on success
~TcpTransport() override;
int read_byte(int timeout_ms) override;
bool write(const uint8_t* p, size_t n) override;
private:
int fd_ = -1;
};
// Seeder framing loop (transport-agnostic): resync on 'M''S', verify the request checksum, dispatch to
// `core`, frame the reply. Device text/log lines sharing the wire are surfaced via `devline` (serial
// only; the TCP seeder port carries no log text). Runs until *stop becomes true.
void serve_loop(Transport& t, const SeederCore& core, bool verbose,
const std::function<void(const std::string&)>& devline,
const volatile bool* stop);
} // namespace mota