diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index c471100b8c..73d066bae0 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -446,14 +446,6 @@ const char *MyMesh::getLogDateTime() { return tmp; } -void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { -#if MESH_PACKET_LOGGING - Serial.print(getLogDateTime()); - Serial.print(" RAW: "); - mesh::Utils::printHex(Serial, raw, len); - Serial.println(); -#endif -} void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { #ifdef WITH_BRIDGE diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8857a1f71b..c13e00c263 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -137,8 +137,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool allowPacketForward(const mesh::Packet* packet) override; const char* getLogDateTime() override; - void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; - void logRx(mesh::Packet* pkt, int len, float score) override; void logTx(mesh::Packet* pkt, int len) override; void logTxFail(mesh::Packet* pkt, int len) override; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..ae6f99c246 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -2,6 +2,17 @@ #include #include "MyMesh.h" +#if BLE_PACKET_LOGGING + #if defined(NRF52_PLATFORM) || defined(ESP32) + #include + #else + #error "BLE_PACKET_LOGGING is not supported on this platform (only ESP32 and nRF52)" + #endif +#endif + +#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && (defined(NRF52_PLATFORM) || defined(ESP32)) + static BLELogInterface ble_log; +#endif #ifdef DISPLAY_CLASS #include "UITask.h" @@ -95,6 +106,11 @@ void setup() { the_mesh.begin(fs); +#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && (defined(NRF52_PLATFORM) || defined(ESP32)) + ble_log.begin(the_mesh.getNodeName()); + the_mesh.setPacketLogStream(&ble_log); +#endif + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -153,6 +169,9 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); +#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && defined(ESP32) + ble_log.loop(); +#endif if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 0cb0441240..fafa4f8791 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -193,14 +193,6 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t return 0; // unknown command } -void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { -#if MESH_PACKET_LOGGING - Serial.print(getLogDateTime()); - Serial.print(" RAW: "); - mesh::Utils::printHex(Serial, raw, len); - Serial.println(); -#endif -} void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { if (_logging) { diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 9e2fbffda2..7dde402ba3 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -124,7 +124,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { return _prefs.airtime_factor; } - void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override; void logRx(mesh::Packet* pkt, int len, float score) override; void logTx(mesh::Packet* pkt, int len) override; void logTxFail(mesh::Packet* pkt, int len) override; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..13a16ecbfe 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -2,6 +2,17 @@ #include #include "MyMesh.h" +#if BLE_PACKET_LOGGING + #if defined(NRF52_PLATFORM) || defined(ESP32) + #include + #else + #error "BLE_PACKET_LOGGING is not supported on this platform (only ESP32 and nRF52)" + #endif +#endif + +#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && (defined(NRF52_PLATFORM) || defined(ESP32)) + static BLELogInterface ble_log; +#endif #ifdef DISPLAY_CLASS #include "UITask.h" @@ -72,6 +83,11 @@ void setup() { the_mesh.begin(fs); +#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && (defined(NRF52_PLATFORM) || defined(ESP32)) + ble_log.begin(the_mesh.getNodeName()); + the_mesh.setPacketLogStream(&ble_log); +#endif + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -113,4 +129,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); +#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && defined(ESP32) + ble_log.loop(); +#endif } diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..66e1768575 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -217,21 +217,18 @@ void Dispatcher::checkRecv() { } if (pkt) { #if MESH_PACKET_LOGGING - Serial.print(getLogDateTime()); - Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d", - pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + _packet_log.printf("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d", + getLogDateTime(), pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, (int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time); - static uint8_t packet_hash[MAX_HASH_SIZE]; pkt->calculatePacketHash(packet_hash); - Serial.print(" hash="); - mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE); - + _packet_log.print(" hash="); + mesh::Utils::printHex(_packet_log, packet_hash, MAX_HASH_SIZE); 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]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + _packet_log.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); } else { - Serial.printf("\n"); + _packet_log.print("\n"); } #endif logRx(pkt, pkt->getRawLength(), score); // hook for custom logging @@ -338,14 +335,13 @@ void Dispatcher::checkSend() { outbound_expiry = futureMillis(max_airtime); #if MESH_PACKET_LOGGING - Serial.print(getLogDateTime()); - Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", - len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len); + _packet_log.printf("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)", + getLogDateTime(), len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len); if (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ || outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { - Serial.printf(" [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]); + _packet_log.printf(" [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]); } else { - Serial.printf("\n"); + _packet_log.print("\n"); } #endif } diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 2a99d0682b..5ae41e1925 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -1,6 +1,34 @@ #pragma once #include +#if MESH_PACKET_LOGGING && ARDUINO + #include + #include + + class LogPrint : public Print { + Print* _impl; + public: + LogPrint() : _impl(&Serial) {} + void setStream(Print* s) { if (s) _impl = s; } + size_t write(uint8_t c) override { return _impl->write(c); } + size_t write(const uint8_t* buf, size_t n) override { return _impl->write(buf, n); } + void printf(const char* fmt, ...) { + char buf[192]; // sized for longest log line: ~31 char datetime + ~90 char fields + va_list args; + va_start(args, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + if (n >= (int)sizeof(buf)) { + // truncation occurred: mark it visibly rather than silently losing data + buf[sizeof(buf) - 4] = '.'; + buf[sizeof(buf) - 3] = '.'; + buf[sizeof(buf) - 2] = '\n'; + buf[sizeof(buf) - 1] = '\0'; + } + _impl->print(buf); + } + }; +#endif #include #include #include @@ -129,6 +157,9 @@ class Dispatcher { void processRecvPacket(Packet* pkt); void updateTxBudget(); +#if MESH_PACKET_LOGGING + LogPrint _packet_log; +#endif protected: PacketManager* _mgr; @@ -154,7 +185,13 @@ class Dispatcher { virtual DispatcherAction onRecvPacket(Packet* pkt) = 0; - virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { } // custom hook + virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { // custom hook + #if MESH_PACKET_LOGGING + _packet_log.printf("%s RAW: ", getLogDateTime()); + mesh::Utils::printHex(_packet_log, raw, len); + _packet_log.print("\n"); + #endif + } virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging virtual void logTx(Packet* packet, int len) { } @@ -172,6 +209,9 @@ class Dispatcher { public: void begin(); void loop(); +#if MESH_PACKET_LOGGING + void setPacketLogStream(Print* s) { _packet_log.setStream(s); } +#endif Packet* obtainNewPacket(); void releasePacket(Packet* packet); diff --git a/src/Utils.cpp b/src/Utils.cpp index 186c8720a2..1b35371171 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -99,7 +99,7 @@ void Utils::toHex(char* dest, const uint8_t* src, size_t len) { *dest = 0; } -void Utils::printHex(Stream& s, const uint8_t* src, size_t len) { +void Utils::printHex(Print& s, const uint8_t* src, size_t len) { while (len > 0) { uint8_t b = *src++; s.print(hex_chars[b >> 4]); diff --git a/src/Utils.h b/src/Utils.h index 5736b8747a..34b5751fc3 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include namespace mesh { @@ -69,7 +69,7 @@ class Utils { /** * \brief Prints the hexadecimal representation of 'src' bytes of given length, to Stream 's'. */ - static void printHex(Stream& s, const uint8_t* src, size_t len); + static void printHex(Print& s, const uint8_t* src, size_t len); /** * \brief parse 'text' into parts separated by 'separator' char. diff --git a/src/helpers/BLELogInterface.h b/src/helpers/BLELogInterface.h new file mode 100644 index 0000000000..6556595338 --- /dev/null +++ b/src/helpers/BLELogInterface.h @@ -0,0 +1,17 @@ +#pragma once + +/** + * Platform-selecting shim for BLELogInterface. + * Include this header and use BLELogInterface directly; the correct + * platform implementation is pulled in automatically. + * + * Supported platforms: ESP32, nRF52. + * On unsupported platforms this header intentionally defines nothing — + * guard usage with #if defined(NRF52_PLATFORM) || defined(ESP32). + */ + +#if defined(NRF52_PLATFORM) + #include "nrf52/BLELogInterface.h" +#elif defined(ESP32) + #include "esp32/BLELogInterface.h" +#endif diff --git a/src/helpers/esp32/BLELogInterface.h b/src/helpers/esp32/BLELogInterface.h new file mode 100644 index 0000000000..8196bb4b4a --- /dev/null +++ b/src/helpers/esp32/BLELogInterface.h @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Nordic UART Service UUIDs (standard, recognised by nRF Connect and bleak) +#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define NUS_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +#define BLE_LOG_ADVERT_RESTART_DELAY_MS 1000 + +/** + * Unsecured BLE UART (Nordic UART Service) logger for ESP32 platforms. + * Implements Print so it can be passed to Dispatcher::setPacketLogStream(). + * Any BLE NUS client (e.g. nRF Connect, a Raspberry Pi running bleak) can + * connect without pairing and receive the log stream as plain text. + * + * Lines are buffered and flushed to BLE on each newline character. + * Call loop() from the Arduino loop() to handle advertising restart on disconnect. + */ +class BLELogInterface : public Print, BLEServerCallbacks { + BLEServer *_server; + BLECharacteristic *_tx_char; + bool _connected; + uint16_t _conn_id; + unsigned long _adv_restart_time; + char _line_buf[256]; + int _line_len; + + // Returns the max bytes per notification for the current connection. + // BLE notifications carry ATT_MTU-3 bytes of payload. Falls back to 20 + // (the minimum guaranteed by the spec) if MTU has not been negotiated yet. + int notifyPayloadSize() const { + const uint16_t mtu = _server->getPeerMTU(_conn_id); + return (mtu > 3) ? mtu - 3 : 20; + } + + void flushLine() { + if (_line_len == 0 || !_connected) return; + const int chunk = notifyPayloadSize(); + int offset = 0; + while (offset < _line_len) { + int n = _line_len - offset; + if (n > chunk) n = chunk; + _tx_char->setValue((uint8_t *)_line_buf + offset, n); + _tx_char->notify(); + offset += n; + } + _line_len = 0; + } + + void onConnect(BLEServer *pServer) override { + _connected = true; + } + + void onConnect(BLEServer *pServer, esp_ble_gatts_cb_param_t *param) override { + _connected = true; + _conn_id = param->connect.conn_id; + } + + void onDisconnect(BLEServer *pServer) override { + _connected = false; + _line_len = 0; // discard partial line + _adv_restart_time = millis() + BLE_LOG_ADVERT_RESTART_DELAY_MS; + } + +public: + BLELogInterface() + : _server(nullptr), _tx_char(nullptr), _connected(false), _conn_id(0), _adv_restart_time(0), + _line_len(0) {} + + void begin(const char *device_name) { + BLEDevice::init(device_name); + BLEDevice::setMTU(256); // raise the ceiling; client may negotiate a larger MTU + + // Explicitly disable bonding so the ESP32 does not send security requests. + // Without this the BLE stack initiates Just Works pairing by default, which + // fails with AUTH FAILED when no security callbacks are registered. + BLESecurity *pSecurity = new BLESecurity(); + pSecurity->setAuthenticationMode(ESP_LE_AUTH_NO_BOND); + pSecurity->setCapability(ESP_IO_CAP_NONE); + + _server = BLEDevice::createServer(); + _server->setCallbacks(this); + + BLEService *service = _server->createService(NUS_SERVICE_UUID); + _tx_char = service->createCharacteristic(NUS_TX_UUID, BLECharacteristic::PROPERTY_NOTIFY); + _tx_char->addDescriptor(new BLE2902()); + + // Characteristic Presentation Format (0x2904): declare value as UTF-8 string + // Format: format(1) exponent(1) unit(2) namespace(1) description(2) + static const uint8_t utf8_format[] = { 0x19, 0x00, 0x00, 0x27, 0x01, 0x00, 0x00 }; + BLEDescriptor *pFormat = new BLEDescriptor((uint16_t)0x2904); + pFormat->setValue(const_cast(utf8_format), sizeof(utf8_format)); + _tx_char->addDescriptor(pFormat); + service->start(); + + BLEAdvertising *adv = BLEDevice::getAdvertising(); + adv->addServiceUUID(NUS_SERVICE_UUID); + adv->setScanResponse(true); + adv->setMinPreferred(0x06); // helps iOS find and stay connected to the device + adv->setMaxPreferred(0x12); + BLEDevice::startAdvertising(); + } + + void loop() { + if (!_adv_restart_time || millis() < _adv_restart_time) return; + _adv_restart_time = 0; + if (_server->getConnectedCount() == 0) { + BLEDevice::startAdvertising(); + } + } + + size_t write(uint8_t c) override { + if (_line_len < (int)sizeof(_line_buf) - 1) { + _line_buf[_line_len++] = c; + } + if (c == '\n' || _line_len >= (int)sizeof(_line_buf) - 1) { + flushLine(); + } + return 1; + } + + size_t write(const uint8_t *buf, size_t size) override { + for (size_t i = 0; i < size; i++) + write(buf[i]); + return size; + } +}; diff --git a/src/helpers/nrf52/BLELogInterface.h b/src/helpers/nrf52/BLELogInterface.h new file mode 100644 index 0000000000..06cfc5b7f5 --- /dev/null +++ b/src/helpers/nrf52/BLELogInterface.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#ifndef BLE_LOG_TX_POWER + #define BLE_LOG_TX_POWER 4 +#endif + +/** + * Unsecured BLE UART (Nordic UART Service) logger for nRF52 platforms. + * Implements Print so it can be passed to Dispatcher::setPacketLogStream(). + * Any BLE NUS client (e.g. nRF Connect, a Raspberry Pi running bleak) can + * connect without pairing and receive the log stream as plain text. + */ +class BLELogInterface : public Print { + BLEUart _uart; + +public: + void begin(const char* device_name) { + Bluefruit.begin(); + Bluefruit.setTxPower(BLE_LOG_TX_POWER); + Bluefruit.setName(device_name); + + _uart.begin(); + + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addTxPower(); + Bluefruit.Advertising.addService(_uart); + Bluefruit.ScanResponse.addName(); + + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.start(0); + } + + size_t write(uint8_t c) override { + return _uart.write(c); + } + + size_t write(const uint8_t* buf, size_t size) override { + return _uart.write(buf, size); + } +}; diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 1dbda126d2..a57f049a63 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -178,6 +178,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' ; -D MESH_PACKET_LOGGING=1 +; -D BLE_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_tracker_base.build_src_filter} + diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index ec5cdb8390..d77920c51d 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -26,6 +26,15 @@ build_src_filter = ${rp2040_base.build_src_filter} + +<../variants/rpi_picow> lib_deps = ${rp2040_base.lib_deps} +; Use chain+ so the LDF evaluates preprocessor conditions when scanning for +; library dependencies. Without this, the LDF (in default 'deep' mode) ignores +; #ifdef guards and follows all #include directives unconditionally. That causes +; it to find #include inside the #ifdef ESP32 block in +; esp32/BLELogInterface.h, pulling in the Arduino-Pico BLE library. That +; library only compiles correctly when PIO_FRAMEWORK_ARDUINO_ENABLE_BLUETOOTH is +; set (which enables the required BTstack defines), so without that flag the +; build fails. chain+ prevents this by honouring the #ifdef ESP32 guard. +lib_ldf_mode = chain+ [env:PicoW_repeater] extends = rpi_picow diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index b9bd09f139..c191795c81 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -46,6 +46,7 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 +; -D BLE_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${SenseCap_Solar.build_src_filter} +<../examples/simple_repeater/*.cpp>