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:
1 parent
54c8b586c1
commit
280b9b1555
7 files changed
+243
-57
No files matched your search
+20
-12
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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...");
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = [&]() {
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user