Add BLE mesh logging

Made with Claude AI.
This commit is contained in:
Sybren A. Stüvel 2026-03-14 19:00:00 +01:00
parent aedc00e16a
commit 9f7d644a0c
9 changed files with 217 additions and 18 deletions

View file

@ -2,6 +2,11 @@
#include <Mesh.h>
#include "MyMesh.h"
#include <helpers/BLELogInterface.h>
#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 +100,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 +163,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)

View file

@ -2,6 +2,11 @@
#include <Mesh.h>
#include "MyMesh.h"
#include <helpers/BLELogInterface.h>
#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 +77,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 +123,7 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
#if MESH_PACKET_LOGGING && BLE_PACKET_LOGGING && defined(ESP32)
ble_log.loop();
#endif
}

View file

@ -217,21 +217,24 @@ 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,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
{
char buf[128];
snprintf(buf, sizeof(buf), "%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);
_packet_log->print(buf);
}
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]);
char buf[16];
snprintf(buf, sizeof(buf), " [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
_packet_log->print(buf);
} else {
Serial.printf("\n");
_packet_log->print("\n");
}
#endif
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
@ -338,14 +341,19 @@ 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);
{
char buf[128];
snprintf(buf, sizeof(buf), "%s: TX, len=%d (type=%d, route=%s, payload_len=%d)",
getLogDateTime(), len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
_packet_log->print(buf);
}
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]);
char buf[16];
snprintf(buf, sizeof(buf), " [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
_packet_log->print(buf);
} else {
Serial.printf("\n");
_packet_log->print("\n");
}
#endif
}

View file

@ -1,6 +1,9 @@
#pragma once
#include <MeshCore.h>
#if MESH_PACKET_LOGGING && ARDUINO
#include <Arduino.h>
#endif
#include <Identity.h>
#include <Packet.h>
#include <Utils.h>
@ -129,6 +132,9 @@ class Dispatcher {
void processRecvPacket(Packet* pkt);
void updateTxBudget();
#if MESH_PACKET_LOGGING
Print* _packet_log;
#endif
protected:
PacketManager* _mgr;
@ -150,6 +156,9 @@ protected:
tx_budget_ms = 0;
last_budget_update = 0;
duty_cycle_window_ms = 3600000;
#if MESH_PACKET_LOGGING
_packet_log = &Serial;
#endif
}
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
@ -172,6 +181,9 @@ protected:
public:
void begin();
void loop();
#if MESH_PACKET_LOGGING
void setPacketLogStream(Print* s) { if (s) _packet_log = s; }
#endif
Packet* obtainNewPacket();
void releasePacket(Packet* packet);

View file

@ -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]);

View file

@ -1,7 +1,7 @@
#pragma once
#include <MeshCore.h>
#include <Stream.h>
#include <Print.h>
#include <string.h>
namespace mesh {
@ -69,7 +69,7 @@ public:
/**
* \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.

View file

@ -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

View file

@ -0,0 +1,93 @@
#pragma once
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
// 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;
unsigned long _adv_restart_time;
char _line_buf[256];
int _line_len;
void flushLine() {
if (_line_len > 0 && _connected) {
_tx_char->setValue((uint8_t*)_line_buf, _line_len);
_tx_char->notify();
}
_line_len = 0;
}
void onConnect(BLEServer* pServer) override {
_connected = true;
}
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), _adv_restart_time(0), _line_len(0) {}
void begin(const char* device_name) {
BLEDevice::init(device_name);
_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());
service->start();
_server->getAdvertising()->addServiceUUID(NUS_SERVICE_UUID);
_server->getAdvertising()->start();
}
void loop() {
if (_adv_restart_time && millis() >= _adv_restart_time) {
if (_server->getConnectedCount() == 0) {
_server->getAdvertising()->start();
}
_adv_restart_time = 0;
}
}
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;
}
};

View file

@ -0,0 +1,43 @@
#pragma once
#include <Arduino.h>
#include <bluefruit.h>
#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);
}
};