From 12ff56bef073a664438b3931776b2aa59bd70a01 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 19 Jul 2026 23:57:48 -0700 Subject: [PATCH] feat(cli): add log tail on/off serial mirror Co-authored-by: Cursor --- docs/cli_commands.md | 14 +++++++++ examples/simple_repeater/MyMesh.cpp | 42 ++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 6 ++++ examples/simple_repeater/main.cpp | 5 +++ examples/simple_room_server/MyMesh.cpp | 42 ++++++++++++++++++++++++++ examples/simple_room_server/MyMesh.h | 6 ++++ examples/simple_room_server/main.cpp | 5 +++ examples/simple_sensor/SensorMesh.h | 1 + src/helpers/CommonCLI.cpp | 8 +++++ src/helpers/CommonCLI.h | 1 + 10 files changed, 130 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 819b6133..b87e4152 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -157,6 +157,20 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Mirror packet log to serial (live) +**Usage:** `log tail on` + +**Note:** Streams RX/TX log lines to USB serial as they are captured. No-op unless `log start` is active. Serial-only (not available via remote admin). + +--- + +### Stop live serial log mirror +**Usage:** `log tail off` + +**Serial Only:** Yes + +--- + ### Erase captured log **Usage:** `log erase` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b66e1952..7b011c98 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -472,6 +472,38 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { #endif } +void MyMesh::mirrorPacketLogRxToSerial(mesh::Packet *pkt, int len, float score) { + Serial.print(getLogDateTime()); + Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len, + pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000)); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + Serial.printf(" [%02X -> %02X]", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } + Serial.print("\r\n"); +} + +void MyMesh::mirrorPacketLogTxToSerial(mesh::Packet *pkt, int len) { + Serial.print(getLogDateTime()); + Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + Serial.printf(" [%02X -> %02X]", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } + Serial.print("\r\n"); +} + +void MyMesh::mirrorPacketLogTxFailToSerial(mesh::Packet *pkt, int len) { + Serial.print(getLogDateTime()); + Serial.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + Serial.print("\r\n"); +} + void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { #ifdef WITH_BRIDGE if (_prefs.bridge_pkt_src == 1) { @@ -496,6 +528,9 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { f.close(); } } + if (_logging && _tailing) { + mirrorPacketLogRxToSerial(pkt, len, score); + } } void MyMesh::logTx(mesh::Packet *pkt, int len) { @@ -521,6 +556,9 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) { f.close(); } } + if (_logging && _tailing) { + mirrorPacketLogTxToSerial(pkt, len); + } } void MyMesh::logTxFail(mesh::Packet *pkt, int len) { @@ -533,6 +571,9 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { f.close(); } } + if (_logging && _tailing) { + mirrorPacketLogTxFailToSerial(pkt, len); + } } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { @@ -864,6 +905,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; + _tailing = false; region_load_active = false; recv_pkt_region = NULL; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0b2e7491..14ac2316 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -86,6 +86,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; bool _logging; + bool _tailing; NodePrefs _prefs; ClientACL acl; CommonCLI _cli; @@ -129,6 +130,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { File openAppend(const char* fname); bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]); + void mirrorPacketLogRxToSerial(mesh::Packet* pkt, int len, float score); + void mirrorPacketLogTxToSerial(mesh::Packet* pkt, int len); + void mirrorPacketLogTxFailToSerial(mesh::Packet* pkt, int len); protected: float getAirtimeBudgetFactor() const override { @@ -206,6 +210,8 @@ public: void setLoggingOn(bool enable) override { _logging = enable; } + void setTailOn(bool enable) override { _tailing = enable; } + void eraseLogFile() override { _fs->remove(PACKET_LOG_FILE); } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68..95895d19 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -127,6 +127,11 @@ void loop() { int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); + if (c == 3) { // Ctrl+C — stop live log tail + the_mesh.setTailOn(false); + Serial.print("\r\n -> tail off\r\n"); + continue; + } if (c != '\n') { command[len++] = c; command[len] = 0; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e80..fce5c333 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -208,6 +208,38 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { #endif } +void MyMesh::mirrorPacketLogRxToSerial(mesh::Packet *pkt, int len, float score) { + Serial.print(getLogDateTime()); + Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len, + pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000)); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + Serial.printf(" [%02X -> %02X]", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } + Serial.print("\r\n"); +} + +void MyMesh::mirrorPacketLogTxToSerial(mesh::Packet *pkt, int len) { + Serial.print(getLogDateTime()); + Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + Serial.printf(" [%02X -> %02X]", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } + Serial.print("\r\n"); +} + +void MyMesh::mirrorPacketLogTxFailToSerial(mesh::Packet *pkt, int len) { + Serial.print(getLogDateTime()); + Serial.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + Serial.print("\r\n"); +} + void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { if (_logging) { File f = openAppend(PACKET_LOG_FILE); @@ -226,6 +258,9 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { f.close(); } } + if (_logging && _tailing) { + mirrorPacketLogRxToSerial(pkt, len, score); + } } void MyMesh::logTx(mesh::Packet *pkt, int len) { if (_logging) { @@ -244,6 +279,9 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) { f.close(); } } + if (_logging && _tailing) { + mirrorPacketLogTxToSerial(pkt, len); + } } void MyMesh::logTxFail(mesh::Packet *pkt, int len) { if (_logging) { @@ -255,6 +293,9 @@ void MyMesh::logTxFail(mesh::Packet *pkt, int len) { f.close(); } } + if (_logging && _tailing) { + mirrorPacketLogTxFailToSerial(pkt, len); + } } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { @@ -623,6 +664,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; _logging = false; + _tailing = false; region_load_active = false; set_radio_at = revert_radio_at = 0; recv_pkt_region = NULL; diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 6bab9dc2..a0342cb7 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -94,6 +94,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint64_t uptime_millis; unsigned long next_local_advert, next_flood_advert; bool _logging; + bool _tailing; bool region_load_active; NodePrefs _prefs; TransportKeyStore key_store; @@ -124,6 +125,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool processAck(const uint8_t *data); mesh::Packet* createSelfAdvert(); File openAppend(const char* fname); + void mirrorPacketLogRxToSerial(mesh::Packet* pkt, int len, float score); + void mirrorPacketLogTxToSerial(mesh::Packet* pkt, int len); + void mirrorPacketLogTxFailToSerial(mesh::Packet* pkt, int len); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); protected: @@ -200,6 +204,8 @@ public: void setLoggingOn(bool enable) override { _logging = enable; } + void setTailOn(bool enable) override { _tailing = enable; } + void eraseLogFile() override { _fs->remove(PACKET_LOG_FILE); } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d833fff3..1349bc26 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -107,6 +107,11 @@ void loop() { int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); + if (c == 3) { // Ctrl+C — stop live log tail + the_mesh.setTailOn(false); + Serial.print("\r\n -> tail off\r\n"); + continue; + } if (c != '\n') { command[len++] = c; command[len] = 0; diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 1d65b877..b850cf2d 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -65,6 +65,7 @@ public: void updateAdvertTimer() override; void updateFloodAdvertTimer() override; void setLoggingOn(bool enable) override { } + void setTailOn(bool enable) override { } void eraseLogFile() override { } void dumpLogFile() override { } void setTxPower(int8_t power_dbm) override; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c95e3e34..dd941a0c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -461,6 +461,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "off"); } + } else if (sender_timestamp == 0 && memcmp(command, "log tail on", 11) == 0 + && (command[11] == 0 || command[11] == ' ')) { + _callbacks->setTailOn(true); + strcpy(reply, " tail on"); + } else if (sender_timestamp == 0 && memcmp(command, "log tail off", 12) == 0 + && (command[12] == 0 || command[12] == ' ')) { + _callbacks->setTailOn(false); + strcpy(reply, " tail off"); } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f3abcf47..bbec37d4 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -78,6 +78,7 @@ public: virtual void updateAdvertTimer() = 0; virtual void updateFloodAdvertTimer() = 0; virtual void setLoggingOn(bool enable) = 0; + virtual void setTailOn(bool enable) = 0; virtual void eraseLogFile() = 0; virtual void dumpLogFile() = 0; virtual void setTxPower(int8_t power_dbm) = 0;