From ba18c32c04c3c2acb560cd96121d4bad113b945d Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Thu, 7 May 2026 14:03:42 -0400 Subject: [PATCH] feat(test-hooks): T:SEND/SENDOPP/SENDPROP + T:SETPROP/SYNCPROP harness API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a USB-CDC serial command interface gated behind PYXIS_TEST_HOOKS that lets a host-side harness drive pyxis end-to-end without UI taps. Built specifically to run /tmp/tdeck_harness.py and prove LXMF DIRECT, OPPORTUNISTIC, and PROPAGATED delivery against a Mac-side echo bot over the Mac's rnsd + lxmd. Commands (all newline-terminated, replies T:OK or T:ERR): T:DEST — pyxis's delivery dest hash (hex) T:ID — pyxis's identity hash (hex) T:ANN — force an announce T:PATHS — count + dump in-memory path table T:HASPATH — Transport::has_path + in-memory check T:RECALL — Identity::recall_app_data hex T:SEND — outbound DIRECT LXMessage T:SENDOPP — outbound OPPORTUNISTIC LXMessage T:SENDPROP — outbound PROPAGATED LXMessage T:SETPROP — set outbound propagation node T:SYNCPROP — request_messages_from_propagation_node T:SYNCSTATE — current PR_* sync state T:STATE — LXMessage state for a tracked send T:RX — drain inbound RX ring T:RXCLR — clear RX ring Build-flag side: -DPYXIS_TEST_HOOKS — gates all of the above -DPYXIS_TEST_TCP_HOST="..." — hard-overrides NVS tcp_host so the harness's rnsd is the only target -DPYXIS_TEST_TCP_PORT=... — same for tcp_port Also: replaces `lib_extra_dirs = deps/microReticulum` with an explicit `file://~/repos/microReticulum` lib_dep. lib_extra_dirs caused PIO to compile microReticulum twice (once through the extra dir, once through microLXMF's transitive auto-fetch), producing two copies of `Transport::_path_store` in BSS. Different translation units linked against different statics, so put() and exists() landed in different in-memory indexes. Symptom: `T:HASPATH` returned 0 even when the previous announce's `[ustore] put: wrote key` log line was visible. Single source path → single static → consistent reads. `patch_filestore.py` is committed but commented out in extra_scripts — used during diagnostic when the dual-static issue was being triaged. Easy to re-arm if FileStore put/exists drift recurs. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/microreticulum-shim/pyxis_test_hooks.h | 19 ++ lib/tdeck_ui/UI/LXMF/UIManager.cpp | 7 + patch_filestore.py | 70 ++++++ platformio.ini | 31 ++- src/main.cpp | 273 ++++++++++++++++++++- 5 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 lib/microreticulum-shim/pyxis_test_hooks.h create mode 100644 patch_filestore.py diff --git a/lib/microreticulum-shim/pyxis_test_hooks.h b/lib/microreticulum-shim/pyxis_test_hooks.h new file mode 100644 index 00000000..ba839a2e --- /dev/null +++ b/lib/microreticulum-shim/pyxis_test_hooks.h @@ -0,0 +1,19 @@ +// pyxis_test_hooks.h +// +// Header for the test-hook helpers defined in main.cpp under +// `-DPYXIS_TEST_HOOKS`. Declared at global scope (no namespace) so any +// translation unit can call them without ADL surprises. +// +// The implementations capture inbound messages so the harness on the +// Mac can poll T:RX and confirm round-trip. Without PYXIS_TEST_HOOKS +// these are not declared. + +#pragma once + +#ifdef PYXIS_TEST_HOOKS + +#include + +void pyxis_test_hook_record_rx(const ::LXMF::LXMessage& msg); + +#endif diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 7f1f8780..a5a4dda4 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -8,6 +8,9 @@ #include #include #include "Log.h" +#ifdef PYXIS_TEST_HOOKS +#include "pyxis_test_hooks.h" +#endif #include "Tone.h" #include "../LVGL/LVGLLock.h" #include "lxst_audio.h" @@ -743,6 +746,10 @@ void UIManager::on_message_received(::LXMF::LXMessage& message) { std::string msg = "Message received from " + source_hex + "..."; INFO(msg.c_str()); +#ifdef PYXIS_TEST_HOOKS + pyxis_test_hook_record_rx(message); +#endif + // Pre-graft: RNS::Identity::mark_persistent — fork-only. See note above. // (void)RNS::Identity::mark_persistent(message.source_hash()); diff --git a/patch_filestore.py b/patch_filestore.py new file mode 100644 index 00000000..6c8652e7 --- /dev/null +++ b/patch_filestore.py @@ -0,0 +1,70 @@ +""" +PlatformIO pre-build script: TEMPORARY diagnostic patch for +microStore's FileStore::exists(). + +Adds a printf at the top of `bool exists(const uint8_t*, uint8_t)` so +we can see why the path-table store's exists returns false even when +the most recent put for the same key succeeded. Remove once the +investigation is done. +""" +Import("env") +import os + +FILESTORE_H = os.path.join( + env.get("PROJECT_DIR", "."), + ".pio", "libdeps", "tdeck", "microStore", "include", "microStore", "FileStore.h", +) + +OLD = """\tbool exists(const uint8_t* key, uint8_t key_len) +\t{ + if (!isValid()) return false; +\t\tif(key_len > USTORE_MAX_KEY_LEN) return false; +\t\tIndexValue* e = index_find(key, key_len); +\t\tif (!e) return false; +\t\tif (is_ttl_expired_(e->timestamp, e->ttl)) { index_remove(key, key_len); return false; } +\t\treturn true; +\t}""" + +NEW = """\tbool exists(const uint8_t* key, uint8_t key_len) +\t{ +\t\tif (!isValid()) { printf("[ustore] exists: !isValid len=%u idx_size=%zu store=%p\\n", (unsigned)key_len, _index.size(), (void*)this); return false; } +\t\tif(key_len > USTORE_MAX_KEY_LEN) { printf("[ustore] exists: key too long\\n"); return false; } +\t\tIndexValue* e = index_find(key, key_len); +\t\tif (!e) { printf("[ustore] exists: not_in_index len=%u key=%s idx_size=%zu store=%p\\n", (unsigned)key_len, bin_str(key, key_len), _index.size(), (void*)this); return false; } +\t\tif (is_ttl_expired_(e->timestamp, e->ttl)) { printf("[ustore] exists: ttl_expired ts=%u ttl=%u now=%u\\n", e->timestamp, e->ttl, microStore::time()); index_remove(key, key_len); return false; } +\t\tprintf("[ustore] exists: found key=%s store=%p\\n", bin_str(key, key_len), (void*)this); +\t\treturn true; +\t}""" + +PUT_OLD = """\t\tindex_insert(key, key_len, current_segment, offset, ts, ttl); + +\t\t// Enforce max_recs:""" + +PUT_NEW = """\t\tindex_insert(key, key_len, current_segment, offset, ts, ttl); +\t\tprintf("[ustore] put: index_insert done key=%s idx_size=%zu store=%p\\n", bin_str(key, key_len), _index.size(), (void*)this); + +\t\t// Enforce max_recs:""" + +def patch(content): + out = content + if OLD in out: + out = out.replace(OLD, NEW) + print("PATCH: FileStore.h: exists() diagnostics added") + elif "store=%p" in content and "exists:" in content: + print("PATCH: FileStore.h: exists() already patched") + if PUT_OLD in out: + out = out.replace(PUT_OLD, PUT_NEW) + print("PATCH: FileStore.h: put-after-insert diagnostics added") + elif "put: index_insert done" in content: + print("PATCH: FileStore.h: put-after-insert already patched") + return out + +if os.path.exists(FILESTORE_H): + with open(FILESTORE_H) as f: + content = f.read() + new = patch(content) + if new != content: + with open(FILESTORE_H, "w") as f: + f.write(new) +else: + print("PATCH: FileStore.h not found, skipping") diff --git a/platformio.ini b/platformio.ini index 493cc5dd..d724c589 100644 --- a/platformio.ini +++ b/platformio.ini @@ -54,7 +54,9 @@ lib_deps = ; declared in library.json, so deps/microReticulum (via lib_extra_dirs) ; is the only microReticulum source. lib_ldf_mode = chain+ -lib_extra_dirs = deps/microReticulum +; (was: lib_extra_dirs = deps/microReticulum — replaced by an +; explicit file:// lib_dep below. lib_extra_dirs caused the duplicate +; compilation that produced two _path_store statics.) ; Build configuration build_type = release @@ -97,6 +99,14 @@ build_flags = ; sustained-write pattern is tractable. -DRNS_USE_FS -DRNS_PERSIST_PATHS + ; Test-mode hooks: hard-override the TCP server NVS settings to + ; point at the Mac-side rnsd (:4242) and add a `T:`- + ; prefixed serial command interface (T:DEST, T:SEND, T:STATE, + ; T:RX, T:ANN, etc.) for the harness to drive the device. Remove + ; this group of flags for production firmware. + -DPYXIS_TEST_HOOKS + '-DPYXIS_TEST_TCP_HOST=""' + -DPYXIS_TEST_TCP_PORT=4242 ; microStore LittleFS adapter — replaces pyxis's pre-graft ; lib/universal_filesystem/ (which targeted SPIFFS directly). ; LittleFS reuses the existing partition labeled "spiffs" (LittleFS's @@ -125,6 +135,8 @@ extra_scripts = pre:generate_splash.py pre:patch_nimble.py pre:patch_msgpack.py + ; pre:patch_filestore.py — diagnostic patch (very chatty); re-add + ; only when investigating put/exists drift in the path-table store platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino @@ -153,11 +165,18 @@ monitor_filters = ; declared in library.json, so deps/microReticulum (via lib_extra_dirs) ; is the only microReticulum source. lib_ldf_mode = chain+ -lib_extra_dirs = deps/microReticulum ; Build type build_type = release lib_deps = + ; Explicit file:// dep so PIO links exactly ONE microReticulum (the + ; live source). lib_extra_dirs was creating duplicate compilations + ; — PIO compiled deps/microReticulum/ as one library AND auto- + ; fetched the same source as another, leaving two copies of + ; Transport::_path_store / _new_path_table in BSS. Different TUs + ; ended up linking against different statics, so put() and + ; exists() landed in different stores. + file://~/repos/microReticulum lvgl/lvgl@^8.3.11 bblanchon/ArduinoJson@^7.4.2 hideakitai/MsgPack@^0.4.2 @@ -220,6 +239,14 @@ build_flags = ; sustained-write pattern is tractable. -DRNS_USE_FS -DRNS_PERSIST_PATHS + ; Test-mode hooks: hard-override the TCP server NVS settings to + ; point at the Mac-side rnsd (:4242) and add a `T:`- + ; prefixed serial command interface (T:DEST, T:SEND, T:STATE, + ; T:RX, T:ANN, etc.) for the harness to drive the device. Remove + ; this group of flags for production firmware. + -DPYXIS_TEST_HOOKS + '-DPYXIS_TEST_TCP_HOST=""' + -DPYXIS_TEST_TCP_PORT=4242 ; microStore LittleFS adapter — replaces pyxis's pre-graft ; lib/universal_filesystem/ (which targeted SPIFFS directly). ; LittleFS reuses the existing partition labeled "spiffs" (LittleFS's diff --git a/src/main.cpp b/src/main.cpp index 77a6648a..3d1834ff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -50,6 +50,10 @@ #include #include +#ifdef PYXIS_TEST_HOOKS +#include "pyxis_test_hooks.h" +#endif + // Hardware drivers #include #include @@ -440,6 +444,15 @@ void load_app_settings() { app_settings.tcp_host = prefs.getString("tcp_host", "sideband.connect.reticulum.network"); app_settings.tcp_port = prefs.getUShort("tcp_port", 4965); +#ifdef PYXIS_TEST_HOOKS + // Test mode: hard-override the TCP server so the harness on the Mac + // can reach this T-Deck regardless of what's persisted in NVS. The + // harness runs an rnsd with TCPServerInterface on the configured + // address; pyxis dials it as a TCP CLIENT. + app_settings.tcp_host = String(PYXIS_TEST_TCP_HOST); + app_settings.tcp_port = PYXIS_TEST_TCP_PORT; +#endif + // Identity app_settings.display_name = prefs.getString("disp_name", ""); @@ -1479,13 +1492,264 @@ static volatile uint8_t loop_step = 0; // Feed WDT and advance loop step tracker #define LOOP_STEP(n) do { loop_step = (n); esp_task_wdt_reset(); } while(0) +#ifdef PYXIS_TEST_HOOKS +// Test-hook serial command interface for the Mac-side harness. All +// outputs are prefixed `T:OK` or `T:ERR` so the harness can parse them +// out of the regular log stream. +// +// Commands: +// T:DEST — print our delivery dest hash +// T:ID — print our identity hash +// T:ANN — force an announce +// T:PATHS — print known path destination hashes +// T:HASPATH — query path-table membership +// T:RECALL — print app_data hex for that dest +// T:SEND — queue an outbound DIRECT LXMessage, +// print the message hash on success +// T:STATE — print the LXMessage state if known +// T:RX — print received-message count then a +// one-line summary per message +// T:SETPROP — configure outbound propagation node +// T:SENDPROP — queue an outbound PROPAGATED message +// T:SYNCPROP — request_messages_from_propagation_node +// T:SYNCSTATE — print current PR_* sync state +static String hex_byte_to_string(const RNS::Bytes& b) { return String(b.toHex().c_str()); } + +static RNS::Bytes parse_hex_arg(const String& hex) { + std::string s = std::string(hex.c_str()); + RNS::Bytes b; + for (size_t i = 0; i + 1 < s.size(); i += 2) { + char buf[3] = {s[i], s[i+1], 0}; + b << (uint8_t)strtoul(buf, nullptr, 16); + } + return b; +} + +// Track sent messages so T:STATE can look them up. Capped circular +// buffer; oldest entries drop on overflow. Index 0 = most recent. +struct TestSentEntry { RNS::Bytes hash; LXMF::LXMessage msg; bool in_use = false; }; +static const size_t TEST_SENT_RING = 16; +static TestSentEntry test_sent_ring[TEST_SENT_RING]; +static size_t test_sent_head = 0; +static void test_sent_record(const LXMF::LXMessage& msg) { + test_sent_ring[test_sent_head].hash = msg.hash(); + test_sent_ring[test_sent_head].msg = msg; + test_sent_ring[test_sent_head].in_use = true; + test_sent_head = (test_sent_head + 1) % TEST_SENT_RING; +} +static LXMF::LXMessage* test_sent_find(const RNS::Bytes& hash) { + for (size_t i = 0; i < TEST_SENT_RING; ++i) { + if (test_sent_ring[i].in_use && test_sent_ring[i].hash == hash) { + return &test_sent_ring[i].msg; + } + } + return nullptr; +} + +// Track received messages so T:RX can summarize. +struct TestRxEntry { RNS::Bytes source; RNS::Bytes content; bool in_use = false; }; +static const size_t TEST_RX_RING = 32; +static TestRxEntry test_rx_ring[TEST_RX_RING]; +static size_t test_rx_count = 0; + +// Public wrapper exposed via pyxis_test_hooks.h (global scope, no +// namespace) so other TUs (eg UIManager.cpp) can record received +// messages without ADL gymnastics. +void pyxis_test_hook_record_rx(const ::LXMF::LXMessage& msg) { + if (test_rx_count >= TEST_RX_RING) return; + test_rx_ring[test_rx_count].source = msg.source_hash(); + test_rx_ring[test_rx_count].content = msg.content(); + test_rx_ring[test_rx_count].in_use = true; + test_rx_count++; +} + +static const char* test_state_name(LXMF::Type::Message::State s) { + switch (s) { + case LXMF::Type::Message::GENERATING: return "GENERATING"; + case LXMF::Type::Message::OUTBOUND: return "OUTBOUND"; + case LXMF::Type::Message::SENDING: return "SENDING"; + case LXMF::Type::Message::SENT: return "SENT"; + case LXMF::Type::Message::DELIVERED: return "DELIVERED"; + case LXMF::Type::Message::REJECTED: return "REJECTED"; + case LXMF::Type::Message::CANCELLED: return "CANCELLED"; + case LXMF::Type::Message::FAILED: return "FAILED"; + default: return "UNKNOWN"; + } +} + +static void handle_test_hook_command(const String& line) { + int sep = line.indexOf(' '); + String cmd = (sep < 0) ? line : line.substring(0, sep); + String args = (sep < 0) ? "" : line.substring(sep + 1); + + if (cmd == "T:DEST") { + if (!router) { Serial.println("T:ERR no router"); return; } + Serial.println(String("T:OK ") + router->delivery_destination().hash().toHex().c_str()); + } + else if (cmd == "T:ID") { + Serial.println(String("T:OK ") + identity->hash().toHex().c_str()); + } + else if (cmd == "T:ANN") { + if (!router) { Serial.println("T:ERR no router"); return; } + router->announce(); + Serial.println("T:OK announced"); + } + else if (cmd == "T:PATHS") { + const auto& path_table = RNS::Transport::get_path_table(); + Serial.print("T:OK count="); + Serial.println(String((unsigned)path_table.size())); + for (const auto& kv : path_table) { + Serial.print("T:PATH "); + Serial.println(kv.first.toHex().c_str()); + } + } + else if (cmd == "T:HASPATH") { + RNS::Bytes dest = parse_hex_arg(args); + if (dest.size() != 16) { Serial.println("T:ERR bad hex"); return; } + bool has = RNS::Transport::has_path(dest); + // Diagnostic: also dump whether the in-memory _path_table has it, + // and the size of each store. They should match when the dual- + // write fix is working. + const auto& mem_table = RNS::Transport::get_path_table(); + bool mem_has = (mem_table.find(dest) != mem_table.end()); + Serial.print("T:OK "); + Serial.print(has ? "1" : "0"); + Serial.print(" mem="); + Serial.print(mem_has ? "1" : "0"); + Serial.print(" mem_count="); + Serial.println(String((unsigned)mem_table.size())); + } + else if (cmd == "T:RECALL") { + RNS::Bytes dest = parse_hex_arg(args); + if (dest.size() != 16) { Serial.println("T:ERR bad hex"); return; } + RNS::Bytes app = RNS::Identity::recall_app_data(dest); + Serial.println(String("T:OK size=") + String((unsigned)app.size()) + + " hex=" + app.toHex().c_str()); + } + else if (cmd == "T:SEND" || cmd == "T:SENDOPP") { + if (!router) { Serial.println("T:ERR no router"); return; } + int sp = args.indexOf(' '); + if (sp < 0) { Serial.println("T:ERR usage "); return; } + String hex = args.substring(0, sp); + String text = args.substring(sp + 1); + RNS::Bytes dest_hash = parse_hex_arg(hex); + if (dest_hash.size() != 16) { Serial.println("T:ERR bad hex"); return; } + RNS::Identity dest_identity = RNS::Identity::recall(dest_hash); + RNS::Destination destination(RNS::Type::NONE); + if (dest_identity) { + destination = RNS::Destination(dest_identity, RNS::Type::Destination::OUT, + RNS::Type::Destination::SINGLE, + "lxmf", "delivery"); + } + RNS::Bytes content_b((const uint8_t*)text.c_str(), text.length()); + RNS::Bytes title_b; + LXMF::Type::Message::Method method = (cmd == "T:SENDOPP") + ? LXMF::Type::Message::OPPORTUNISTIC + : LXMF::Type::Message::DIRECT; + LXMF::LXMessage msg(destination, router->delivery_destination(), + content_b, title_b, method); + if (!dest_identity) msg.destination_hash(dest_hash); + msg.pack(); + router->handle_outbound(msg); + test_sent_record(msg); + Serial.println(String("T:OK hash=") + msg.hash().toHex().c_str() + + " state=" + test_state_name(msg.state()) + + " method=" + (method == LXMF::Type::Message::OPPORTUNISTIC + ? "OPPORTUNISTIC" : "DIRECT")); + } + else if (cmd == "T:STATE") { + RNS::Bytes hash = parse_hex_arg(args); + LXMF::LXMessage* m = test_sent_find(hash); + if (!m) { Serial.println("T:ERR not found"); return; } + Serial.println(String("T:OK state=") + test_state_name(m->state())); + } + else if (cmd == "T:RX") { + Serial.print("T:OK count="); + Serial.println(String((unsigned)test_rx_count)); + for (size_t i = 0; i < test_rx_count; ++i) { + const auto& e = test_rx_ring[i]; + std::string c((const char*)e.content.data(), e.content.size()); + Serial.print("T:RXMSG src="); + Serial.print(e.source.toHex().c_str()); + Serial.print(" content="); + Serial.println(c.c_str()); + } + } + else if (cmd == "T:RXCLR") { + test_rx_count = 0; + Serial.println("T:OK cleared"); + } + else if (cmd == "T:SETPROP") { + // T:SETPROP — configure outbound propagation node. + if (!router) { Serial.println("T:ERR no router"); return; } + int sp = args.indexOf(' '); + String hex = (sp < 0) ? args : args.substring(0, sp); + int stamp_cost = (sp < 0) ? 0 : args.substring(sp + 1).toInt(); + RNS::Bytes node_hash = parse_hex_arg(hex); + if (node_hash.size() != 16) { Serial.println("T:ERR bad hex"); return; } + router->set_outbound_propagation_node(node_hash); + router->set_outbound_propagation_stamp_cost((uint8_t)stamp_cost); + Serial.println(String("T:OK pn=") + hex + " cost=" + String(stamp_cost)); + } + else if (cmd == "T:SENDPROP") { + // T:SENDPROP — send PROPAGATED via the + // currently-configured outbound propagation node. + if (!router) { Serial.println("T:ERR no router"); return; } + int sp = args.indexOf(' '); + if (sp < 0) { Serial.println("T:ERR usage T:SENDPROP "); return; } + String hex = args.substring(0, sp); + String text = args.substring(sp + 1); + RNS::Bytes dest_hash = parse_hex_arg(hex); + if (dest_hash.size() != 16) { Serial.println("T:ERR bad hex"); return; } + RNS::Identity dest_identity = RNS::Identity::recall(dest_hash); + RNS::Destination destination(RNS::Type::NONE); + if (dest_identity) { + destination = RNS::Destination(dest_identity, RNS::Type::Destination::OUT, + RNS::Type::Destination::SINGLE, + "lxmf", "delivery"); + } + RNS::Bytes content_b((const uint8_t*)text.c_str(), text.length()); + RNS::Bytes title_b; + LXMF::LXMessage msg(destination, router->delivery_destination(), + content_b, title_b, LXMF::Type::Message::PROPAGATED); + if (!dest_identity) msg.destination_hash(dest_hash); + msg.pack(); + router->handle_outbound(msg); + test_sent_record(msg); + Serial.println(String("T:OK hash=") + msg.hash().toHex().c_str() + + " state=" + test_state_name(msg.state()) + + " method=PROPAGATED"); + } + else if (cmd == "T:SYNCPROP") { + // T:SYNCPROP — kick off a sync from the configured propagation + // node. State machine progresses asynchronously; the harness + // can poll T:SYNCSTATE to track progress. + if (!router) { Serial.println("T:ERR no router"); return; } + router->request_messages_from_propagation_node(); + Serial.println("T:OK sync_requested"); + } + else if (cmd == "T:SYNCSTATE") { + // T:SYNCSTATE — return the current PR_* state of the prop sync FSM. + if (!router) { Serial.println("T:ERR no router"); return; } + Serial.print("T:OK state="); + Serial.println(String((unsigned)router->get_sync_state())); + } + else { + Serial.print("T:ERR unknown cmd "); + Serial.println(cmd); + } +} +#endif // PYXIS_TEST_HOOKS + void loop() { esp_task_wdt_reset(); // Handle OTA updates (must be called frequently) ArduinoOTA.handle(); - // Handle serial commands for web flasher detection + // Handle serial commands for web flasher detection + (under + // PYXIS_TEST_HOOKS) the harness command interface. Buffer up to + // 1024 chars so long T:SEND payloads work. while (Serial.available()) { char c = Serial.read(); if (c == '\n' || c == '\r') { @@ -1500,8 +1764,13 @@ void loop() { REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT); esp_restart(); } +#ifdef PYXIS_TEST_HOOKS + else if (serial_cmd_buffer.startsWith("T:")) { + handle_test_hook_command(serial_cmd_buffer); + } +#endif serial_cmd_buffer = ""; - } else if (serial_cmd_buffer.length() < 32) { + } else if (serial_cmd_buffer.length() < 1024) { serial_cmd_buffer += c; } }