Harden USB logging and persist watchdog events in management reports
This commit is contained in:
1 parent
bd55369aa9
commit
3add4695cc
98 files changed
+7207
-372
No files matched your search
+1
-1
@@ -1232,7 +1232,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
|
||||
break;
|
||||
}
|
||||
|
||||
// MGR1 is a plaintext management extension, not an encrypted channel
|
||||
// MGR1/MGR2/MGR3 are plaintext management extensions, not encrypted channel
|
||||
// datagram. Keep the legacy group payload length shape for opaque relay
|
||||
// compatibility. Recognize it before any channel decryption/delivery.
|
||||
if (pkt->getPayloadType() == PAYLOAD_TYPE_GRP_DATA &&
|
||||
|
||||
+6
-6
@@ -46,7 +46,7 @@ namespace mesh {
|
||||
// overlapping debug formatter calls. The historical helper name
|
||||
// is retained for callers; ESP32 TinyUSB uses the same bounded formatter.
|
||||
inline size_t nrf52DebugPrintf(const char* format, ...) {
|
||||
if (format == nullptr || !isUsbLoggingEnabled()) return 0;
|
||||
if (format == nullptr || !isUsbDebugLoggingEnabled()) return 0;
|
||||
|
||||
static std::atomic_flag writer_busy = ATOMIC_FLAG_INIT;
|
||||
if (writer_busy.test_and_set(std::memory_order_acquire)) return 0;
|
||||
@@ -90,8 +90,8 @@ inline size_t nrf52DebugPrintf(const char* format, ...) {
|
||||
#define MESH_DEBUG_PRINT(F, ...) do { mesh::nrf52DebugPrintf("DEBUG: " F, ##__VA_ARGS__); } while(0)
|
||||
#define MESH_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("DEBUG: " F "\n", ##__VA_ARGS__); } while(0)
|
||||
#else
|
||||
#define MESH_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("DEBUG: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MESH_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("DEBUG: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define MESH_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("DEBUG: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MESH_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("DEBUG: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#endif
|
||||
#else
|
||||
#define MESH_DEBUG_PRINT(...) {}
|
||||
@@ -102,7 +102,7 @@ inline size_t nrf52DebugPrintf(const char* format, ...) {
|
||||
#if defined(NRF52_PLATFORM) || MESH_ESP32_USB_CONSOLE_COOPERATIVE
|
||||
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } while(0)
|
||||
#else
|
||||
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } } while(0)
|
||||
#define BRIDGE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__); } } while(0)
|
||||
#endif
|
||||
#else
|
||||
#define BRIDGE_DEBUG_PRINTLN(...) {}
|
||||
@@ -114,8 +114,8 @@ inline size_t nrf52DebugPrintf(const char* format, ...) {
|
||||
#define POWERSAVING_DEBUG_PRINT(F, ...) do { mesh::nrf52DebugPrintf("POWERSAVING: " F, ##__VA_ARGS__); } while(0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { mesh::nrf52DebugPrintf("POWERSAVING: " F "\n", ##__VA_ARGS__); } while(0)
|
||||
#else
|
||||
#define POWERSAVING_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("POWERSAVING: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("POWERSAVING: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define POWERSAVING_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("POWERSAVING: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("POWERSAVING: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#endif
|
||||
#else
|
||||
#define POWERSAVING_DEBUG_PRINT(...) {}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
#ifdef MQTT_DEBUG
|
||||
#include <Arduino.h>
|
||||
#define ALERT_DEBUG_PRINTLN(...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("Alert: " __VA_ARGS__); mesh::usbLoggingPort().println(); } } while (0)
|
||||
#define ALERT_DEBUG_PRINTLN(...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("Alert: " __VA_ARGS__); mesh::usbLoggingPort().println(); } } while (0)
|
||||
#else
|
||||
#define ALERT_DEBUG_PRINTLN(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <Arduino.h>
|
||||
#include "CommonCLI.h"
|
||||
#include "PrefsSaveReplyGuard.h"
|
||||
#include "UsbLoggingWatchdog.h"
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#include "CLICommandUtils.h"
|
||||
#include "FloodAdvertCLI.h"
|
||||
@@ -762,6 +763,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
_prefs->system_watchdog_enabled = 1;
|
||||
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
||||
_prefs->usb_logging_enabled = 1;
|
||||
_prefs->usb_debug_enabled = 0;
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
#else
|
||||
@@ -903,6 +905,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
if (loaded) syncOtaConfigFromPrefs(); // persisted OTA policy/keys -> OtaContext (else keep safe defaults)
|
||||
#endif
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
mesh::setUsbDebugEnabled(_prefs->usb_debug_enabled != 0);
|
||||
mesh::setUsbLoggingEnabled(_prefs->usb_logging_enabled != 0);
|
||||
#endif
|
||||
_radio_profiles.adoptPrimaryPreamble(_prefs->primary_radio_preamble);
|
||||
@@ -932,6 +935,9 @@ void CommonCLI::syncOtaConfigFromPrefs() {
|
||||
#endif
|
||||
|
||||
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
// Older or truncated preference images must never inherit debug opt-in
|
||||
// from a previous load of a newer image.
|
||||
_prefs->usb_debug_enabled = 0;
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = fs->open(filename, "r");
|
||||
#else
|
||||
@@ -1321,6 +1327,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (file.available() >= (int)sizeof(_prefs->espnow_bridge_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->espnow_bridge_enabled,
|
||||
sizeof(_prefs->espnow_bridge_enabled));
|
||||
if (file.available() >= (int)sizeof(_prefs->usb_debug_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->usb_debug_enabled,
|
||||
sizeof(_prefs->usb_debug_enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1433,6 +1443,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean
|
||||
_prefs->radio_fem_txgain = constrain(_prefs->radio_fem_txgain, 0, 1); // boolean
|
||||
_prefs->usb_logging_enabled = constrain(_prefs->usb_logging_enabled, 0, 1); // boolean
|
||||
// Only a deliberately saved 1 enables diagnostics; corrupted bytes are OFF.
|
||||
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
|
||||
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
|
||||
if (!directRetryPrefsValid(_prefs)) {
|
||||
setDefaultDirectRetryPrefs(_prefs);
|
||||
@@ -1645,6 +1657,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
WRITE_COMMON_PREFS(&prefs->bridge_format); // 863
|
||||
WRITE_COMMON_PREFS(&prefs->primary_radio_preamble); // appended primary tuple field
|
||||
WRITE_COMMON_PREFS(&prefs->espnow_bridge_enabled); // appended Full ESP-NOW intent
|
||||
WRITE_COMMON_PREFS(&prefs->usb_debug_enabled); // appended USB debug intent
|
||||
|
||||
#undef WRITE_COMMON_PREFS_BYTES
|
||||
#undef WRITE_COMMON_PREFS
|
||||
@@ -1657,6 +1670,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
if (plan.common) {
|
||||
_common_save_result_known = true;
|
||||
_common_save_succeeded = false;
|
||||
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
|
||||
}
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Observer builds use a verified temp/backup transaction for common prefs.
|
||||
@@ -1666,7 +1680,6 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
_observer_save_result_known = true;
|
||||
_observer_save_succeeded = saveMQTTPrefs(fs);
|
||||
}
|
||||
return;
|
||||
#else
|
||||
// Observer-only saves are a no-op on roles with no observer preference image.
|
||||
if (!plan.common) return;
|
||||
@@ -1803,6 +1816,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
file.write((uint8_t *)&_prefs->bridge_format, sizeof(_prefs->bridge_format)); // 863
|
||||
file.write((uint8_t *)&_prefs->primary_radio_preamble, sizeof(_prefs->primary_radio_preamble)); // appended
|
||||
file.write((uint8_t *)&_prefs->espnow_bridge_enabled, sizeof(_prefs->espnow_bridge_enabled)); // appended
|
||||
file.write((uint8_t *)&_prefs->usb_debug_enabled, sizeof(_prefs->usb_debug_enabled)); // appended
|
||||
|
||||
_common_save_succeeded = file.commit();
|
||||
if (!_common_save_succeeded) {
|
||||
@@ -1810,6 +1824,14 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
if (plan.common && _common_save_succeeded) {
|
||||
// Dynamic config imports use this path too. Do not enable diagnostics
|
||||
// until the corresponding common preference image is committed.
|
||||
mesh::setUsbDebugEnabled(_prefs->usb_debug_enabled != 0);
|
||||
mesh::setUsbLoggingEnabled(_prefs->usb_logging_enabled != 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
@@ -2599,6 +2621,7 @@ uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) {
|
||||
void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
PrefsSaveReplyGuard save_reply(_prefs_save_failures, reply);
|
||||
mesh::cli::normalizeCommandVerb(command);
|
||||
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
|
||||
if (handleManagementCommand(command, reply)) return;
|
||||
if (mesh::wireless::control().handle(command, reply, 160, millis(),
|
||||
_callbacks->wirelessCommandSource(sender_timestamp))) return;
|
||||
@@ -3398,6 +3421,27 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strncmp(config, "usb.debug", 9) == 0
|
||||
&& (config[9] == 0 || config[9] == ' ' || config[9] == '\t')) {
|
||||
const char* value = &config[9];
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
bool enabled = false, reboot_if_needed = false;
|
||||
if (mesh::cli::parseLoggingToggle(value, enabled, reboot_if_needed)
|
||||
&& !reboot_if_needed) {
|
||||
const uint8_t previous = _prefs->usb_debug_enabled;
|
||||
_prefs->usb_debug_enabled = enabled ? 1 : 0;
|
||||
if (!trySavePrefs()) {
|
||||
_prefs->usb_debug_enabled = previous;
|
||||
strcpy(reply, "Error: USB debug not saved; unchanged");
|
||||
return;
|
||||
}
|
||||
mesh::setUsbDebugEnabled(enabled);
|
||||
snprintf(reply, 160, "OK - USB debug %s (saved)", enabled ? "on" : "off");
|
||||
} else {
|
||||
strcpy(reply, "Error: usage set usb.debug on|off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MESH_USB_LOGGING_AVAILABLE && defined(WITH_MQTT_BRIDGE)
|
||||
if (strncmp(config, "logging.output", 14) == 0
|
||||
@@ -4686,6 +4730,11 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
mesh::isUsbLoggingEnabled() ? "on" : "off");
|
||||
return;
|
||||
}
|
||||
if (strcmp(config, "usb.debug") == 0) {
|
||||
snprintf(reply, 160, "> %s",
|
||||
mesh::isUsbDebugEnabled() ? "on" : "off");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MESH_USB_LOGGING_AVAILABLE && defined(WITH_MQTT_BRIDGE)
|
||||
if (strcmp(config, "logging.output") == 0) {
|
||||
|
||||
@@ -182,6 +182,9 @@ public:
|
||||
// older images remain readable and merged standard builds keep the former
|
||||
// logging artifact's enabled-at-first-boot behavior.
|
||||
uint8_t usb_logging_enabled = 1;
|
||||
// USB diagnostics are opt-in independently of the packet output gate.
|
||||
// Persisted at the very end of /com_prefs, not beside usb_log.
|
||||
uint8_t usb_debug_enabled = 0;
|
||||
// Runtime UART choice for merged RS-232 repeater artifacts. Appended at
|
||||
// /com_prefs offset 862; single-UART builds keep their compiled port here.
|
||||
uint8_t bridge_uart = 0;
|
||||
@@ -331,6 +334,7 @@ private:
|
||||
def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret));
|
||||
def("format", _parent->bridge_format);
|
||||
def("usb_log", _parent->usb_logging_enabled);
|
||||
def("usb_dbg", _parent->usb_debug_enabled);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -48,7 +48,7 @@ static void s2v(const uint8_t key[32], const uint8_t* aad, size_t aad_len,
|
||||
for (unsigned i = 0; i < 16; ++i) buf[len - 16 + i] ^= d[i];
|
||||
cmac(aes, buf, len, tag);
|
||||
} else {
|
||||
dbl(d); memcpy(buf, data, len); buf[len] = 0x80;
|
||||
dbl(d); if (len) memcpy(buf, data, len); buf[len] = 0x80;
|
||||
for (unsigned i = 0; i < 16; ++i) buf[i] ^= d[i];
|
||||
cmac(aes, buf, 16, tag);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,20 @@
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include "UsbLoggingStatus.h"
|
||||
|
||||
// Portable management wire format. No update authorization is granted by this
|
||||
// protocol. See docs/management_reports.md for the byte layout and trust model.
|
||||
namespace mesh { namespace management {
|
||||
constexpr size_t HEADER = 83, TAG = 16, ENTRY = 13, PER_PAGE = 6;
|
||||
constexpr size_t MAX_KEYS = 36, MAX_PAGES = 6, MAX_PAYLOAD = HEADER + TAG + PER_PAGE * ENTRY;
|
||||
constexpr size_t USB_STATUS_SIZE = 15, MGR2_HEADER = HEADER + USB_STATUS_SIZE;
|
||||
constexpr size_t MGR2_PER_PAGE = 5;
|
||||
constexpr size_t WATCHDOG_EVENT_OFFSET = MGR2_HEADER;
|
||||
constexpr size_t CURRENT_HEADER = MGR2_HEADER + USB_WATCHDOG_EVENT_SIZE;
|
||||
constexpr size_t CURRENT_PER_PAGE = 4;
|
||||
constexpr size_t MAX_KEYS = 36, MAX_PAGES = 9;
|
||||
constexpr size_t MAX_PAYLOAD = CURRENT_HEADER + TAG + CURRENT_PER_PAGE * ENTRY;
|
||||
static_assert(MAX_PAYLOAD == 179, "MGR3 must fit the legacy padded payload budget");
|
||||
constexpr uint32_t DAY = 86400, FLOOD_INTERVAL = 21 * DAY;
|
||||
enum Feature : uint8_t { WIFI = 1, GPS = 2, NTP = 4, USB = 8, OTA = 16 };
|
||||
enum Valid : uint16_t { FIRMWARE = 1, BOOTLOADER = 2, BASE = 4, STORE = 8,
|
||||
@@ -95,21 +103,64 @@ bool seal(const uint8_t key[32], const uint8_t* aad, size_t aad_len,
|
||||
bool open(const uint8_t key[32], const uint8_t* aad, size_t aad_len,
|
||||
uint8_t* data, size_t len, const uint8_t tag[16]);
|
||||
bool equal(const uint8_t* a, const uint8_t* b, size_t size);
|
||||
inline size_t pageSize(const uint8_t* p) { return HEADER + p[82] * ENTRY + TAG; }
|
||||
inline bool currentPage(const uint8_t* p) { return !memcmp(p, "MGR3", 4); }
|
||||
inline bool usbPage(const uint8_t* p) { return currentPage(p) || !memcmp(p, "MGR2", 4); }
|
||||
inline size_t headerSize(const uint8_t* p) {
|
||||
return currentPage(p) ? CURRENT_HEADER : usbPage(p) ? MGR2_HEADER : HEADER;
|
||||
}
|
||||
inline size_t entriesPerPage(const uint8_t* p) {
|
||||
return currentPage(p) ? CURRENT_PER_PAGE : usbPage(p) ? MGR2_PER_PAGE : PER_PAGE;
|
||||
}
|
||||
inline size_t pageSize(const uint8_t* p) { return headerSize(p) + p[82] * ENTRY + TAG; }
|
||||
inline size_t floodSize(size_t canonical) { return 3 + ((canonical - 3 + 15) / 16) * 16; }
|
||||
#if defined(STM32_PLATFORM) && defined(__GNUC__) && !defined(__clang__)
|
||||
// The two Mesh callers use different padding modes. Keep one validator on
|
||||
// flash-constrained STM32 targets instead of LTO specializing each call site.
|
||||
__attribute__((noinline, noclone))
|
||||
#endif
|
||||
inline bool validPage(const uint8_t* p, size_t size, bool flood_padding = false) {
|
||||
if (!p || size < HEADER + TAG || size > (flood_padding ? 179 : MAX_PAYLOAD) || memcmp(p, "MGR1", 4)) return false;
|
||||
if (!p || size < HEADER + TAG || size > MAX_PAYLOAD) return false;
|
||||
const uint32_t magic = read32(p);
|
||||
if ((magic & 0xffffffu) != 0x52474du) return false; // exact MGR prefix
|
||||
const unsigned version = (magic >> 24) - '1';
|
||||
if (version > 2) return false;
|
||||
const size_t minimum = version == 0 ? HEADER + TAG
|
||||
: version == 1 ? MGR2_HEADER + TAG : CURRENT_HEADER + TAG;
|
||||
const size_t per_page = PER_PAGE - version;
|
||||
if (size < minimum) return false;
|
||||
// Reserved bits occupy the upper flag byte and upper two stage bits.
|
||||
if (version && (read16(p + HEADER + 1) & 0xc0f8u)) return false;
|
||||
if (version == 2 && !validUsbWatchdogEvent(p + WATCHDOG_EVENT_OFFSET)) return false;
|
||||
const unsigned page = p[78], pages = p[79], total = p[80], first = p[81], count = p[82];
|
||||
const unsigned expected_pages = total ? (total + PER_PAGE - 1) / PER_PAGE : 1;
|
||||
if (total > MAX_KEYS || pages != expected_pages || page >= pages || first != page * PER_PAGE || first > total) return false;
|
||||
const unsigned expected_pages = total ? (total + per_page - 1) / per_page : 1;
|
||||
if (total > MAX_KEYS || pages != expected_pages || page >= pages || first != page * per_page) return false;
|
||||
// Canonical pages and page bounds imply first <= total (also for total 0).
|
||||
const unsigned remaining = total - first;
|
||||
if (count != (remaining < PER_PAGE ? remaining : PER_PAGE)) return false;
|
||||
const size_t canonical = pageSize(p);
|
||||
if (size != (flood_padding ? floodSize(canonical) : canonical)) return false;
|
||||
if (count != (remaining < per_page ? remaining : per_page)) return false;
|
||||
const size_t canonical = minimum + count * ENTRY;
|
||||
// Exactly one length in [canonical, canonical + 15] is 3 modulo 16.
|
||||
// Unsigned underflow also rejects lengths shorter than canonical.
|
||||
const size_t padding = size - canonical;
|
||||
if (padding > (flood_padding ? 15u : 0u) ||
|
||||
(flood_padding && (size & 15u) != 3)) return false;
|
||||
for (size_t i = canonical; i < size; ++i) if (p[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// MGR2/MGR3 public USB snapshot. The AES-SIV AAD authenticates every byte.
|
||||
inline void encodeUsbStatus(uint8_t* p, const UsbLoggingStatus& status) {
|
||||
const uint16_t flags = (status.supported ? 1u : 0u) |
|
||||
(status.logging_enabled ? 2u : 0u) | (status.watchdog_enabled ? 4u : 0u) |
|
||||
(status.host_connected ? 8u : 0u) | (status.reader_connected ? 16u : 0u) |
|
||||
(status.stalled ? 32u : 0u) | (status.recovering ? 64u : 0u) |
|
||||
(status.recovery_deferred ? 128u : 0u) | (status.persistence_ready ? 256u : 0u) |
|
||||
(status.watchdog_auto ? 512u : 0u) | (status.logger_active ? 1024u : 0u);
|
||||
write16(p, flags);
|
||||
p[2] = (status.stage & 3u) | ((status.backoff_step & 15u) << 2);
|
||||
write32(p + 3, status.retry_seconds); write32(p + 7, status.inactive_seconds);
|
||||
write32(p + 11, status.auto_connected_seconds);
|
||||
}
|
||||
|
||||
struct AclList {
|
||||
uint8_t entries[MAX_KEYS][ENTRY] = {};
|
||||
uint8_t count = 0;
|
||||
@@ -120,6 +171,8 @@ struct AclList {
|
||||
if (count == MAX_KEYS) return false;
|
||||
memcpy(entries[count], token, 12); entries[count++][12] = permissions; return true;
|
||||
}
|
||||
uint8_t pages() const { return count ? (count + PER_PAGE - 1) / PER_PAGE : 1; }
|
||||
uint8_t pages(size_t per_page = CURRENT_PER_PAGE) const {
|
||||
return count ? (count + per_page - 1) / per_page : 1;
|
||||
}
|
||||
};
|
||||
} }
|
||||
@@ -26,7 +26,7 @@ struct ManagementReporter::Working {
|
||||
History history;
|
||||
Extrema during_report;
|
||||
AclList acl;
|
||||
uint8_t header[HEADER] = {};
|
||||
uint8_t header[CURRENT_HEADER] = {};
|
||||
TransportKey scope;
|
||||
uint8_t route_path[MAX_PATH_SIZE] = {};
|
||||
uint8_t route_path_len = OUT_PATH_UNKNOWN;
|
||||
@@ -103,7 +103,10 @@ uint32_t ManagementReporter::jitter() const {
|
||||
}
|
||||
void ManagementReporter::snapshot() {
|
||||
auto& w = *work; memset(w.header, 0, sizeof(w.header)); w.acl = AclList();
|
||||
auto* p = w.header; memcpy(p, "MGR1", 4); memcpy(p + 4, mesh.self_id.pub_key, 16);
|
||||
auto* p = w.header; memcpy(p, "MGR3", 4); memcpy(p + 4, mesh.self_id.pub_key, 16);
|
||||
const UsbLoggingStatus usb = usbLoggingStatus();
|
||||
encodeUsbStatus(p + HEADER, usb);
|
||||
encodeUsbWatchdogEvent(p + WATCHDOG_EVENT_OFFSET, usb.last_event);
|
||||
write32(p + 20, sequence); write32(p + 24, mesh.getRTCClock()->getCurrentTime());
|
||||
const uint64_t hours = uptime / 3600; write16(p + 60, hours > 65535 ? 65535 : hours);
|
||||
w.history.week().encode(p + 62); w.history.period.encode(p + 66);
|
||||
@@ -229,15 +232,16 @@ void ManagementReporter::start(bool flood) {
|
||||
}
|
||||
void ManagementReporter::sendPage() {
|
||||
auto& w = *work;
|
||||
uint8_t packet[MAX_PAYLOAD], enc[32]; memcpy(packet, w.header, HEADER);
|
||||
const uint8_t first = w.page * PER_PAGE;
|
||||
const uint8_t count = w.acl.count - first < PER_PAGE ? w.acl.count - first : PER_PAGE;
|
||||
uint8_t packet[MAX_PAYLOAD], enc[32]; memcpy(packet, w.header, CURRENT_HEADER);
|
||||
const uint8_t first = w.page * CURRENT_PER_PAGE;
|
||||
const uint8_t count = w.acl.count - first < CURRENT_PER_PAGE ? w.acl.count - first : CURRENT_PER_PAGE;
|
||||
packet[78] = w.page; packet[79] = w.acl.pages(); packet[80] = w.acl.count;
|
||||
packet[81] = first; packet[82] = count;
|
||||
const size_t private_len = count * ENTRY, size = HEADER + private_len + TAG;
|
||||
memcpy(packet + HEADER, w.acl.entries + first, private_len);
|
||||
const size_t private_len = count * ENTRY, size = CURRENT_HEADER + private_len + TAG;
|
||||
memcpy(packet + CURRENT_HEADER, w.acl.entries + first, private_len);
|
||||
deriveKey(key, "MeshCore-MGR1-SIV", packet + 4, enc);
|
||||
seal(enc, packet, HEADER, packet + HEADER, private_len, packet + HEADER + private_len);
|
||||
seal(enc, packet, CURRENT_HEADER, packet + CURRENT_HEADER, private_len,
|
||||
packet + CURRENT_HEADER + private_len);
|
||||
erase(enc, sizeof(enc));
|
||||
Packet* pkt = mesh.createRawData(packet, size);
|
||||
if (!pkt) { w.page_in = 60; return; }
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "MQTTConnectionPolicy.h"
|
||||
#include "WifiPowerSavePolicy.h"
|
||||
#include "UsbLogging.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <climits>
|
||||
@@ -26,7 +27,7 @@ extern void tcpipInit();
|
||||
|
||||
// Same "MQTT: " prefix as MQTT_DEBUG_PRINTLN so existing log greps keep matching.
|
||||
#if defined(MQTT_DEBUG)
|
||||
#define NETWORK_DEBUG_PRINTLN(F, ...) do { if (Serial.availableForWrite() > 0) { Serial.printf("MQTT: " F "\n", ##__VA_ARGS__); } } while (0)
|
||||
#define NETWORK_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F "\n", ##__VA_ARGS__); } } while (0)
|
||||
#else
|
||||
#define NETWORK_DEBUG_PRINTLN(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
@@ -221,14 +221,17 @@ class BufferedNonBlockingWriteStream : public Stream {
|
||||
return written;
|
||||
}
|
||||
|
||||
void discardPending() {
|
||||
if (_busy.test_and_set(std::memory_order_acquire)) return;
|
||||
bool tryDiscardPending() {
|
||||
if (_busy.test_and_set(std::memory_order_acquire)) return false;
|
||||
_head = 0;
|
||||
_tail = 0;
|
||||
_count = 0;
|
||||
_busy.clear(std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
void discardPending() { (void)tryDiscardPending(); }
|
||||
|
||||
size_t queuedByteCount() {
|
||||
if (_busy.test_and_set(std::memory_order_acquire)) return CAPACITY;
|
||||
const size_t count = _count;
|
||||
|
||||
+302
-24
@@ -1,4 +1,5 @@
|
||||
#include "UsbLogging.h"
|
||||
#include "UsbLoggingClientActivity.h"
|
||||
|
||||
#if defined(ARDUINO)
|
||||
#include <Arduino.h>
|
||||
@@ -17,12 +18,15 @@
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
#include "esp32-hal-tinyusb.h"
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#endif
|
||||
#if MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
#include "UsbAsciiBinarySwitch.h"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(ENABLE_USB_INTERFACE) || defined(OTA_FOLDER_SERIAL))
|
||||
(defined(USE_TINYUSB) || defined(ENABLE_USB_INTERFACE) || defined(OTA_FOLDER_SERIAL))
|
||||
#define MESH_NRF52_PRIMARY_USB_NONBLOCKING 1
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#if !defined(CFG_TUD_CDC) || CFG_TUD_CDC < 1
|
||||
@@ -59,6 +63,23 @@ static std::atomic<bool> usb_logging_enabled{false};
|
||||
static std::atomic<bool> usb_logging_enabled{true};
|
||||
#endif
|
||||
static std::atomic<bool> usb_logging_preference_known{false};
|
||||
static std::atomic<bool> usb_debug_enabled{false};
|
||||
static std::atomic<uint32_t> usb_logging_tx_progress{0};
|
||||
static std::atomic<bool> usb_logging_tx_waiting{false};
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING \
|
||||
|| (defined(NRF52_PLATFORM) && defined(USE_TINYUSB))
|
||||
static bool usb_logging_watchdog_detached = false;
|
||||
static uint32_t usb_logging_watchdog_attach_at = 0;
|
||||
#endif
|
||||
|
||||
static void noteUsbLoggingTxAttempt() {
|
||||
if (isUsbLoggingEnabled()) usb_logging_tx_waiting.store(true, std::memory_order_release);
|
||||
}
|
||||
static void noteUsbLoggingTxComplete() {
|
||||
if (!isUsbLoggingEnabled()) return;
|
||||
usb_logging_tx_progress.fetch_add(1, std::memory_order_acq_rel);
|
||||
usb_logging_tx_waiting.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
class NullUsbLoggingStream : public Stream {
|
||||
public:
|
||||
@@ -100,12 +121,14 @@ static void handleEsp32TinyUsbEvent(void*, esp_event_base_t, int32_t event_id,
|
||||
void* event_data) {
|
||||
// This is the Arduino event task, not TinyUSB's owner. Only publish an
|
||||
// epoch: queue cleanup and endpoint access remain in application service.
|
||||
if (event_id == ARDUINO_USB_CDC_TX_EVENT) noteUsbLoggingTxComplete();
|
||||
bool closed = event_id == ARDUINO_USB_CDC_DISCONNECTED_EVENT;
|
||||
if (event_id == ARDUINO_USB_CDC_LINE_STATE_EVENT && event_data != nullptr) {
|
||||
const auto* event = static_cast<const arduino_usb_cdc_event_data_t*>(event_data);
|
||||
closed = !event->line_state.dtr;
|
||||
}
|
||||
if (closed) {
|
||||
clearUsbLoggingClientActivity();
|
||||
esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +153,7 @@ class Esp32TinyUsbFifoStream : public Stream {
|
||||
const size_t available = tud_cdc_n_write_available(0);
|
||||
const size_t attempt = size < available ? size : available;
|
||||
if (attempt == 0) return 0;
|
||||
noteUsbLoggingTxAttempt();
|
||||
const size_t written = tud_cdc_n_write(0, data, attempt);
|
||||
(void)tud_cdc_n_write_flush(0);
|
||||
return written;
|
||||
@@ -288,6 +312,7 @@ static UsbSelfResetBurstGuard esp32_hwcdc_self_reset_guard;
|
||||
// remain parked until a later request happens to kick TX. Keep requesting that
|
||||
// interrupt until the framework reports actual TX progress.
|
||||
static std::atomic<bool> esp32_hwcdc_tx_kick_pending{false};
|
||||
static std::atomic<bool> esp32_hwcdc_tx_primed{false};
|
||||
static portMUX_TYPE esp32_hwcdc_session_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static uint32_t esp32_hwcdc_taken_bus_reset_generation = 0;
|
||||
static bool esp32_hwcdc_event_handler_registered = false;
|
||||
@@ -307,6 +332,7 @@ static bool canAccessEsp32Hwcdc(void*) {
|
||||
static void handleEsp32HwcdcEvent(void*, esp_event_base_t, int32_t event_id,
|
||||
void*) {
|
||||
if (event_id == ARDUINO_HW_CDC_BUS_RESET_EVENT) {
|
||||
clearUsbLoggingClientActivity();
|
||||
// A host may issue several reset requests while enumerating the clean
|
||||
// transport. Suppress the complete burst; post-clean traffic below ends
|
||||
// the exemption before any later active-session reset can be ignored.
|
||||
@@ -321,6 +347,7 @@ static void handleEsp32HwcdcEvent(void*, esp_event_base_t, int32_t event_id,
|
||||
esp32_hwcdc_access_generation.fetch_add(
|
||||
1, std::memory_order_acq_rel);
|
||||
esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release);
|
||||
esp32_hwcdc_tx_primed.store(false, std::memory_order_release);
|
||||
usb_serial_jtag_ll_disable_intr_mask(
|
||||
USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
|
||||
portEXIT_CRITICAL(&esp32_hwcdc_session_mux);
|
||||
@@ -336,6 +363,12 @@ static void handleEsp32HwcdcEvent(void*, esp_event_base_t, int32_t event_id,
|
||||
// expected enumeration is over. A subsequent reset must not be ignored.
|
||||
esp32_hwcdc_self_reset_guard.notePostCleanActivity();
|
||||
}
|
||||
if (event_id == ARDUINO_HW_CDC_TX_EVENT
|
||||
&& esp32_hwcdc_tx_primed.exchange(true, std::memory_order_acq_rel)) {
|
||||
// The first empty interrupt only stages the first packet. A subsequent
|
||||
// one proves the host picked up the preceding IN transfer.
|
||||
noteUsbLoggingTxComplete();
|
||||
}
|
||||
}
|
||||
|
||||
class Esp32HwcdcSessionStream : public Stream {
|
||||
@@ -364,6 +397,7 @@ public:
|
||||
|
||||
size_t write(const uint8_t* data, size_t size) override {
|
||||
if (data == nullptr || size == 0) return 0;
|
||||
noteUsbLoggingTxAttempt();
|
||||
esp32_hwcdc_tx_kick_pending.store(true, std::memory_order_release);
|
||||
return Serial.write(data, size);
|
||||
}
|
||||
@@ -424,7 +458,7 @@ static void serviceEsp32HwcdcTxKick() {
|
||||
#endif
|
||||
|
||||
#if MESH_NRF52_PRIMARY_USB_NONBLOCKING
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
|
||||
// DTR or actual host input proves a CDC0 client. Stock MeshCLI deliberately
|
||||
// deasserts DTR, so it cannot be a prerequisite for receiving its first frame.
|
||||
// Access still requires the application to complete the current close epoch.
|
||||
@@ -433,6 +467,7 @@ static void serviceEsp32HwcdcTxKick() {
|
||||
// older epoch and can never reopen across a newer close.
|
||||
static std::atomic<uint32_t> primary_usb_reset_generation{0};
|
||||
static std::atomic<uint32_t> primary_usb_allowed_generation{0};
|
||||
static std::atomic<bool> primary_usb_terminal_discard_pending{false};
|
||||
static std::atomic<bool> primary_usb_line_state_dtr{false};
|
||||
static std::atomic<uint32_t> primary_usb_rx_generation{UINT32_MAX};
|
||||
static constexpr uint32_t primary_usb_session_settle_millis = 8;
|
||||
@@ -443,6 +478,7 @@ static uint32_t primaryUsbSessionGeneration() {
|
||||
}
|
||||
|
||||
static bool canAccessPrimaryUsbSession(void*) {
|
||||
if (primary_usb_terminal_discard_pending.load(std::memory_order_acquire)) return false;
|
||||
const uint32_t generation = primaryUsbSessionGeneration();
|
||||
if (!tud_mounted()
|
||||
|| primary_usb_allowed_generation.load(std::memory_order_acquire)
|
||||
@@ -477,6 +513,9 @@ static void endPrimaryUsbHostSession(bool clear_cdc_fifos) {
|
||||
millis() + primary_usb_session_settle_millis,
|
||||
std::memory_order_release);
|
||||
primary_usb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
#if !defined(MESH_DUAL_CDC_LOGGING)
|
||||
clearUsbLoggingClientActivity();
|
||||
#endif
|
||||
if (clear_cdc_fifos) {
|
||||
// The line-state callback runs before TinyUSB resets the device. A device
|
||||
// unmount/remount boundary has already reset these class FIFOs.
|
||||
@@ -497,7 +536,7 @@ static size_t writeTinyUsbCdcOnce(void* context, const uint8_t* data,
|
||||
if (data == nullptr || size == 0 || instance >= CFG_TUD_CDC) {
|
||||
return 0;
|
||||
}
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
|
||||
if (instance == 0) {
|
||||
if (!canAccessPrimaryUsbSession(nullptr)) return 0;
|
||||
} else
|
||||
@@ -509,12 +548,17 @@ static size_t writeTinyUsbCdcOnce(void* context, const uint8_t* data,
|
||||
const size_t available = tud_cdc_n_write_available(instance);
|
||||
const size_t attempt = size < available ? size : available;
|
||||
if (attempt == 0) return 0;
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
if (instance == 1) noteUsbLoggingTxAttempt();
|
||||
#else
|
||||
if (instance == 0) noteUsbLoggingTxAttempt();
|
||||
#endif
|
||||
return tud_cdc_n_write(instance, data, attempt);
|
||||
}
|
||||
|
||||
static SingleAttemptNonBlockingStream nonblocking_primary_usb_companion_port(
|
||||
Serial, writeTinyUsbCdcOnce, reinterpret_cast<void*>(uintptr_t{0})
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
|
||||
, canAccessPrimaryUsbSession
|
||||
#endif
|
||||
);
|
||||
@@ -524,7 +568,7 @@ static SingleAttemptNonBlockingStream nonblocking_primary_usb_companion_port(
|
||||
static AtomicWholeRecordNonBlockingStream<11>
|
||||
nonblocking_primary_usb_mota_port(
|
||||
nonblocking_primary_usb_companion_port);
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB)
|
||||
// Unlike Binary Companion frames, a terminal reply is produced as many Print
|
||||
// calls whose return values are not consumed by the CLI. Retain one complete
|
||||
// multi-line response while a normal host drains CDC0, but bound the memory and
|
||||
@@ -534,6 +578,30 @@ static BufferedNonBlockingWriteStream<4096>
|
||||
nonblocking_primary_usb_companion_port);
|
||||
static uint32_t primary_usb_terminal_seen_reset_generation = 0;
|
||||
static uint32_t primary_usb_terminal_taken_reset_generation = 0;
|
||||
|
||||
// Functional replies and diagnostics on one CDC need one chronological queue
|
||||
// so their unwritten suffixes cannot overtake each other. Diagnostics leave
|
||||
// most capacity available for stats/configuration replies.
|
||||
static constexpr size_t nrf52_usb_functional_reserve = 3072;
|
||||
class Nrf52BufferedLoggingStream : public Stream {
|
||||
public:
|
||||
int available() override { return 0; }
|
||||
int read() override { return -1; }
|
||||
int peek() override { return -1; }
|
||||
void flush() override { serviceUsbTerminalPort(); }
|
||||
int availableForWrite() override {
|
||||
const int free = buffered_primary_usb_terminal_port.availableForWrite();
|
||||
return free > static_cast<int>(nrf52_usb_functional_reserve)
|
||||
? free - nrf52_usb_functional_reserve : 0;
|
||||
}
|
||||
size_t write(uint8_t value) override { return write(&value, 1); }
|
||||
size_t write(const uint8_t* data, size_t size) override {
|
||||
if (!data || !size || size > 4096 || !canAccessPrimaryUsbSession(nullptr)
|
||||
|| availableForWrite() < static_cast<int>(size)) return 0;
|
||||
return buffered_primary_usb_terminal_port.write(data, size);
|
||||
}
|
||||
};
|
||||
static Nrf52BufferedLoggingStream buffered_primary_usb_logging_port;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -567,6 +635,7 @@ static void setPlatformDebugOutputEnabled(bool enabled) {
|
||||
static bool dedicated_usb_logging_port_configured = false;
|
||||
static bool dedicated_usb_logging_port_started = false;
|
||||
static std::atomic<bool> dedicated_usb_logging_port_connected{false};
|
||||
static std::atomic<bool> dedicated_usb_logging_watchdog_reset_requested{false};
|
||||
static bool dedicated_usb_logging_sof_enabled = false;
|
||||
|
||||
static constexpr char dedicated_usb_logging_descriptor[] =
|
||||
@@ -622,6 +691,7 @@ static size_t dedicated_usb_logging_identity_offset = 0;
|
||||
// discard the identity marker and expose a later diagnostic as byte zero.
|
||||
static constexpr uint8_t dedicated_usb_logging_host_settle_sofs = 50;
|
||||
static uint8_t dedicated_usb_logging_quiet_sofs = 0;
|
||||
static UsbLoggingStatsLineParser dedicated_usb_logging_stats_parser;
|
||||
|
||||
static void resetDedicatedUsbLoggingIdentity() {
|
||||
dedicated_usb_logging_identity_offset = 0;
|
||||
@@ -634,6 +704,9 @@ static void resetDedicatedUsbLoggingUsbTaskState() {
|
||||
}
|
||||
|
||||
static void restartDedicatedUsbLoggingHostSession() {
|
||||
clearUsbLoggingClientActivity();
|
||||
dedicated_usb_logging_stats_parser.reset();
|
||||
tud_cdc_n_read_flush(1);
|
||||
// This callback and the SOF drain both run in TinyUSB's task. Gate producers
|
||||
// before clearing both queues, then publish a generation so the next SOF
|
||||
// repeats the reset after any producer which was already in flight. Clearing
|
||||
@@ -682,6 +755,10 @@ static void handleDedicatedUsbLoggingLineCoding() {
|
||||
// 1 kHz also catches a close/reopen that occurs entirely between main-loop
|
||||
// service calls.
|
||||
void serviceDedicatedUsbLoggingFromUsbTask() {
|
||||
if (dedicated_usb_logging_watchdog_reset_requested.exchange(false,
|
||||
std::memory_order_acq_rel)) {
|
||||
restartDedicatedUsbLoggingHostSession();
|
||||
}
|
||||
const uint32_t reset_generation =
|
||||
dedicated_usb_logging_reset_generation.load(std::memory_order_acquire);
|
||||
if (reset_generation != dedicated_usb_logging_seen_reset_generation) {
|
||||
@@ -699,6 +776,9 @@ void serviceDedicatedUsbLoggingFromUsbTask() {
|
||||
false, std::memory_order_release);
|
||||
}
|
||||
if (!connected) {
|
||||
if (dedicated_usb_logging_usb_task_connected)
|
||||
clearUsbLoggingClientActivity();
|
||||
dedicated_usb_logging_stats_parser.reset();
|
||||
dedicated_usb_logging_port_connected.store(
|
||||
false, std::memory_order_release);
|
||||
resetDedicatedUsbLoggingUsbTaskState();
|
||||
@@ -719,6 +799,16 @@ void serviceDedicatedUsbLoggingFromUsbTask() {
|
||||
dedicated_usb_logging_port_connected.store(
|
||||
true, std::memory_order_release);
|
||||
|
||||
// CDC1 is still logging-only, not a second configuration console. Consume
|
||||
// bounded stats polling input solely to distinguish an application from
|
||||
// a plugged cable/open port. Endpoint access stays in TinyUSB's owner.
|
||||
for (size_t budget = 0; budget < 64 && tud_cdc_n_available(1); ++budget) {
|
||||
uint8_t value;
|
||||
if (tud_cdc_n_read(1, &value, 1) != 1) break;
|
||||
if (dedicated_usb_logging_stats_parser.consume(static_cast<char>(value)))
|
||||
noteUsbLoggingStatsCommand("stats-core");
|
||||
}
|
||||
|
||||
if (!dedicated_usb_logging_usb_task_connected) {
|
||||
// Drop diagnostics from the prior host before sending byte zero of the
|
||||
// marker to the new host.
|
||||
@@ -762,7 +852,11 @@ void serviceDedicatedUsbLoggingFromUsbTask() {
|
||||
// Single-CDC nRF52 roles need the same protection. In particular, BLE debug
|
||||
// callbacks and packet logging write through usbLoggingPort() without going
|
||||
// through MeshCore's formatted-debug helper.
|
||||
#if MESH_NRF52_PRIMARY_USB_NONBLOCKING
|
||||
#if MESH_NRF52_PRIMARY_USB_NONBLOCKING && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
static WholeRecordNonBlockingStream<640>
|
||||
nonblocking_primary_usb_logging_port(
|
||||
buffered_primary_usb_logging_port);
|
||||
#elif MESH_NRF52_PRIMARY_USB_NONBLOCKING
|
||||
static AtomicWholeRecordNonBlockingStream<>
|
||||
nonblocking_primary_usb_logging_port(
|
||||
nonblocking_primary_usb_companion_port);
|
||||
@@ -776,13 +870,26 @@ bool isUsbLoggingEnabled() {
|
||||
return usb_logging_enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool isUsbDebugEnabled() {
|
||||
return usb_debug_enabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void setUsbDebugEnabled(bool enabled) {
|
||||
usb_debug_enabled.store(enabled, std::memory_order_relaxed);
|
||||
setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled());
|
||||
}
|
||||
|
||||
void setUsbLoggingEnabled(bool enabled) {
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
const bool was_enabled = isUsbLoggingEnabled();
|
||||
#endif
|
||||
usb_logging_enabled.store(enabled, std::memory_order_relaxed);
|
||||
if (!enabled) {
|
||||
clearUsbLoggingClientActivity();
|
||||
usb_logging_tx_waiting.store(false, std::memory_order_release);
|
||||
}
|
||||
usb_logging_preference_known.store(true, std::memory_order_relaxed);
|
||||
setPlatformDebugOutputEnabled(enabled);
|
||||
setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled());
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
// Text shares one chronological queue. When leaving logging mode, discard
|
||||
// its residual application bytes before later Binary/mOTA traffic can start.
|
||||
@@ -825,8 +932,8 @@ void beginUsbLoggingPort() {
|
||||
// setup() calls this once before role preferences are loaded and again
|
||||
// afterwards. The first call silences framework diagnostics on a protected
|
||||
// ESP32 Companion stream; setUsbLoggingEnabled() restores them only when the
|
||||
// saved setting explicitly enables logging.
|
||||
setPlatformDebugOutputEnabled(isUsbLoggingEnabled());
|
||||
// saved settings explicitly enable both USB output and debug verbosity.
|
||||
setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled());
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
if (!esp32_tinyusb_event_handler_registered) {
|
||||
Serial.onEvent(ARDUINO_USB_CDC_ANY_EVENT, handleEsp32TinyUsbEvent);
|
||||
@@ -868,6 +975,16 @@ void beginUsbLoggingPort() {
|
||||
}
|
||||
|
||||
void serviceUsbLoggingPort() {
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING \
|
||||
|| (defined(NRF52_PLATFORM) && defined(USE_TINYUSB))
|
||||
// A timed reconnect retains all descriptors and never blocks the radio loop
|
||||
// in delay(), Serial.end(), or a host-progress wait.
|
||||
if (usb_logging_watchdog_detached
|
||||
&& int32_t(millis() - usb_logging_watchdog_attach_at) >= 0) {
|
||||
(void)tud_connect();
|
||||
usb_logging_watchdog_detached = false;
|
||||
}
|
||||
#endif
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
serviceEsp32TinyUsbPorts();
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
@@ -912,6 +1029,9 @@ void serviceUsbLoggingPort() {
|
||||
dedicated_usb_logging_sof_enabled = should_service;
|
||||
}
|
||||
#endif
|
||||
#if MESH_NRF52_USB_CONSOLE_COOPERATIVE && !defined(ENABLE_USB_INTERFACE)
|
||||
serviceUsbTerminalPort();
|
||||
#endif
|
||||
}
|
||||
|
||||
struct Esp32HwcdcPurgeResult {
|
||||
@@ -989,6 +1109,7 @@ bool resetUsbCompanionTransport() {
|
||||
esp32_hwcdc_access_generation.fetch_add(
|
||||
1, std::memory_order_acq_rel) + 1U;
|
||||
esp32_hwcdc_tx_kick_pending.store(false, std::memory_order_release);
|
||||
esp32_hwcdc_tx_primed.store(false, std::memory_order_release);
|
||||
usb_serial_jtag_ll_disable_intr_mask(
|
||||
USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
|
||||
portEXIT_CRITICAL(&esp32_hwcdc_session_mux);
|
||||
@@ -1037,7 +1158,7 @@ bool resetUsbCompanionTransport() {
|
||||
esp32_hwcdc_cleanup_pending = false;
|
||||
// A concurrent runtime preference change is authoritative; never restore a
|
||||
// stale snapshot taken before the purge.
|
||||
setPlatformDebugOutputEnabled(isUsbLoggingEnabled());
|
||||
setPlatformDebugOutputEnabled(isUsbDebugLoggingEnabled());
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -1063,6 +1184,11 @@ Stream& usbLoggingPort() {
|
||||
#endif
|
||||
}
|
||||
|
||||
Stream& usbDebugPort() {
|
||||
if (!isUsbDebugLoggingEnabled()) return null_usb_logging_stream;
|
||||
return usbLoggingPort();
|
||||
}
|
||||
|
||||
Stream& usbCompanionPort() {
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
return nonblocking_esp32_tinyusb_port;
|
||||
@@ -1075,7 +1201,7 @@ Stream& usbCompanionPort() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
bool isUsbCompanionClientConnected() {
|
||||
return canAccessPrimaryUsbSession(nullptr);
|
||||
}
|
||||
@@ -1097,7 +1223,7 @@ Stream& usbTerminalPort(bool enabled) {
|
||||
if (!enabled) return null_usb_logging_stream;
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
return buffered_esp32_tinyusb_terminal_port;
|
||||
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
return buffered_primary_usb_terminal_port;
|
||||
#else
|
||||
return usbCompanionPort();
|
||||
@@ -1109,6 +1235,8 @@ Stream& usbConsolePort() {
|
||||
return buffered_esp32_tinyusb_terminal_port;
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
return guarded_esp32_hwcdc_port;
|
||||
#elif MESH_NRF52_USB_CONSOLE_COOPERATIVE
|
||||
return buffered_primary_usb_terminal_port;
|
||||
#else
|
||||
return Serial;
|
||||
#endif
|
||||
@@ -1122,6 +1250,9 @@ bool canAcceptUsbConsoleCommand() {
|
||||
// Leave enough room for a complete ordinary CLI response. Larger listings
|
||||
// already retain and retry a short write from their cooperative pump.
|
||||
return guarded_esp32_hwcdc_port.availableForWrite() >= 256;
|
||||
#elif MESH_NRF52_USB_CONSOLE_COOPERATIVE
|
||||
return buffered_primary_usb_terminal_port.availableForWrite()
|
||||
>= static_cast<int>(nrf52_usb_functional_reserve);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
@@ -1130,13 +1261,18 @@ bool canAcceptUsbConsoleCommand() {
|
||||
void serviceUsbTerminalPort() {
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
serviceEsp32TinyUsbPorts();
|
||||
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
const uint32_t reset_generation =
|
||||
primaryUsbSessionGeneration();
|
||||
if (reset_generation != primary_usb_terminal_seen_reset_generation) {
|
||||
buffered_primary_usb_terminal_port.discardPending();
|
||||
if (!buffered_primary_usb_terminal_port.tryDiscardPending()) return;
|
||||
primary_usb_terminal_seen_reset_generation = reset_generation;
|
||||
}
|
||||
if (primary_usb_terminal_discard_pending.exchange(false, std::memory_order_acq_rel)
|
||||
&& !buffered_primary_usb_terminal_port.tryDiscardPending()) {
|
||||
primary_usb_terminal_discard_pending.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
buffered_primary_usb_terminal_port.service();
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
serviceEsp32HwcdcTxKick();
|
||||
@@ -1152,8 +1288,9 @@ void discardUsbTerminalOutput() {
|
||||
esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed);
|
||||
esp32_tinyusb_terminal_discard_pending.store(false, std::memory_order_release);
|
||||
esp32_tinyusb_queue_busy.clear(std::memory_order_release);
|
||||
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
buffered_primary_usb_terminal_port.discardPending();
|
||||
#elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
primary_usb_terminal_discard_pending.store(true, std::memory_order_release);
|
||||
serviceUsbTerminalPort();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1163,8 +1300,9 @@ bool hasPendingUsbTerminalOutput() {
|
||||
const bool pending = esp32_tinyusb_text_queue.queuedByteCount() != 0;
|
||||
esp32_tinyusb_queue_busy.clear(std::memory_order_release);
|
||||
return pending;
|
||||
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
return buffered_primary_usb_terminal_port.queuedByteCount() != 0;
|
||||
#elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
return primary_usb_terminal_discard_pending.load(std::memory_order_acquire)
|
||||
|| buffered_primary_usb_terminal_port.queuedByteCount() != 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
@@ -1185,7 +1323,7 @@ bool takeUsbTerminalSessionReset() {
|
||||
if (reset_generation == esp32_tinyusb_taken_reset_generation) return false;
|
||||
esp32_tinyusb_taken_reset_generation = reset_generation;
|
||||
return true;
|
||||
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
const uint32_t reset_generation =
|
||||
primaryUsbSessionGeneration();
|
||||
if (reset_generation == primary_usb_terminal_taken_reset_generation) {
|
||||
@@ -1206,7 +1344,7 @@ bool takeUsbTerminalSessionReset() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
static void completePrimaryUsbSessionReset(void*) {
|
||||
// This runs under the primary SingleAttempt stream's producer gate. It
|
||||
// removes a write that raced the owner-task close callback before allowing
|
||||
@@ -1227,12 +1365,22 @@ bool tryCompleteUsbTerminalSessionReset() {
|
||||
serviceEsp32TinyUsbPorts();
|
||||
return esp32_tinyusb_clean_generation.load(std::memory_order_acquire)
|
||||
== esp32_tinyusb_reset_generation.load(std::memory_order_acquire);
|
||||
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#elif defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
serviceUsbTerminalPort();
|
||||
const uint32_t generation = primaryUsbSessionGeneration();
|
||||
if (primary_usb_terminal_discard_pending.load(std::memory_order_acquire)
|
||||
|| primary_usb_terminal_seen_reset_generation != generation) return false;
|
||||
// Ordinary roles call this every loop, not only on a reset. Never purge
|
||||
// an already-open endpoint: that would drop every pending reply prefix.
|
||||
if (primary_usb_allowed_generation.load(std::memory_order_acquire) == generation)
|
||||
return true;
|
||||
const uint32_t settle_until =
|
||||
primary_usb_reset_settle_until.load(std::memory_order_acquire);
|
||||
if ((int32_t)(millis() - settle_until) < 0) return false;
|
||||
return nonblocking_primary_usb_companion_port.tryRunExclusive(
|
||||
completePrimaryUsbSessionReset);
|
||||
if (!nonblocking_primary_usb_companion_port.tryRunExclusive(
|
||||
completePrimaryUsbSessionReset)) return false;
|
||||
return primary_usb_allowed_generation.load(std::memory_order_acquire)
|
||||
== primaryUsbSessionGeneration();
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
if (!esp32_hwcdc_cleanup_pending
|
||||
&& esp32_hwcdc_allowed_generation.load(std::memory_order_acquire)
|
||||
@@ -1277,9 +1425,138 @@ const char* usbLoggingPortDescription() {
|
||||
#endif
|
||||
}
|
||||
|
||||
UsbLoggingObservation observeUsbLoggingTransport() {
|
||||
UsbLoggingObservation result;
|
||||
result.tx_progress = usb_logging_tx_progress.load(std::memory_order_acquire);
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
result.supported = true;
|
||||
if (xPortInIsrContext()) return result;
|
||||
result.host_connected = tud_mounted();
|
||||
result.reader_connected = tud_cdc_n_connected(0);
|
||||
result.pending = hasPendingUsbTerminalOutput()
|
||||
|| usb_logging_tx_waiting.load(std::memory_order_acquire);
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
result.supported = true;
|
||||
if (xPortInIsrContext()) return result;
|
||||
result.host_connected = Serial.isPlugged();
|
||||
result.reader_connected = result.host_connected && bool(Serial);
|
||||
const size_t capacity = esp32_hwcdc_tx_buffer_capacity.load(std::memory_order_acquire);
|
||||
result.pending = usb_logging_tx_waiting.load(std::memory_order_acquire)
|
||||
|| (capacity != 0 && Serial.availableForWrite() < int(capacity));
|
||||
#elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
result.supported = true;
|
||||
const uint8_t instance = hasDedicatedUsbLoggingPort() ? 1 : 0;
|
||||
result.host_connected = tud_mounted();
|
||||
result.reader_connected = tud_cdc_n_connected(instance);
|
||||
result.pending = usb_logging_tx_waiting.load(std::memory_order_acquire)
|
||||
|| tud_cdc_n_write_available(instance) < CFG_TUD_CDC_TX_BUFSIZE;
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
result.pending = result.pending || usb_task_dedicated_usb_logging_port.queuedRecordCount() != 0;
|
||||
#else
|
||||
result.pending = result.pending || hasPendingUsbTerminalOutput();
|
||||
#endif
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
void probeUsbLoggingTransport() {
|
||||
if (!isUsbLoggingEnabled()) return;
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING || MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
if (xPortInIsrContext()) return;
|
||||
#endif
|
||||
static const uint8_t record[] = "[USB watchdog] heartbeat\r\n";
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING || MESH_ESP32_HWCDC_SESSION_GUARD \
|
||||
|| defined(MESH_DUAL_CDC_LOGGING) || MESH_NRF52_USB_CONSOLE_COOPERATIVE
|
||||
Stream& port = usbLoggingPort();
|
||||
if (port.availableForWrite() >= int(sizeof(record) - 1))
|
||||
(void)port.write(record, sizeof(record) - 1);
|
||||
#elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
// Ordinary nRF52 roles may still use the framework Serial facade. Never
|
||||
// call its retrying write path from the watchdog: use one native attempt.
|
||||
if (tud_cdc_n_connected(0)
|
||||
&& tud_cdc_n_write_available(0) >= sizeof(record) - 1) {
|
||||
noteUsbLoggingTxAttempt();
|
||||
(void)tud_cdc_n_write(0, record, sizeof(record) - 1);
|
||||
(void)tud_cdc_n_write_flush(0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
UsbLoggingRecoveryResult recoverUsbLoggingTransport(uint8_t stage) {
|
||||
if (!isUsbLoggingEnabled() || (stage != 1 && stage != 2))
|
||||
return UsbLoggingRecoveryResult::Unsupported;
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING
|
||||
if (xPortInIsrContext()) return UsbLoggingRecoveryResult::Deferred;
|
||||
if (stage == 1) {
|
||||
if (!resetUsbCompanionTransport()) return UsbLoggingRecoveryResult::Deferred;
|
||||
probeUsbLoggingTransport();
|
||||
} else {
|
||||
if (!usb_logging_watchdog_detached) {
|
||||
if (!tud_disconnect()) return UsbLoggingRecoveryResult::Deferred;
|
||||
usb_logging_watchdog_attach_at = millis() + 50;
|
||||
usb_logging_watchdog_detached = true;
|
||||
}
|
||||
}
|
||||
return UsbLoggingRecoveryResult::Attempted;
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
if (xPortInIsrContext()) return UsbLoggingRecoveryResult::Deferred;
|
||||
if (stage == 1) {
|
||||
// First just re-kick the existing ISR. Purge/PHY reconnection is the next
|
||||
// stage, and the bounded existing helper retains the driver/descriptors.
|
||||
serviceEsp32HwcdcTxKick();
|
||||
probeUsbLoggingTransport();
|
||||
} else if (!resetUsbCompanionTransport()) {
|
||||
return UsbLoggingRecoveryResult::Deferred;
|
||||
}
|
||||
return UsbLoggingRecoveryResult::Attempted;
|
||||
#elif defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
if (stage == 1) {
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
// Only publish a request. FIFO/identity/queue mutation remains in CDC1's
|
||||
// existing TinyUSB-owner SOF service, never in this application call.
|
||||
dedicated_usb_logging_watchdog_reset_requested.store(true, std::memory_order_release);
|
||||
dedicated_usb_logging_port_connected.store(false, std::memory_order_release);
|
||||
if (tud_cdc_n_connected(1)) tud_sof_cb_enable(true);
|
||||
#else
|
||||
endPrimaryUsbHostSession(false);
|
||||
(void)tud_cdc_n_write_clear(0);
|
||||
probeUsbLoggingTransport();
|
||||
#endif
|
||||
} else if (!usb_logging_watchdog_detached) {
|
||||
if (!tud_disconnect()) return UsbLoggingRecoveryResult::Deferred;
|
||||
usb_logging_watchdog_attach_at = millis() + 50;
|
||||
usb_logging_watchdog_detached = true;
|
||||
}
|
||||
return UsbLoggingRecoveryResult::Attempted;
|
||||
#else
|
||||
return UsbLoggingRecoveryResult::Unsupported;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool isUsbLoggingTransportRecoveryPending() {
|
||||
#if MESH_ESP32_TINYUSB_NONBLOCKING \
|
||||
|| (defined(NRF52_PLATFORM) && defined(USE_TINYUSB))
|
||||
return usb_logging_watchdog_detached;
|
||||
#elif MESH_ESP32_HWCDC_SESSION_GUARD
|
||||
return esp32_hwcdc_cleanup_pending;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
extern "C" void meshTinyUsbLoggingTxComplete(uint8_t instance) {
|
||||
#if defined(MESH_DUAL_CDC_LOGGING)
|
||||
if (instance == 1) mesh::noteUsbLoggingTxComplete();
|
||||
#else
|
||||
if (instance == 0) mesh::noteUsbLoggingTxComplete();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
// A protocol-only, weak-declaration-free translation unit supplies strong
|
||||
// versions of TinyUSB's callbacks and forwards here. Preserve Adafruit's CDC0
|
||||
// 1200-baud touch behavior exactly, capture every CDC0 host-session close and
|
||||
@@ -1365,4 +1642,5 @@ extern "C" void meshTinyUsbStartOfFrame(uint32_t frame_count) {
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "UsbLoggingStatus.h"
|
||||
|
||||
// Canonical USB-capable images compile at least one of these two diagnostics.
|
||||
// One runtime gate covers every diagnostic category that writes to the USB
|
||||
// Serial stream, so a separate logging firmware is unnecessary.
|
||||
// A master USB output gate and a separate debug-verbosity gate keep packet
|
||||
// capture available without verbose diagnostics or a separate logging image.
|
||||
#if !defined(MESH_USB_LOGGING_DISABLED) && defined(ARDUINO) && \
|
||||
((defined(MESH_DEBUG) && MESH_DEBUG) || \
|
||||
(defined(MESH_PACKET_LOGGING) && MESH_PACKET_LOGGING))
|
||||
@@ -41,6 +43,14 @@
|
||||
#define MESH_ESP32_USB_CONSOLE_COOPERATIVE 0
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
#define MESH_NRF52_USB_CONSOLE_COOPERATIVE 1
|
||||
#else
|
||||
#define MESH_NRF52_USB_CONSOLE_COOPERATIVE 0
|
||||
#endif
|
||||
#define MESH_USB_CONSOLE_COOPERATIVE \
|
||||
(MESH_ESP32_USB_CONSOLE_COOPERATIVE || MESH_NRF52_USB_CONSOLE_COOPERATIVE)
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#ifndef MESH_ESP32_USB_TX_BUFFER_SIZE
|
||||
@@ -65,6 +75,13 @@ inline size_t nextUsbLoggingIdentityOffset(size_t current_offset,
|
||||
// protect framed traffic and restores its saved choice after preferences load.
|
||||
bool isUsbLoggingEnabled();
|
||||
void setUsbLoggingEnabled(bool enabled);
|
||||
// Verbose diagnostics are opt-in and persisted by the role preferences.
|
||||
// The requested setting survives switching the master USB output gate off.
|
||||
bool isUsbDebugEnabled();
|
||||
void setUsbDebugEnabled(bool enabled);
|
||||
inline bool isUsbDebugLoggingEnabled() {
|
||||
return isUsbLoggingEnabled() && isUsbDebugEnabled();
|
||||
}
|
||||
|
||||
// Kept as a platform-neutral persistence hook. Single-TTY builds need no
|
||||
// descriptor mirror; nRF52 selects its optional second interface after loading
|
||||
@@ -91,6 +108,9 @@ void beginUsbLoggingPort();
|
||||
// such as /dev/ttyACM1 or COM7.
|
||||
void serviceUsbLoggingPort();
|
||||
Stream& usbLoggingPort();
|
||||
// Debug-only output uses the same bounded transport when BOTH gates are on.
|
||||
// Packet records, identity markers and functional replies must not use this.
|
||||
Stream& usbDebugPort();
|
||||
// Primary USB Companion data stream. Native TinyUSB on nRF52 and ESP32 uses
|
||||
// one FIFO attempt per write, never the framework's wait-for-space loop.
|
||||
// Callers retain and retry unwritten suffixes. Other platforms retain Serial
|
||||
@@ -99,7 +119,7 @@ Stream& usbCompanionPort();
|
||||
// A primary nRF52 client may send commands with DTR low (stock MeshCLI does).
|
||||
// Accept actual input in the current USB session as proof of a reader, while
|
||||
// retaining the same close/reset generation gate used by the transport.
|
||||
#if defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
|
||||
#if defined(NRF52_PLATFORM) && (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
bool isUsbCompanionClientConnected();
|
||||
#endif
|
||||
// Serial mOTA requests are binary records of at most 11 bytes. On TinyUSB this
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "UsbLoggingClientActivity.h"
|
||||
|
||||
#if defined(ARDUINO)
|
||||
#include <Arduino.h>
|
||||
#include "UsbLogging.h"
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#include <atomic>
|
||||
|
||||
namespace mesh {
|
||||
namespace {
|
||||
std::atomic<uint32_t> session_generation{1};
|
||||
std::atomic<uint32_t> stats_generation{0};
|
||||
std::atomic<uint32_t> stats_deadline{0};
|
||||
}
|
||||
|
||||
void noteUsbLoggingStatsCommand(const char* command) {
|
||||
if (!isUsbLoggingStatsCommand(command)) return;
|
||||
const uint32_t generation = session_generation.load(std::memory_order_acquire);
|
||||
uint32_t deadline = millis() + USB_LOGGING_CLIENT_LEASE_MS;
|
||||
if (!deadline) deadline = UINT32_MAX; // Reserve zero; at most 1 ms early.
|
||||
stats_deadline.store(deadline, std::memory_order_relaxed);
|
||||
stats_generation.store(generation, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool isUsbLoggingClientActive() {
|
||||
const uint32_t generation = stats_generation.load(std::memory_order_acquire);
|
||||
if (!generation || generation != session_generation.load(std::memory_order_acquire))
|
||||
return false;
|
||||
uint32_t deadline = stats_deadline.load(std::memory_order_relaxed);
|
||||
if (!deadline) return false;
|
||||
if (int32_t(deadline - millis()) > 0) return true;
|
||||
// Latch expiry, so millis wrapping weeks later cannot revive an old poll.
|
||||
// Compare the timestamp, not just the session: an owner-task renewal that
|
||||
// raced this expiry must retain its new deadline.
|
||||
stats_deadline.compare_exchange_strong(deadline, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
void clearUsbLoggingClientActivity() {
|
||||
// A callback racing a poll invalidates that poll's old generation without
|
||||
// blocking either the USB owner task or the application loop.
|
||||
session_generation.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
#else
|
||||
namespace mesh {
|
||||
void noteUsbLoggingStatsCommand(const char*) {}
|
||||
bool isUsbLoggingClientActive() { return false; }
|
||||
void clearUsbLoggingClientActivity() {}
|
||||
} // namespace mesh
|
||||
#endif // MESH_USB_CONSOLE_COOPERATIVE
|
||||
#endif // ARDUINO
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// meshcoretomqtt polls these commands every five minutes. A short-lived lease
|
||||
// proves application input, unlike a cable, DTR, or a kernel USB ACK. It does
|
||||
// not identify a particular application or prove MQTT/disk health.
|
||||
constexpr uint32_t USB_LOGGING_CLIENT_LEASE_MS = 15UL * 60UL * 1000UL;
|
||||
|
||||
inline bool isUsbLoggingStatsCommand(const char* command) {
|
||||
if (!command) return false;
|
||||
while (*command == ' ' || *command == '\t') ++command;
|
||||
const char* const keys[] = {"stats-core", "stats-radio", "stats-packets"};
|
||||
for (const char* key : keys) {
|
||||
const size_t length = strlen(key);
|
||||
if (strncmp(command, key, length)) continue;
|
||||
const char* end = command + length;
|
||||
while (*end == ' ' || *end == '\t') ++end;
|
||||
if (!*end) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bounded receive-only parser for the dedicated logging CDC. It recognizes
|
||||
// stats polls but does not expose a second configuration or binary CLI. A
|
||||
// truncated/overlong record must never count as a valid poll.
|
||||
class UsbLoggingStatsLineParser {
|
||||
public:
|
||||
void reset() { length = 0; discard = false; }
|
||||
bool consume(char c) {
|
||||
if (c == '\r' || c == '\n') {
|
||||
line[length] = 0;
|
||||
const bool matched = !discard && isUsbLoggingStatsCommand(line);
|
||||
reset();
|
||||
return matched;
|
||||
}
|
||||
if (discard) return false;
|
||||
if ((static_cast<uint8_t>(c) < 32 && c != '\t')
|
||||
|| static_cast<uint8_t>(c) >= 127) {
|
||||
discard = true;
|
||||
length = 0;
|
||||
return false;
|
||||
}
|
||||
if (length == sizeof(line) - 1) {
|
||||
discard = true;
|
||||
length = 0;
|
||||
} else {
|
||||
line[length++] = c;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
char line[32] = {};
|
||||
size_t length = 0;
|
||||
bool discard = false;
|
||||
};
|
||||
|
||||
// Call only after receiving a complete command on the actual USB logging
|
||||
// endpoint. BLE, TCP, LoRa, and the primary CDC of a dual-CDC device cannot
|
||||
// establish a logging-reader lease. These calls never persist preferences.
|
||||
void noteUsbLoggingStatsCommand(const char* command);
|
||||
bool isUsbLoggingClientActive();
|
||||
void clearUsbLoggingClientActivity();
|
||||
|
||||
} // namespace mesh
|
||||
@@ -4,8 +4,17 @@
|
||||
// strong callbacks which override Adafruit's weak defaults for nRF52 USB
|
||||
// Companion builds. CDC0 needs exact close events even on single-CDC targets;
|
||||
// dual-CDC builds additionally forward CDC1 reconnect events.
|
||||
#if defined(ARDUINO) && defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
|
||||
#include <stdint.h>
|
||||
extern "C" void meshTinyUsbLoggingTxComplete(uint8_t instance);
|
||||
// No weak callback declaration is imported in this translation unit.
|
||||
extern "C" void tud_cdc_tx_complete_cb(uint8_t instance) {
|
||||
meshTinyUsbLoggingTxComplete(instance);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ARDUINO) && defined(NRF52_PLATFORM) \
|
||||
&& defined(ENABLE_USB_INTERFACE)
|
||||
&& (defined(ENABLE_USB_INTERFACE) || defined(USE_TINYUSB))
|
||||
#include <class/cdc/cdc.h>
|
||||
|
||||
extern "C" void meshTinyUsbCdcLineStateChanged(uint8_t instance, bool dtr,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "UsbLoggingWatchdogEvent.h"
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Public, non-secret status shared by the CLI and management reports.
|
||||
struct UsbLoggingStatus {
|
||||
bool supported = false;
|
||||
bool logging_enabled = false;
|
||||
bool watchdog_enabled = false;
|
||||
bool watchdog_auto = false;
|
||||
bool host_connected = false;
|
||||
bool reader_connected = false;
|
||||
bool logger_active = false;
|
||||
bool stalled = false;
|
||||
bool recovering = false;
|
||||
bool recovery_deferred = false;
|
||||
bool persistence_ready = false;
|
||||
uint8_t stage = 0;
|
||||
uint8_t backoff_step = 0;
|
||||
uint32_t retry_seconds = 3600;
|
||||
uint32_t inactive_seconds = 0;
|
||||
uint32_t auto_connected_seconds = 0;
|
||||
uint32_t recovery_count = 0;
|
||||
uint32_t reboot_count = 0;
|
||||
UsbLoggingWatchdogEvent last_event;
|
||||
};
|
||||
|
||||
struct UsbLoggingObservation {
|
||||
bool supported = false;
|
||||
bool host_connected = false;
|
||||
bool reader_connected = false;
|
||||
bool pending = false;
|
||||
// Changes ONLY for a downstream USB TX-completion event, never enqueue,
|
||||
// queue discard, driver initialization, or our own recovery request.
|
||||
uint32_t tx_progress = 0;
|
||||
};
|
||||
|
||||
enum class UsbLoggingRecoveryResult : uint8_t {
|
||||
Deferred, Attempted, Unsupported
|
||||
};
|
||||
|
||||
UsbLoggingStatus usbLoggingStatus();
|
||||
UsbLoggingObservation observeUsbLoggingTransport();
|
||||
// stage 1: bounded local cleanup/probe; stage 2: descriptor-preserving USB
|
||||
// re-enumeration. Neither function may reboot the board or wait for a host.
|
||||
UsbLoggingRecoveryResult recoverUsbLoggingTransport(uint8_t stage);
|
||||
// Optional watchdog heartbeat. Bounded and plaintext logging-only; accepting
|
||||
// the record into a queue is not proof of host progress.
|
||||
void probeUsbLoggingTransport();
|
||||
bool isUsbLoggingTransportRecoveryPending();
|
||||
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,588 @@
|
||||
#if defined(ARDUINO)
|
||||
#include "UsbLoggingWatchdog.h"
|
||||
#include "UsbLoggingWatchdogPolicy.h"
|
||||
#include "UsbLogging.h"
|
||||
#include "UsbLoggingClientActivity.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#if MESH_USB_CONSOLE_COOPERATIVE
|
||||
#include "FilePresence.h"
|
||||
#include "PersistentStoreFormat.h"
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
#include "AtomicFileWriter.h"
|
||||
#elif defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
#include "ContactFileTransaction.h"
|
||||
#endif
|
||||
#if defined(ENABLE_OTA)
|
||||
#include "ota/OtaContext.h"
|
||||
#endif
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace mesh {
|
||||
namespace {
|
||||
using Policy = UsbLoggingWatchdogPolicy;
|
||||
using Event = UsbLoggingWatchdogEvent;
|
||||
constexpr size_t LegacyImageSize = 20;
|
||||
constexpr size_t ImageSize = 33;
|
||||
constexpr size_t EventOffset = 16;
|
||||
constexpr size_t CrcOffset = EventOffset + USB_WATCHDOG_EVENT_SIZE;
|
||||
constexpr uint32_t ProbeMs = 60000;
|
||||
constexpr uint32_t DeferredRetryMs = 5000;
|
||||
const char* const Path = "/usb_wdg";
|
||||
const char* const Backup = "/usb_wdg.bak";
|
||||
struct Config {
|
||||
uint8_t mode = 2; // Off=0, On=1, Auto=2; new nodes qualify before arming.
|
||||
uint8_t step = 0;
|
||||
uint32_t recoveries = 0;
|
||||
uint32_t reboots = 0;
|
||||
Event last_event;
|
||||
};
|
||||
enum class ReadResult : uint8_t { Missing, Valid, Invalid, Unreadable };
|
||||
FILESYSTEM* settings_fs = nullptr;
|
||||
Config config;
|
||||
Policy policy;
|
||||
// Separate USB health from application stats health. Actions from this tracker
|
||||
// are ignored; it keeps physical TX stalls observable after a lease expires.
|
||||
Policy physical_policy;
|
||||
UsbLoggingAutoArmPolicy auto_arm;
|
||||
bool persistence_ready = false;
|
||||
bool settings_durable = false;
|
||||
bool backend_unsupported = false;
|
||||
bool backend_deferred = false;
|
||||
bool backend_retry_started = false;
|
||||
bool reader_seen = false;
|
||||
uint32_t backend_retry_at = 0;
|
||||
uint32_t probe_at = 0;
|
||||
uint32_t (*event_epoch_seconds)() = nullptr;
|
||||
uint32_t uptime_last_tick = 0;
|
||||
uint64_t uptime_ms = 0;
|
||||
bool event_pending = false;
|
||||
|
||||
// Only the application loop/CLI writes policy and configuration. Management
|
||||
// readers get a bounded, non-blocking snapshot without filesystem/USB calls.
|
||||
std::atomic<uint32_t> status_flags{0}, status_stage_step{0};
|
||||
std::atomic<uint32_t> status_retry{3600}, status_inactive{0};
|
||||
std::atomic<uint32_t> status_recoveries{0}, status_reboots{0};
|
||||
std::atomic<uint32_t> status_auto_seconds{0};
|
||||
std::atomic<uint32_t> status_event_guard{0}, status_event_code{0};
|
||||
std::atomic<uint32_t> status_event_epoch{0}, status_event_uptime{0}, status_event_sequence{0};
|
||||
|
||||
uint32_t incrementSaturated(uint32_t value) {
|
||||
return value == UINT32_MAX ? value : value + 1;
|
||||
}
|
||||
|
||||
void tickUptime(uint32_t now) {
|
||||
const uint32_t delta = now - uptime_last_tick;
|
||||
if (delta > 0x80000000UL && uint32_t(uptime_last_tick - now) <= 5000) return;
|
||||
uptime_ms += delta;
|
||||
uptime_last_tick = now;
|
||||
}
|
||||
|
||||
uint8_t physicalReasons(const UsbLoggingObservation& observation) {
|
||||
return (!observation.host_connected ? Event::HOST_ABSENT : 0)
|
||||
| (!observation.reader_connected ? Event::READER_ABSENT : 0)
|
||||
| (physical_policy.stalled() ? Event::TX_STALLED : 0);
|
||||
}
|
||||
|
||||
Event newEvent(uint8_t action, uint8_t reasons) {
|
||||
Event event;
|
||||
event.action = action;
|
||||
event.reasons = reasons;
|
||||
event.epoch = event_epoch_seconds ? event_epoch_seconds() : 0;
|
||||
const uint64_t seconds = uptime_ms / 1000;
|
||||
event.uptime_seconds = seconds > UINT32_MAX ? UINT32_MAX : uint32_t(seconds);
|
||||
event.sequence = incrementSaturated(config.last_event.sequence);
|
||||
return event;
|
||||
}
|
||||
|
||||
void publish(const UsbLoggingObservation& observation) {
|
||||
const bool logger_active = isUsbLoggingEnabled() && observation.host_connected
|
||||
&& observation.reader_connected && isUsbLoggingClientActive() && !physical_policy.stalled();
|
||||
const uint32_t flags = (observation.supported && !backend_unsupported ? 1U : 0U)
|
||||
| (isUsbLoggingEnabled() ? 2U : 0U) | (config.mode == 1 ? 4U : 0U)
|
||||
| (observation.host_connected ? 8U : 0U)
|
||||
| (observation.reader_connected ? 16U : 0U)
|
||||
| (physical_policy.stalled() ? 32U : 0U) | (policy.stage() ? 64U : 0U)
|
||||
| (policy.deferred() || backend_deferred ? 128U : 0U)
|
||||
| (persistence_ready ? 256U : 0U) | (config.mode == 2 ? 512U : 0U)
|
||||
| (reader_seen ? 1024U : 0U) | (logger_active ? 2048U : 0U);
|
||||
status_stage_step.store(uint32_t(policy.stage()) | (uint32_t(config.step) << 8));
|
||||
status_retry.store(Policy::intervalMs(config.step) / 1000);
|
||||
status_inactive.store(policy.inactiveSeconds());
|
||||
status_recoveries.store(config.recoveries);
|
||||
status_reboots.store(config.reboots);
|
||||
status_auto_seconds.store(auto_arm.connectedSeconds());
|
||||
// A bounded sequence guard prevents mixing fields from different events.
|
||||
// All words are atomic, and seq_cst keeps the guard around their publication.
|
||||
status_event_guard.fetch_add(1);
|
||||
status_event_code.store(config.last_event.reasons | (uint32_t(config.last_event.action) << 4)
|
||||
| (config.last_event.persisted ? 128U : 0U));
|
||||
status_event_epoch.store(config.last_event.epoch);
|
||||
status_event_uptime.store(config.last_event.uptime_seconds);
|
||||
status_event_sequence.store(config.last_event.sequence);
|
||||
status_event_guard.fetch_add(1);
|
||||
status_flags.store(flags, std::memory_order_release);
|
||||
}
|
||||
|
||||
ReadResult readImage(const char* path, uint8_t* image, size_t* read_size = nullptr) {
|
||||
bool present = false;
|
||||
if (!filePresence(settings_fs, path, present)) return ReadResult::Unreadable;
|
||||
if (!present) return ReadResult::Missing;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
File file(*settings_fs);
|
||||
if (!file.open(path, FILE_O_READ)) return ReadResult::Unreadable;
|
||||
#elif defined(STM32_PLATFORM)
|
||||
File file = settings_fs->open(path, FILE_O_READ);
|
||||
#else
|
||||
File file = settings_fs->open(path, "r");
|
||||
#endif
|
||||
if (!file) return ReadResult::Unreadable;
|
||||
const size_t size = file.size();
|
||||
const bool legacy = size == LegacyImageSize;
|
||||
const bool right_size = legacy || size == ImageSize;
|
||||
const bool complete = right_size && static_cast<size_t>(file.read(image, size)) == size;
|
||||
file.close();
|
||||
if (!right_size) return ReadResult::Invalid;
|
||||
if (!complete) return ReadResult::Unreadable;
|
||||
const size_t crc_offset = size - sizeof(uint32_t);
|
||||
const bool valid = image[0] == 'U' && image[1] == 'W'
|
||||
&& image[2] == (legacy ? 1 : 2) && image[3] == 0 && image[4] <= 2
|
||||
&& image[5] <= Policy::MAX_STEP && image[6] == 0 && image[7] == 0
|
||||
&& storage::readLE32(image + crc_offset)
|
||||
== (storage::updateCRC32(0xffffffffU, image, crc_offset) ^ 0xffffffffU);
|
||||
Event event;
|
||||
if (!valid || (!legacy && (!decodeUsbWatchdogEvent(image + EventOffset, event)
|
||||
|| (event.sequence && !event.persisted)))) return ReadResult::Invalid;
|
||||
if (read_size) *read_size = size;
|
||||
return ReadResult::Valid;
|
||||
}
|
||||
|
||||
bool saveConfig(const Config& candidate, bool explicit_write = false) {
|
||||
// Automatic actions stop after any unverifiable commit. An explicit CLI
|
||||
// setter may retry/repair only this independent statefile on durable media.
|
||||
if (!settings_fs || !settings_durable || (!persistence_ready && !explicit_write)) return false;
|
||||
uint8_t image[ImageSize] = {'U', 'W', 2, 0};
|
||||
image[4] = candidate.mode;
|
||||
image[5] = candidate.step;
|
||||
storage::writeLE32(image + 8, candidate.recoveries);
|
||||
storage::writeLE32(image + 12, candidate.reboots);
|
||||
Event durable_event = candidate.last_event;
|
||||
durable_event.persisted = durable_event.sequence != 0;
|
||||
encodeUsbWatchdogEvent(image + EventOffset, durable_event);
|
||||
storage::writeLE32(image + CrcOffset,
|
||||
storage::updateCRC32(0xffffffffU, image, CrcOffset) ^ 0xffffffffU);
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
AtomicFileWriter writer(settings_fs, Path);
|
||||
#elif defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
ContactFileTransaction writer(settings_fs, Path, filePresence<FILESYSTEM>);
|
||||
#else
|
||||
persistence_ready = false;
|
||||
return false;
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) \
|
||||
|| defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
||||
bool ok = writer && writer.write(image, sizeof(image)) == sizeof(image)
|
||||
&& writer.commit();
|
||||
uint8_t verify[ImageSize];
|
||||
size_t verify_size = 0;
|
||||
ok = ok && readImage(Path, verify, &verify_size) == ReadResult::Valid && verify_size == ImageSize
|
||||
&& !memcmp(image, verify, sizeof(image));
|
||||
persistence_ready = ok; // Never reset on an unverified tier.
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool safeNow(bool (*safe)(void*), void* context) {
|
||||
return safe && !isUsbLoggingWatchdogUpdateActive() && safe(context);
|
||||
}
|
||||
|
||||
bool keyMatches(const char* command, const char* key) {
|
||||
const size_t length = strlen(key);
|
||||
return strncmp(command, key, length) == 0
|
||||
&& (command[length] == 0 || command[length] == ' ' || command[length] == '\t');
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UsbLoggingStatus usbLoggingStatus() {
|
||||
UsbLoggingStatus status;
|
||||
const uint32_t flags = status_flags.load(std::memory_order_acquire);
|
||||
const uint32_t stage_step = status_stage_step.load();
|
||||
status.supported = flags & 1U;
|
||||
status.logging_enabled = flags & 2U;
|
||||
status.watchdog_enabled = flags & 4U;
|
||||
status.host_connected = flags & 8U;
|
||||
status.reader_connected = flags & 16U;
|
||||
status.stalled = flags & 32U;
|
||||
status.recovering = flags & 64U;
|
||||
status.recovery_deferred = flags & 128U;
|
||||
status.persistence_ready = flags & 256U;
|
||||
status.watchdog_auto = flags & 512U;
|
||||
status.logger_active = flags & 2048U;
|
||||
status.stage = stage_step & 0xff;
|
||||
status.backoff_step = (stage_step >> 8) & 0xff;
|
||||
status.retry_seconds = status_retry.load();
|
||||
status.inactive_seconds = status_inactive.load();
|
||||
status.recovery_count = status_recoveries.load();
|
||||
status.reboot_count = status_reboots.load();
|
||||
status.auto_connected_seconds = status_auto_seconds.load();
|
||||
for (uint8_t attempt = 0; attempt < 3; ++attempt) {
|
||||
const uint32_t before = status_event_guard.load();
|
||||
if (before & 1U) continue;
|
||||
Event event;
|
||||
const uint32_t code = status_event_code.load();
|
||||
event.reasons = code & 15U;
|
||||
event.action = (code >> 4) & 7U;
|
||||
event.persisted = code & 128U;
|
||||
event.epoch = status_event_epoch.load();
|
||||
event.uptime_seconds = status_event_uptime.load();
|
||||
event.sequence = status_event_sequence.load();
|
||||
if (before == status_event_guard.load()) {
|
||||
status.last_event = event;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool isUsbLoggingWatchdogArmed() {
|
||||
if (isUsbLoggingTransportRecoveryPending()) return true;
|
||||
const auto status = usbLoggingStatus();
|
||||
const bool seen = status_flags.load(std::memory_order_acquire) & 1024U;
|
||||
return status.supported && status.logging_enabled
|
||||
&& (status.watchdog_enabled || (status.watchdog_auto && seen && status.stage < 2))
|
||||
&& status.persistence_ready;
|
||||
}
|
||||
|
||||
void loadUsbLoggingWatchdog(FILESYSTEM* fs, bool durable, uint32_t (*epoch_seconds)()) {
|
||||
settings_fs = fs;
|
||||
settings_durable = fs && durable;
|
||||
config = Config();
|
||||
persistence_ready = false;
|
||||
backend_unsupported = backend_deferred = backend_retry_started = false;
|
||||
reader_seen = false;
|
||||
event_epoch_seconds = epoch_seconds;
|
||||
uptime_last_tick = millis();
|
||||
uptime_ms = uptime_last_tick;
|
||||
event_pending = false;
|
||||
clearUsbLoggingClientActivity();
|
||||
probe_at = millis();
|
||||
uint8_t image[ImageSize];
|
||||
size_t image_size = 0;
|
||||
if (fs && durable) {
|
||||
ReadResult result = readImage(Path, image, &image_size);
|
||||
if (result == ReadResult::Missing) {
|
||||
result = readImage(Backup, image, &image_size);
|
||||
if (result == ReadResult::Valid) {
|
||||
// Repair only a conclusively absent primary. Never remove or replace
|
||||
// unreadable/corrupt images automatically, even with a good backup.
|
||||
const bool renamed = fs->rename(Backup, Path);
|
||||
uint8_t verify[ImageSize];
|
||||
size_t verify_size = 0;
|
||||
if (!renamed || readImage(Path, verify, &verify_size) != ReadResult::Valid
|
||||
|| image_size != verify_size || memcmp(image, verify, image_size)) {
|
||||
result = ReadResult::Unreadable;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == ReadResult::Valid) {
|
||||
config.mode = image[4];
|
||||
config.step = image[5];
|
||||
config.recoveries = storage::readLE32(image + 8);
|
||||
config.reboots = storage::readLE32(image + 12);
|
||||
if (image_size == ImageSize) decodeUsbWatchdogEvent(image + EventOffset, config.last_event);
|
||||
persistence_ready = true;
|
||||
} else if (result == ReadResult::Missing) {
|
||||
persistence_ready = true; // Fresh durable store qualifies in Auto.
|
||||
}
|
||||
}
|
||||
policy.begin(millis(), config.step);
|
||||
physical_policy.begin(millis(), config.step);
|
||||
auto_arm.begin(millis());
|
||||
publish(observeUsbLoggingTransport());
|
||||
}
|
||||
|
||||
bool isUsbLoggingWatchdogUpdateActive() {
|
||||
#if defined(ENABLE_OTA)
|
||||
const auto* ota = mesh::ota::ota_context_if_active();
|
||||
if (ota) {
|
||||
const auto state = ota->manager.fetchState();
|
||||
if (ota->apply_pending || ota->bootloader_apply_pending || ota->folder_active
|
||||
|| ota->folder_dest || ota->folderCaptureWaiting()
|
||||
|| ota->manager.pendingServeJobs() || ota->manager.pendingManifestJobs()
|
||||
|| ota->serve_expected || (state != mesh::ota::OtaManager::IDLE
|
||||
&& state != mesh::ota::OtaManager::FAILED)) return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool handleUsbLoggingWatchdogCommand(const char* command, char* reply,
|
||||
size_t capacity) {
|
||||
if (!command || !reply || !capacity) return false;
|
||||
if (keyMatches(command, "get usb.watchdog.last")) {
|
||||
if (strcmp(command, "get usb.watchdog.last")) {
|
||||
snprintf(reply, capacity, "Error: usage get usb.watchdog.last");
|
||||
return true;
|
||||
}
|
||||
const auto event = usbLoggingStatus().last_event;
|
||||
snprintf(reply, capacity, "> %s reasons=0x%02X epoch=%lu uptime=%lus seq=%lu persisted=%u",
|
||||
usbWatchdogActionName(event.action), unsigned(event.reasons), (unsigned long)event.epoch,
|
||||
(unsigned long)event.uptime_seconds, (unsigned long)event.sequence, event.persisted);
|
||||
return true;
|
||||
}
|
||||
if (keyMatches(command, "get usb.watchdog")) {
|
||||
if (strcmp(command, "get usb.watchdog")) {
|
||||
snprintf(reply, capacity, "Error: usage get usb.watchdog");
|
||||
return true;
|
||||
}
|
||||
publish(observeUsbLoggingTransport());
|
||||
const auto status = usbLoggingStatus();
|
||||
snprintf(reply, capacity,
|
||||
"> %s usb=%u log=%u host=%u reader=%u logger=%u stall=%u stage=%u step=%u "
|
||||
"retry=%lus idle=%lus auto=%lus defer=%u fs=%u rec=%lu boot=%lu",
|
||||
status.watchdog_auto ? "auto" : (status.watchdog_enabled ? "on" : "off"),
|
||||
status.supported,
|
||||
status.logging_enabled, status.host_connected, status.reader_connected, status.logger_active,
|
||||
status.stalled, status.stage, status.backoff_step,
|
||||
(unsigned long)status.retry_seconds, (unsigned long)status.inactive_seconds,
|
||||
(unsigned long)status.auto_connected_seconds,
|
||||
status.recovery_deferred, status.persistence_ready,
|
||||
(unsigned long)status.recovery_count, (unsigned long)status.reboot_count);
|
||||
return true;
|
||||
}
|
||||
if (!keyMatches(command, "set usb.watchdog")) return false;
|
||||
const char* value = command + strlen("set usb.watchdog");
|
||||
while (*value == ' ' || *value == '\t') ++value;
|
||||
if (strcmp(value, "on") && strcmp(value, "off") && strcmp(value, "auto")) {
|
||||
snprintf(reply, capacity, "Error: usage set usb.watchdog off|on|auto");
|
||||
return true;
|
||||
}
|
||||
const uint8_t mode = !strcmp(value, "on") ? 1 : (!strcmp(value, "auto") ? 2 : 0);
|
||||
const auto observation = observeUsbLoggingTransport();
|
||||
if (mode == 1 && !observation.supported) {
|
||||
snprintf(reply, capacity, "Error: USB watchdog unsupported on this transport");
|
||||
return true;
|
||||
}
|
||||
Config candidate = config;
|
||||
candidate.mode = mode;
|
||||
if (!saveConfig(candidate, true)) {
|
||||
snprintf(reply, capacity, "Error: USB watchdog state not durable; unchanged");
|
||||
} else {
|
||||
config = candidate;
|
||||
if (config.last_event.sequence) config.last_event.persisted = true;
|
||||
event_pending = false;
|
||||
backend_unsupported = backend_deferred = backend_retry_started = false;
|
||||
reader_seen = false;
|
||||
policy.begin(millis(), config.step);
|
||||
physical_policy.begin(millis(), config.step);
|
||||
auto_arm.begin(millis());
|
||||
probe_at = millis();
|
||||
snprintf(reply, capacity, "OK - USB watchdog %s (saved)", value);
|
||||
}
|
||||
publish(observation);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool serviceUsbLoggingWatchdog(bool (*safe)(void*), void* context) {
|
||||
const uint32_t now = millis();
|
||||
tickUptime(now);
|
||||
auto observation = observeUsbLoggingTransport();
|
||||
physical_policy.update(now, isUsbLoggingEnabled(), observation);
|
||||
const bool observing = config.mode != 0 && persistence_ready && isUsbLoggingEnabled()
|
||||
&& !backend_unsupported;
|
||||
if (!observing) reader_seen = false;
|
||||
if (!isUsbLoggingEnabled() || !observation.host_connected || !observation.reader_connected) {
|
||||
clearUsbLoggingClientActivity();
|
||||
}
|
||||
const bool lease_eligible = isUsbLoggingEnabled() && observation.host_connected
|
||||
&& observation.reader_connected && isUsbLoggingClientActive();
|
||||
auto policy_observation = observation;
|
||||
// USB ACK/DTR proves transport availability, not application activity.
|
||||
// Auto repairs a previously confirmed logger even when its process stops
|
||||
// polling but the kernel continues acknowledging bulk USB data.
|
||||
if (config.mode == 2 || reader_seen) policy_observation.reader_connected = lease_eligible;
|
||||
const bool active = observing && (config.mode == 1 || reader_seen);
|
||||
// Once confirmed, missing stats may request USB-only repair in On too.
|
||||
// Never reboot an otherwise healthy USB transport for an app-only stats gap.
|
||||
const bool physical_bad = !observation.host_connected || !observation.reader_connected
|
||||
|| physical_policy.stalled();
|
||||
const uint32_t physical_reboot_seconds = Policy::intervalMs(config.step) / 1000;
|
||||
const bool reboot_allowed = config.mode == 1 && physical_bad
|
||||
&& physical_policy.inactiveSeconds() >= physical_reboot_seconds;
|
||||
const auto action = policy.update(now, active, policy_observation, true, reboot_allowed);
|
||||
const bool logger_active = lease_eligible && !physical_policy.stalled();
|
||||
if (observing && observation.supported && logger_active) reader_seen = true;
|
||||
if (policy_observation.host_connected && policy_observation.reader_connected && !policy.stalled()) {
|
||||
backend_deferred = false;
|
||||
}
|
||||
const bool auto_due = auto_arm.update(now,
|
||||
observing && observation.supported && logger_active, config.mode == 2);
|
||||
// The previous reset was already vetoed. Finishing its journal is not a
|
||||
// recovery action and must not depend on logging remaining enabled.
|
||||
if (event_pending && persistence_ready) {
|
||||
if (safeNow(safe, context)) {
|
||||
event_pending = false; // Failed verification disables automatic retries.
|
||||
if (saveConfig(config)) config.last_event.persisted = true;
|
||||
}
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
if (!observing || !observation.supported) {
|
||||
backend_deferred = backend_retry_started = false;
|
||||
probe_at = now;
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
if (uint32_t(now - probe_at) >= ProbeMs) {
|
||||
probe_at = now; // Throttle unsafe/busy probes too, never a hot retry loop.
|
||||
if ((config.mode == 1 || logger_active)
|
||||
&& safeNow(safe, context)) probeUsbLoggingTransport();
|
||||
}
|
||||
if (config.mode == 2) {
|
||||
if (auto_due) {
|
||||
backend_deferred = !safeNow(safe, context);
|
||||
if (!backend_deferred) {
|
||||
Config candidate = config;
|
||||
candidate.mode = 1;
|
||||
candidate.step = 0; // Fourteen healthy days subsume the ten-minute reset.
|
||||
if (saveConfig(candidate)) {
|
||||
config = candidate;
|
||||
if (config.last_event.sequence) config.last_event.persisted = true;
|
||||
policy.begin(now, config.step);
|
||||
physical_policy.backoffCommitted(config.step);
|
||||
auto_arm.begin(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reader_seen || auto_due) {
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (action == Policy::Action::None) {
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
if (!safeNow(safe, context)) {
|
||||
policy.update(now, active, policy_observation, false, reboot_allowed);
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
if (action == Policy::Action::ResetBackoff && config.mode == 1) {
|
||||
Config candidate = config;
|
||||
candidate.step = 0;
|
||||
if (saveConfig(candidate)) {
|
||||
config = candidate;
|
||||
if (config.last_event.sequence) config.last_event.persisted = true;
|
||||
policy.backoffCommitted(0);
|
||||
}
|
||||
} else if (action == Policy::Action::Reboot && config.mode == 1) {
|
||||
Config candidate = config;
|
||||
candidate.step = Policy::nextStep(config.step);
|
||||
candidate.reboots = incrementSaturated(config.reboots);
|
||||
candidate.last_event = newEvent(Event::REBOOT_REQUESTED, physicalReasons(observation));
|
||||
if (saveConfig(candidate)) {
|
||||
config = candidate;
|
||||
config.last_event.persisted = true;
|
||||
// Recheck real transport progress using the OLD deadline before raising
|
||||
// the policy tier. A host may recover while its statefile is committing.
|
||||
const auto fresh = observeUsbLoggingTransport();
|
||||
const uint32_t fresh_now = millis();
|
||||
physical_policy.update(fresh_now, isUsbLoggingEnabled(), fresh);
|
||||
const bool fresh_physical_bad = !fresh.host_connected || !fresh.reader_connected
|
||||
|| physical_policy.stalled();
|
||||
auto fresh_policy = fresh;
|
||||
if (config.mode == 2 || reader_seen) {
|
||||
fresh_policy.reader_connected = fresh.host_connected && fresh.reader_connected
|
||||
&& isUsbLoggingClientActive();
|
||||
}
|
||||
const bool still_due = policy.update(fresh_now, active, fresh_policy, true,
|
||||
config.mode == 1 && fresh_physical_bad
|
||||
&& physical_policy.inactiveSeconds() >= physical_reboot_seconds)
|
||||
== Policy::Action::Reboot;
|
||||
policy.backoffCommitted(config.step);
|
||||
physical_policy.backoffCommitted(config.step);
|
||||
// A transfer may have gained ownership while flash committed. Keeping
|
||||
// the new tier but restarting the interval is safer than racing it.
|
||||
if (still_due && safeNow(safe, context)) {
|
||||
policy.actionAttempted(action);
|
||||
publish(fresh);
|
||||
return true;
|
||||
}
|
||||
policy.restartInterval();
|
||||
backend_deferred = still_due;
|
||||
// The verified reboot-intent journal is not evidence of a reset. Keep
|
||||
// cancellation truthful in RAM even if its best-effort commit fails.
|
||||
tickUptime(fresh_now);
|
||||
config.last_event = newEvent(Event::REBOOT_CANCELLED, candidate.last_event.reasons);
|
||||
event_pending = true;
|
||||
if (safeNow(safe, context)) {
|
||||
event_pending = false;
|
||||
if (saveConfig(config)) config.last_event.persisted = true;
|
||||
}
|
||||
publish(fresh);
|
||||
return false;
|
||||
} else {
|
||||
config.last_event = candidate.last_event;
|
||||
}
|
||||
} else if (action == Policy::Action::SoftRecovery || action == Policy::Action::Reenumerate) {
|
||||
if (backend_retry_started && uint32_t(now - backend_retry_at) < DeferredRetryMs) {
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
backend_retry_started = true;
|
||||
backend_retry_at = now;
|
||||
const uint8_t stage = action == Policy::Action::SoftRecovery ? 1 : 2;
|
||||
const auto result = recoverUsbLoggingTransport(stage);
|
||||
backend_deferred = result == UsbLoggingRecoveryResult::Deferred;
|
||||
if (result == UsbLoggingRecoveryResult::Attempted) {
|
||||
config.recoveries = incrementSaturated(config.recoveries);
|
||||
policy.actionAttempted(action);
|
||||
tickUptime(millis());
|
||||
uint8_t reasons = physicalReasons(observation);
|
||||
if (!reasons) reasons = Event::CLIENT_INACTIVE;
|
||||
config.last_event = newEvent(stage == 1 ? Event::SOFT_RECOVERY : Event::REENUMERATE, reasons);
|
||||
if (saveConfig(config)) config.last_event.persisted = true;
|
||||
} else if (result == UsbLoggingRecoveryResult::Unsupported) {
|
||||
backend_unsupported = true;
|
||||
}
|
||||
}
|
||||
publish(observation);
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
#else
|
||||
// There is no native transport to observe or repair on UART-bridge/unsupported
|
||||
// targets. Do not spend scarce flash on timers, transactions, or OTA vetoes
|
||||
// that can never authorize recovery; retain truthful management/CLI status.
|
||||
namespace mesh {
|
||||
UsbLoggingStatus usbLoggingStatus() {
|
||||
UsbLoggingStatus status;
|
||||
status.logging_enabled = isUsbLoggingEnabled();
|
||||
return status;
|
||||
}
|
||||
|
||||
void loadUsbLoggingWatchdog(FILESYSTEM*, bool, uint32_t (*)()) {}
|
||||
bool serviceUsbLoggingWatchdog(bool (*)(void*), void*) { return false; }
|
||||
bool isUsbLoggingWatchdogArmed() { return false; }
|
||||
bool isUsbLoggingWatchdogUpdateActive() { return false; }
|
||||
|
||||
bool handleUsbLoggingWatchdogCommand(const char* command, char* reply,
|
||||
size_t capacity) {
|
||||
if (!command || !reply || !capacity) return false;
|
||||
static const char* const keys[] = {"get usb.watchdog.last", "get usb.watchdog", "set usb.watchdog"};
|
||||
for (const char* key : keys) {
|
||||
const size_t length = strlen(key);
|
||||
if (strncmp(command, key, length)
|
||||
|| (command[length] && command[length] != ' ' && command[length] != '\t')) continue;
|
||||
snprintf(reply, capacity, "Error: unsupported");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace mesh
|
||||
#endif // MESH_USB_CONSOLE_COOPERATIVE
|
||||
#endif // ARDUINO
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "IdentityStore.h"
|
||||
#include "UsbLoggingStatus.h"
|
||||
#include <stddef.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// A separate versioned statefile keeps all deployed role preference offsets
|
||||
// unchanged. New nodes default to Auto (14 continuous healthy USB days before
|
||||
// arming). Volatile fallback filesystems must never authorize a reboot.
|
||||
void loadUsbLoggingWatchdog(FILESYSTEM* fs, bool durable = true,
|
||||
uint32_t (*epoch_seconds)() = nullptr);
|
||||
bool handleUsbLoggingWatchdogCommand(const char* command, char* reply,
|
||||
size_t capacity);
|
||||
|
||||
// Call from the application loop only. The callback protects protocol, radio
|
||||
// and dirty-contact ownership and is rechecked after the durable tier commit.
|
||||
// True authorizes the caller's board.reboot(); this helper never reboots.
|
||||
bool serviceUsbLoggingWatchdog(bool (*safe)(void*), void* context = nullptr);
|
||||
bool isUsbLoggingWatchdogArmed();
|
||||
bool isUsbLoggingWatchdogUpdateActive();
|
||||
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Timestamp is the node's advisory RTC, never proof of synchronized UTC.
|
||||
// A reboot request is durable intent, not proof that a physical reset occurred.
|
||||
struct UsbLoggingWatchdogEvent {
|
||||
enum Reason : uint8_t {
|
||||
HOST_ABSENT = 1, READER_ABSENT = 2, TX_STALLED = 4, CLIENT_INACTIVE = 8
|
||||
};
|
||||
enum Action : uint8_t {
|
||||
NONE = 0, SOFT_RECOVERY = 1, REENUMERATE = 2,
|
||||
REBOOT_REQUESTED = 3, REBOOT_CANCELLED = 4
|
||||
};
|
||||
uint8_t reasons = 0;
|
||||
uint8_t action = NONE;
|
||||
uint32_t epoch = 0;
|
||||
uint32_t uptime_seconds = 0;
|
||||
uint32_t sequence = 0;
|
||||
bool persisted = false;
|
||||
};
|
||||
|
||||
constexpr size_t USB_WATCHDOG_EVENT_SIZE = 13;
|
||||
|
||||
inline void encodeUsbWatchdogEvent(uint8_t* out, const UsbLoggingWatchdogEvent& event) {
|
||||
memset(out, 0, USB_WATCHDOG_EVENT_SIZE);
|
||||
if (!event.sequence) return;
|
||||
out[0] = (event.reasons & 15u) | ((event.action & 7u) << 4)
|
||||
| (event.persisted ? 128u : 0u);
|
||||
const uint32_t words[] = {event.epoch, event.uptime_seconds, event.sequence};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
for (size_t j = 0; j < 4; ++j) out[1 + 4 * i + j] = words[i] >> (8 * j);
|
||||
}
|
||||
}
|
||||
|
||||
// Relays need only validate the wire shape, not construct or decode timestamps.
|
||||
// A nonempty record has a nonzero sequence, reason mask and known action.
|
||||
inline bool validUsbWatchdogEvent(const uint8_t* in) {
|
||||
// memcpy keeps unaligned wire inputs legal; zero detection is independent
|
||||
// of the host byte order and lets small MCUs use a single word load.
|
||||
uint32_t sequence;
|
||||
memcpy(&sequence, in + 9, sizeof(sequence));
|
||||
if (sequence) {
|
||||
const uint8_t action = (in[0] >> 4) & 7u;
|
||||
return (in[0] & 15u) && action >= UsbLoggingWatchdogEvent::SOFT_RECOVERY
|
||||
&& action <= UsbLoggingWatchdogEvent::REBOOT_CANCELLED;
|
||||
}
|
||||
uint32_t epoch, uptime;
|
||||
memcpy(&epoch, in + 1, sizeof(epoch));
|
||||
memcpy(&uptime, in + 5, sizeof(uptime));
|
||||
return !(in[0] | epoch | uptime);
|
||||
}
|
||||
|
||||
inline bool decodeUsbWatchdogEvent(const uint8_t* in, UsbLoggingWatchdogEvent& event) {
|
||||
if (!validUsbWatchdogEvent(in)) return false;
|
||||
UsbLoggingWatchdogEvent decoded;
|
||||
decoded.reasons = in[0] & 15u;
|
||||
decoded.action = (in[0] >> 4) & 7u;
|
||||
decoded.persisted = in[0] & 128u;
|
||||
uint32_t* words[] = {&decoded.epoch, &decoded.uptime_seconds, &decoded.sequence};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
for (size_t j = 0; j < 4; ++j) *words[i] |= uint32_t(in[1 + 4 * i + j]) << (8 * j);
|
||||
}
|
||||
event = decoded;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline const char* usbWatchdogActionName(uint8_t action) {
|
||||
switch (action) {
|
||||
case UsbLoggingWatchdogEvent::SOFT_RECOVERY: return "soft-recovery";
|
||||
case UsbLoggingWatchdogEvent::REENUMERATE: return "reenumerate";
|
||||
case UsbLoggingWatchdogEvent::REBOOT_REQUESTED: return "reboot-requested";
|
||||
case UsbLoggingWatchdogEvent::REBOOT_CANCELLED: return "reboot-cancelled";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,192 @@
|
||||
#pragma once
|
||||
|
||||
#include "UsbLoggingStatus.h"
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Pure policy. Persistence, protocol ownership, USB operations and board
|
||||
// reboot are deliberately handled by the caller, not from an ISR/USB task.
|
||||
class UsbLoggingWatchdogPolicy {
|
||||
public:
|
||||
enum class Action : uint8_t { None, SoftRecovery, Reenumerate, Reboot, ResetBackoff };
|
||||
static constexpr uint8_t MAX_STEP = 8;
|
||||
static constexpr uint32_t BASE_MS = 3600000UL;
|
||||
static constexpr uint32_t MAX_MS = 604800000UL;
|
||||
static constexpr uint32_t EARLY_RECOVERY_MS = 300000UL;
|
||||
static constexpr uint32_t RECOVERY_GRACE_MS = 60000UL;
|
||||
static constexpr uint32_t HEALTHY_RESET_MS = 600000UL;
|
||||
static constexpr uint32_t STALL_OBSERVE_MS = 30000UL;
|
||||
|
||||
static uint32_t intervalMs(uint8_t step) {
|
||||
return step >= MAX_STEP ? MAX_MS : BASE_MS << step;
|
||||
}
|
||||
static uint8_t nextStep(uint8_t step) {
|
||||
return step >= MAX_STEP ? MAX_STEP : uint8_t(step + 1);
|
||||
}
|
||||
|
||||
void begin(uint32_t now, uint8_t step) {
|
||||
_now = now;
|
||||
_last_tick = now;
|
||||
_step = step <= MAX_STEP ? step : MAX_STEP;
|
||||
_clock_started = true;
|
||||
clearTracking();
|
||||
}
|
||||
|
||||
Action update(uint32_t now, bool active, const UsbLoggingObservation& observation,
|
||||
bool recovery_safe = true, bool reboot_allowed = true) {
|
||||
tick(now);
|
||||
if (active && observation.supported && !_active) clearTracking();
|
||||
_active = active && observation.supported;
|
||||
const bool progress = _observed && observation.tx_progress != _progress;
|
||||
_progress = observation.tx_progress;
|
||||
if (!_observed || progress || !observation.pending) _pending_since = _now;
|
||||
_observed = true;
|
||||
_stalled = observation.host_connected && observation.reader_connected
|
||||
&& observation.pending && _now - _pending_since >= STALL_OBSERVE_MS;
|
||||
if (!active || !observation.supported) {
|
||||
// Still observe USB stalls while Auto is qualifying or the watchdog is
|
||||
// off, without accumulating a reboot deadline. Enabling always starts
|
||||
// a new full interval via the activation edge above.
|
||||
_fault = _healthy = _need_progress = _deferred = false;
|
||||
_stage = 0;
|
||||
return Action::None;
|
||||
}
|
||||
if (_stalled) _need_progress = true;
|
||||
if (progress && observation.host_connected && observation.reader_connected) {
|
||||
_need_progress = false;
|
||||
}
|
||||
const bool bad = !observation.host_connected || !observation.reader_connected
|
||||
|| _stalled || _need_progress;
|
||||
if (!bad) {
|
||||
_fault = false;
|
||||
_stage = 0;
|
||||
_deferred = false;
|
||||
if (!_healthy) { _healthy = true; _healthy_since = _now; }
|
||||
const bool reset_due = _step != 0 && _now - _healthy_since >= HEALTHY_RESET_MS;
|
||||
_deferred = reset_due && !recovery_safe;
|
||||
return reset_due && recovery_safe ? Action::ResetBackoff : Action::None;
|
||||
}
|
||||
_healthy = false;
|
||||
if (!_fault) {
|
||||
_fault = true;
|
||||
_fault_since = _stalled ? _pending_since : _now;
|
||||
}
|
||||
// A slow but genuinely draining endpoint gets a full new interval. A
|
||||
// purge or accepted producer write cannot postpone this deadline.
|
||||
if (progress && observation.host_connected && observation.reader_connected
|
||||
&& _stage == 0) _fault_since = _now;
|
||||
const bool due = _stage != 0 || _now - _fault_since >= EARLY_RECOVERY_MS;
|
||||
_deferred = due && !recovery_safe;
|
||||
if (!due || !recovery_safe) return Action::None;
|
||||
if (_stage == 0) return Action::SoftRecovery;
|
||||
if (_now - _stage_since < RECOVERY_GRACE_MS) return Action::None;
|
||||
if (_stage == 1) return Action::Reenumerate;
|
||||
// Cheap USB recovery does not move the original board-reboot deadline.
|
||||
// Auto can attempt USB-only repair without ever authorizing an MCU reset.
|
||||
return reboot_allowed && _now - _fault_since >= intervalMs(_step)
|
||||
? Action::Reboot : Action::None;
|
||||
}
|
||||
|
||||
// A Deferred backend result must not call this: it is retried by the caller.
|
||||
// Attempted means requested, not proven healthy. Only observed completion
|
||||
// can clear a previously stalled transport after destructive cleanup.
|
||||
void actionAttempted(Action action) {
|
||||
if (action == Action::SoftRecovery) {
|
||||
_need_progress = _need_progress || _stalled;
|
||||
_stage = 1;
|
||||
_stage_since = _now;
|
||||
} else if (action == Action::Reenumerate) {
|
||||
_stage = 2;
|
||||
_stage_since = _now;
|
||||
} else if (action == Action::Reboot) {
|
||||
_stage = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Call only after the next retry tier was made durable. A failed commit
|
||||
// must never authorize a reboot using the old one-hour tier.
|
||||
void backoffCommitted(uint8_t step) {
|
||||
_step = step <= MAX_STEP ? step : MAX_STEP;
|
||||
}
|
||||
void restartInterval() {
|
||||
_stage = 0;
|
||||
_fault_since = _now;
|
||||
_need_progress = false;
|
||||
}
|
||||
uint8_t backoffStep() const { return _step; }
|
||||
uint8_t stage() const { return _stage; }
|
||||
bool stalled() const { return _stalled || _need_progress; }
|
||||
bool deferred() const { return _deferred; }
|
||||
uint32_t inactiveSeconds() const {
|
||||
const uint64_t seconds = _fault ? (_now - _fault_since) / 1000 : 0;
|
||||
return seconds > UINT32_MAX ? UINT32_MAX : uint32_t(seconds);
|
||||
}
|
||||
|
||||
private:
|
||||
void tick(uint32_t now) {
|
||||
if (!_clock_started) { begin(now, _step); return; }
|
||||
const uint32_t delta = now - _last_tick;
|
||||
// Ignore small out-of-order clock samples, while extending normal
|
||||
// millis rollover to a monotonic clock for nodes up longer than 49 days.
|
||||
if (delta > 0x80000000UL && uint32_t(_last_tick - now) <= 5000) return;
|
||||
_now += delta;
|
||||
_last_tick = now;
|
||||
}
|
||||
void clearTracking() {
|
||||
_fault = false;
|
||||
_healthy = false;
|
||||
_observed = false;
|
||||
_stalled = false;
|
||||
_need_progress = false;
|
||||
_deferred = false;
|
||||
_stage = 0;
|
||||
_fault_since = _pending_since = _healthy_since = _stage_since = _now;
|
||||
}
|
||||
uint64_t _now = 0, _fault_since = 0, _pending_since = 0;
|
||||
uint64_t _healthy_since = 0, _stage_since = 0;
|
||||
uint32_t _last_tick = 0, _progress = 0;
|
||||
uint8_t _step = 0, _stage = 0;
|
||||
bool _clock_started = false, _observed = false, _fault = false, _healthy = false;
|
||||
bool _active = false;
|
||||
bool _stalled = false, _need_progress = false, _deferred = false;
|
||||
};
|
||||
|
||||
// Auto is a continuous qualification, not accumulated occasional cable use.
|
||||
// Its timer is intentionally volatile across reboot: a cold start cannot prove
|
||||
// that the logging reader stayed connected while the MCU was down.
|
||||
class UsbLoggingAutoArmPolicy {
|
||||
public:
|
||||
static constexpr uint32_t QUALIFICATION_MS = 1209600000UL; // 14 days
|
||||
void begin(uint32_t now) {
|
||||
_now = now;
|
||||
_last_tick = now;
|
||||
_started = true;
|
||||
_qualifying = false;
|
||||
_since = _now;
|
||||
}
|
||||
bool update(uint32_t now, bool qualifying, bool auto_mode) {
|
||||
if (!_started) begin(now);
|
||||
const uint32_t delta = now - _last_tick;
|
||||
if (!(delta > 0x80000000UL && uint32_t(_last_tick - now) <= 5000)) {
|
||||
_now += delta;
|
||||
_last_tick = now;
|
||||
}
|
||||
if (!auto_mode || !qualifying) {
|
||||
_qualifying = false;
|
||||
_since = _now;
|
||||
return false;
|
||||
}
|
||||
if (!_qualifying) { _qualifying = true; _since = _now; }
|
||||
return _now - _since >= QUALIFICATION_MS;
|
||||
}
|
||||
uint32_t connectedSeconds() const {
|
||||
const uint64_t seconds = _qualifying ? (_now - _since) / 1000 : 0;
|
||||
return seconds > UINT32_MAX ? UINT32_MAX : uint32_t(seconds);
|
||||
}
|
||||
private:
|
||||
uint64_t _now = 0, _since = 0;
|
||||
uint32_t _last_tick = 0;
|
||||
bool _started = false, _qualifying = false;
|
||||
};
|
||||
|
||||
} // namespace mesh
|
||||
@@ -270,7 +270,7 @@ static unsigned long s_wifi_disconnect_time = 0;
|
||||
// #region agent log
|
||||
static void agentLogHeap(const char* location, const char* message, const char* hypothesisId,
|
||||
size_t free_h, size_t max_alloc, unsigned long internal_free, unsigned long spiram_free) {
|
||||
if (!mesh::isUsbLoggingEnabled()) return;
|
||||
if (!mesh::isUsbDebugLoggingEnabled()) return;
|
||||
char buf[320];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"sessionId\":\"debug-session\",\"location\":\"%s\",\"message\":\"%s\",\"hypothesisId\":\"%s\","
|
||||
|
||||
@@ -36,8 +36,8 @@ class MeshSNMPAgent; // Forward declaration
|
||||
// USB CDC-aware debug macros: only print if Serial is ready (non-blocking check)
|
||||
// Serial.availableForWrite() returns bytes available in write buffer (>0 means ready)
|
||||
// This prevents hangs when USB CDC isn't ready yet (e.g., ESP32-S3 native USB)
|
||||
#define MQTT_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MQTT_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define MQTT_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define MQTT_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define MQTT_DEBUG_PRINT(...) {}
|
||||
#define MQTT_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -71,8 +71,8 @@ public:
|
||||
|
||||
#if ESPNOW_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define ESPNOW_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("ESP-Now: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define ESPNOW_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("ESP-Now: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define ESPNOW_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("ESP-Now: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define ESPNOW_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("ESP-Now: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define ESPNOW_DEBUG_PRINT(...) {}
|
||||
#define ESPNOW_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -196,8 +196,8 @@ public:
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define BLE_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define BLE_DEBUG_PRINT(...) {}
|
||||
#define BLE_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -55,7 +55,7 @@ void TBeamBoard::scanDevices(TwoWire *w)
|
||||
uint8_t err, addr;
|
||||
int nDevices = 0;
|
||||
uint32_t start = 0;
|
||||
const bool usb_logging = mesh::isUsbLoggingEnabled();
|
||||
const bool usb_logging = mesh::isUsbDebugLoggingEnabled();
|
||||
|
||||
if (usb_logging) Serial.println("Scanning I2C for Devices");
|
||||
for (addr = 1; addr < 127; addr++) {
|
||||
@@ -120,7 +120,7 @@ void TBeamBoard::scanDevices(TwoWire *w)
|
||||
}
|
||||
void TBeamBoard::printPMU()
|
||||
{
|
||||
if (!mesh::isUsbLoggingEnabled()) return;
|
||||
if (!mesh::isUsbDebugLoggingEnabled()) return;
|
||||
Serial.print("isCharging:"); Serial.println(PMU->isCharging() ? "YES" : "NO");
|
||||
Serial.print("isDischarge:"); Serial.println(PMU->isDischarge() ? "YES" : "NO");
|
||||
Serial.print("isVbusIn:"); Serial.println(PMU->isVbusIn() ? "YES" : "NO");
|
||||
|
||||
@@ -826,13 +826,13 @@ bool WebConfigServer::startSetupMode(char reply[]) {
|
||||
const esp_err_t sta_protocol_result = esp_wifi_set_protocol(
|
||||
WIFI_IF_STA, mesh::wifi::kProtocolMask);
|
||||
if (ap_protocol_result == ESP_OK && sta_protocol_result == ESP_OK) break;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig protocol reset failed: AP=%d STA=%d\n",
|
||||
(int)ap_protocol_result, (int)sta_protocol_result);
|
||||
ap_ok = false;
|
||||
}
|
||||
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig AP attempt %u failed: mode_ok=%d disconnect_ok=%d mode=%d heap=%u largest=%u\n",
|
||||
(unsigned)attempt, mode_ok, disconnect_ok, (int)WiFi.getMode(),
|
||||
(unsigned)ESP.getFreeHeap(),
|
||||
@@ -1038,7 +1038,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
break;
|
||||
case WebConfigBatch::StopAction::Warn:
|
||||
_stop_warned = true;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: stop waiting for %lu handler(s); retaining session safely\n",
|
||||
(unsigned long)refs);
|
||||
break;
|
||||
@@ -1067,11 +1067,11 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
createServer();
|
||||
_connect_deadline = 0;
|
||||
_last_activity = now;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig ready: http://%s/\n",
|
||||
WiFi.localIP().toString().c_str());
|
||||
} else if (_connect_deadline && (int32_t)(now - _connect_deadline) >= 0) {
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: WiFi '%s' unavailable; opening setup AP\n", _wifi_ssid);
|
||||
const bool retry_saved_wifi = _wifi_ssid[0] != 0;
|
||||
// Keep the WiFi driver running while changing from STA to AP+STA.
|
||||
@@ -1085,7 +1085,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
if (startSetupMode(ignored)) {
|
||||
_retry_saved_wifi_in_setup = retry_saved_wifi;
|
||||
}
|
||||
mesh::usbLoggingPort().println(ignored);
|
||||
mesh::usbDebugPort().println(ignored);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1101,7 +1101,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
_wifi_reconnect_tracker.noteDisconnected(now);
|
||||
if (_wifi_reconnect_tracker.retryDue(now)) {
|
||||
_wifi_reconnect_tracker.noteAttempt(now);
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: WiFi still unavailable; retrying '%s'\n",
|
||||
_wifi_ssid);
|
||||
WiFi.mode(WIFI_STA);
|
||||
@@ -1139,7 +1139,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
_wifi_reconnect_tracker.noteConnected();
|
||||
_mode = MODE_LAN;
|
||||
_last_activity = now;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: saved WiFi recovered; ready at http://%s/\n",
|
||||
WiFi.localIP().toString().c_str());
|
||||
} else if (_owns_wifi && _setup_reconnect_in_progress
|
||||
@@ -1147,14 +1147,14 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
WiFi.disconnect(false, false);
|
||||
_setup_reconnect_in_progress = false;
|
||||
_setup_reconnect_deadline = 0;
|
||||
mesh::usbLoggingPort().println(
|
||||
mesh::usbDebugPort().println(
|
||||
"WebConfig: saved WiFi still unavailable; setup AP remains active");
|
||||
} else if (_owns_wifi && !_setup_reconnect_in_progress
|
||||
&& _wifi_reconnect_tracker.retryDue(now)) {
|
||||
_wifi_reconnect_tracker.noteAttempt(now);
|
||||
_setup_reconnect_in_progress = true;
|
||||
_setup_reconnect_deadline = now + 20000UL;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: retrying saved WiFi '%s'\n", _wifi_ssid);
|
||||
mesh::wifi::beginStation(_wifi_ssid, _wifi_password);
|
||||
}
|
||||
@@ -1172,7 +1172,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
// Consume this request first so a full filesystem cannot turn tick() into
|
||||
// a tight loop of repeated flush attempts.
|
||||
_reboot_at = 0;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: rebooting now (%s)\n",
|
||||
_batch_reboot_armed ? "confirmed" : "fallback");
|
||||
_cb->rebootNow();
|
||||
@@ -1180,7 +1180,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
|
||||
if ((int32_t)(_diag_until - now) > 0 && (now - _diag_last) >= 1000) {
|
||||
_diag_last = now;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: diag sta=%d heap=%u batch=%d/%d state=%d\n",
|
||||
(int)WiFi.softAPgetStationNum(), (unsigned)ESP.getFreeHeap(),
|
||||
(int)_batch_next, (int)_batch_count, (int)_batch_state);
|
||||
@@ -1201,7 +1201,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
_mode == MODE_SETUP, _wifi_ssid[0] != 0, now,
|
||||
_setup_started_at,
|
||||
(uint32_t)WEBCONFIG_UNCONFIGURED_SETUP_TIMEOUT_MS)) {
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: WiFi still unconfigured after %lu minutes; powering off until reboot or explicit restart\n",
|
||||
(unsigned long)((uint32_t)WEBCONFIG_UNCONFIGURED_SETUP_TIMEOUT_MS / 60000UL));
|
||||
requestStop();
|
||||
@@ -1232,7 +1232,7 @@ void WebConfigServer::tick(uint32_t now) {
|
||||
_setup_reconnect_deadline = 0;
|
||||
_mode = MODE_LAN;
|
||||
_last_activity = now;
|
||||
mesh::usbLoggingPort().println(
|
||||
mesh::usbDebugPort().println(
|
||||
"WebConfig: setup AP idle; saved WiFi recovery continues");
|
||||
} else {
|
||||
requestStop();
|
||||
@@ -1268,7 +1268,7 @@ void WebConfigServer::serviceSetupWiFiHandoff(uint32_t now) {
|
||||
_setup_wifi_handoff_pending = false;
|
||||
_setup_wifi_handoff_deadline = 0;
|
||||
}
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: joined '%s' at %s; waiting for browser handoff\n",
|
||||
_wifi_ssid, ip);
|
||||
finishBatch(now);
|
||||
@@ -1297,7 +1297,7 @@ void WebConfigServer::serviceSetupWiFiHandoff(uint32_t now) {
|
||||
_setup_wifi_handoff_deadline = 0;
|
||||
_setup_wifi_handoff_ip[0] = 0;
|
||||
}
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: could not join '%s'; setup AP remains active\n",
|
||||
_wifi_ssid);
|
||||
finishBatch(now);
|
||||
@@ -1404,11 +1404,11 @@ void WebConfigServer::drainBatch(uint32_t now) {
|
||||
// `set wifi.pwd` or `password` from the terminal must not reach the serial
|
||||
// log, which is a different audience from the browser session.
|
||||
if (_batch_kind == BATCH_CLI) {
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: cli %d/%d took %lums\n", (int)_batch_next,
|
||||
(int)_batch_count, (unsigned long)(_batch_last_cmd - t0));
|
||||
} else {
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: cmd %d/%d '%s' took %lums\n", (int)_batch_next,
|
||||
(int)_batch_count, e.key,
|
||||
(unsigned long)(_batch_last_cmd - t0));
|
||||
@@ -1461,7 +1461,7 @@ void WebConfigServer::drainBatch(uint32_t now) {
|
||||
// that will be shown to the operator before the setup AP is shut down.
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.setAutoReconnect(false);
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WebConfig: testing saved WiFi '%s' before reboot\n", _wifi_ssid);
|
||||
mesh::wifi::beginStation(_wifi_ssid, _wifi_password);
|
||||
return;
|
||||
@@ -1477,7 +1477,7 @@ void WebConfigServer::drainBatch(uint32_t now) {
|
||||
// Distinguishes "client stopped sending" from "server stopped accepting" when
|
||||
// a save's confirmation polls go missing on hardware.
|
||||
static void wcLogReq(AsyncWebServerRequest* r) {
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: http %s %s\n", r->methodToString(), r->url().c_str());
|
||||
}
|
||||
|
||||
@@ -2102,7 +2102,7 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
|
||||
uint32_t du = millis() + 60000;
|
||||
if (du == 0) du = 1;
|
||||
_diag_until = du;
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: config POST accepted, %d cmds, reboot=%d\n",
|
||||
count, (int)reboot_after);
|
||||
|
||||
@@ -2117,12 +2117,12 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
|
||||
|
||||
void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
|
||||
if (_mode == MODE_OFF) {
|
||||
mesh::usbLoggingPort().println("WC: result read -> 503 (mode off)");
|
||||
mesh::usbDebugPort().println("WC: result read -> 503 (mode off)");
|
||||
req->send(503);
|
||||
return;
|
||||
}
|
||||
if (!checkAuth(req)) {
|
||||
mesh::usbLoggingPort().println("WC: result read -> 401");
|
||||
mesh::usbDebugPort().println("WC: result read -> 401");
|
||||
req->send(401, "application/json", "{\"error\":\"auth\"}");
|
||||
return;
|
||||
}
|
||||
@@ -2138,7 +2138,7 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
|
||||
|
||||
// Entry print BEFORE the lock (racy state read is fine for diag): if this
|
||||
// fires but no branch print follows, the handler is blocked on _mux.
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: result entry mode=%d state=%d\n",
|
||||
(int)_mode, (int)_batch_state);
|
||||
WCLock lock(_mux);
|
||||
@@ -2150,7 +2150,7 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
|
||||
const WebConfigBatch::ResultOutcome outcome =
|
||||
WebConfigBatch::classifyResult(toSpecState(_batch_state), mine);
|
||||
if (outcome == WebConfigBatch::ResultOutcome::Idle) {
|
||||
mesh::usbLoggingPort().println("WC: result read -> idle");
|
||||
mesh::usbDebugPort().println("WC: result read -> idle");
|
||||
StaticJsonDocument<64> idle;
|
||||
idle["state"] = "idle";
|
||||
idle["reqid"] = requested_reqid;
|
||||
@@ -2172,7 +2172,7 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
|
||||
req->send(200, "application/json", out);
|
||||
return;
|
||||
}
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: result read -> done (reboot=%d armed=%d all_ok=%d)\n",
|
||||
(int)_batch_reboot, (int)_batch_reboot_armed,
|
||||
(int)_batch_all_ok);
|
||||
@@ -2557,7 +2557,7 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
|
||||
strncpy(_batch_reqid, reqid, sizeof(_batch_reqid) - 1);
|
||||
_batch_reqid[sizeof(_batch_reqid) - 1] = 0;
|
||||
_batch_state = BATCH_PENDING; // tick() picks it up on the loop task
|
||||
mesh::usbLoggingPort().printf(
|
||||
mesh::usbDebugPort().printf(
|
||||
"WC: cli POST accepted, %d cmds, reboot=%d\n",
|
||||
count, (int)defer_reboot);
|
||||
|
||||
|
||||
@@ -92,9 +92,9 @@ class SerialEthernetInterface : public BaseSerialInterface {
|
||||
|
||||
#if ETHERNET_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define ETHERNET_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("ETH: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("ETH: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINT_IP(name, ip) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("ETH: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("ETH: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define ETHERNET_DEBUG_PRINT_IP(name, ip) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]); } } while(0)
|
||||
#else
|
||||
#define ETHERNET_DEBUG_PRINT(...) {}
|
||||
#define ETHERNET_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -222,8 +222,8 @@ public:
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define BLE_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define BLE_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("BLE: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define BLE_DEBUG_PRINT(...) {}
|
||||
#define BLE_DEBUG_PRINTLN(...) {}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// Opt-in OTA tracing over Serial: build with -D OTA_DEBUG to watch the fetch (ADV/REQ/block/page-flush)
|
||||
// during bring-up. Compiles to nothing otherwise, and on the native host (no Arduino), so it never
|
||||
// during bring-up, with usb.logging and usb.debug enabled. Compiles to nothing otherwise, and on the native host (no Arduino), so it never
|
||||
// touches a non-debug or test build.
|
||||
#if defined(OTA_DEBUG) && defined(ARDUINO)
|
||||
#include <Arduino.h>
|
||||
#include <helpers/UsbLogging.h>
|
||||
#define OTA_DBG(...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf(__VA_ARGS__); } } while (0)
|
||||
#define OTA_DBG(...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf(__VA_ARGS__); } } while (0)
|
||||
#else
|
||||
#define OTA_DBG(...) do {} while (0)
|
||||
#endif
|
||||
@@ -12,7 +12,7 @@ static inline bool airohaEnterSleep(LocationProvider* nmea) {
|
||||
nmea->sendSentence("$PAIR650,0");
|
||||
if (nmea->waitFor("$PAIR001,650,0", 50)) { // wait for the command received signal
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
if (mesh::isUsbLoggingEnabled()) {
|
||||
if (mesh::isUsbDebugLoggingEnabled()) {
|
||||
mesh::usbLoggingPort().printf("Airoha RTC Backup sleep command accepted by GPS after %u attempts\r\n", attempt + 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -213,7 +213,7 @@ public :
|
||||
if (_gps_serial->available()) {
|
||||
char c = _gps_serial->read();
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
if (mesh::isUsbLoggingEnabled()) mesh::usbLoggingPort().print(c);
|
||||
if (mesh::isUsbDebugLoggingEnabled()) mesh::usbLoggingPort().print(c);
|
||||
#endif
|
||||
// MicroNMEA also returns true for a complete sentence with a
|
||||
// bad checksum. A damaged ACK must not confirm GPS sleep.
|
||||
@@ -235,7 +235,7 @@ public :
|
||||
_last_uart_ms = millis();
|
||||
_uart_seen = true;
|
||||
#ifdef GPS_NMEA_DEBUG
|
||||
if (mesh::isUsbLoggingEnabled()) mesh::usbLoggingPort().print(c);
|
||||
if (mesh::isUsbDebugLoggingEnabled()) mesh::usbLoggingPort().print(c);
|
||||
#endif
|
||||
bool parsed = nmea.process(c);
|
||||
processed++;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <helpers/UsbLogging.h>
|
||||
|
||||
#include "TouchTapDetector.h"
|
||||
|
||||
@@ -36,17 +37,18 @@ public:
|
||||
#endif
|
||||
|
||||
if (_present) {
|
||||
Serial.printf("Touch: CHSC6X found at 0x%02X\n", CHSC6X_I2C_ADDR);
|
||||
} else {
|
||||
if (mesh::isUsbDebugLoggingEnabled())
|
||||
mesh::usbDebugPort().printf("Touch: CHSC6X found at 0x%02X\n", CHSC6X_I2C_ADDR);
|
||||
} else if (mesh::isUsbDebugLoggingEnabled()) {
|
||||
// Report what is actually on the bus, so an unexpected controller or
|
||||
// address can be identified from a normal boot log.
|
||||
Serial.printf("Touch: idle or absent at 0x%02X; I2C bus currently answers:",
|
||||
mesh::usbDebugPort().printf("Touch: idle or absent at 0x%02X; I2C bus currently answers:",
|
||||
CHSC6X_I2C_ADDR);
|
||||
for (uint8_t addr = 8; addr < 0x78; addr++) {
|
||||
_wire->beginTransmission(addr);
|
||||
if (_wire->endTransmission() == 0) Serial.printf(" 0x%02X", addr);
|
||||
if (_wire->endTransmission() == 0) mesh::usbDebugPort().printf(" 0x%02X", addr);
|
||||
}
|
||||
Serial.println();
|
||||
mesh::usbDebugPort().println();
|
||||
}
|
||||
return _present;
|
||||
}
|
||||
@@ -58,7 +60,7 @@ public:
|
||||
bool pressed = readPressed();
|
||||
if (pressed && !_present) {
|
||||
_present = true; // answered late; the boot probe caught it mid-idle
|
||||
Serial.println("Touch: CHSC6X responding");
|
||||
mesh::usbDebugPort().println("Touch: CHSC6X responding");
|
||||
}
|
||||
return _detector.update(now_ms, pressed);
|
||||
}
|
||||
@@ -104,21 +106,22 @@ private:
|
||||
// frames are unconditional so an idle read that never changes is still
|
||||
// visible in the log.
|
||||
void logRaw(uint8_t got, const uint8_t* buf) {
|
||||
if (!mesh::isUsbDebugLoggingEnabled()) return;
|
||||
int16_t key = buf ? (int16_t)buf[0] : (int16_t)(-2 - (int16_t)got);
|
||||
if (key == _logged && _log_budget == 0) return;
|
||||
if (_log_budget > 0) _log_budget--;
|
||||
_logged = key;
|
||||
|
||||
if (!buf) {
|
||||
Serial.printf("Touch: short read (%u of %u bytes)\n", got, CHSC6X_READ_LEN);
|
||||
mesh::usbDebugPort().printf("Touch: short read (%u of %u bytes)\n", got, CHSC6X_READ_LEN);
|
||||
return;
|
||||
}
|
||||
Serial.printf("Touch: raw %02X %02X %02X %02X %02X", buf[0], buf[1], buf[2], buf[3], buf[4]);
|
||||
mesh::usbDebugPort().printf("Touch: raw %02X %02X %02X %02X %02X", buf[0], buf[1], buf[2], buf[3], buf[4]);
|
||||
#ifdef PIN_TOUCH_INT
|
||||
// Pulled up, so an unfitted R13 sits steady HIGH and a wired INT pulses LOW.
|
||||
Serial.printf(" INT=%d", digitalRead(PIN_TOUCH_INT));
|
||||
mesh::usbDebugPort().printf(" INT=%d", digitalRead(PIN_TOUCH_INT));
|
||||
#endif
|
||||
Serial.println();
|
||||
mesh::usbDebugPort().println();
|
||||
}
|
||||
#else
|
||||
void logRaw(uint8_t, const uint8_t*) {}
|
||||
|
||||
@@ -89,8 +89,8 @@ public:
|
||||
|
||||
#if WIFI_DEBUG_LOGGING && ARDUINO
|
||||
#include <Arduino.h>
|
||||
#define WIFI_DEBUG_PRINT(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("WiFi: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define WIFI_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbLoggingEnabled()) { mesh::usbLoggingPort().printf("WiFi: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#define WIFI_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("WiFi: " F, ##__VA_ARGS__); } } while(0)
|
||||
#define WIFI_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled()) { mesh::usbLoggingPort().printf("WiFi: " F "\n", ##__VA_ARGS__); } } while(0)
|
||||
#else
|
||||
#define WIFI_DEBUG_PRINT(...) {}
|
||||
#define WIFI_DEBUG_PRINTLN(...) {}
|
||||
|
||||
Reference in new issue
Block a user