Merge USB session safety and preserve GPS preference compatibility

This commit is contained in:
mikecarper committed 2026-10-03 16:12:32 -07:00
commit 24fcbf3247
62 files changed
+2479 -240

No files matched your search

+52 -6
View File
@@ -4,6 +4,7 @@
#include "UsbLoggingWatchdog.h"
#include <helpers/ui/DisplayPowerSettings.h>
#include "CLICommandUtils.h"
#include "GpsPowerPolicy.h"
#include "FloodAdvertCLI.h"
#include "StorageLayout.h"
#include "radiolib/RadioPowerLimits.h"
@@ -771,6 +772,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
_prefs->bridge_uart = 0;
#endif
_prefs->bridge_format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
_prefs->gps_sync_interval_hours = 0;
#ifdef WITH_MQTT_BRIDGE
bool node_prefs_needs_migration = false;
@@ -940,6 +942,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// from a previous load of a newer image.
_prefs->usb_debug_enabled = 0;
_prefs->trace_when_repeat_off = 0;
_prefs->gps_sync_interval_hours = 0;
#if defined(RP2040_PLATFORM)
File file = fs->open(filename, "r");
#else
@@ -1329,12 +1332,18 @@ 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));
if (file.available() >= (int)sizeof(_prefs->trace_when_repeat_off)) {
file.read((uint8_t *)&_prefs->trace_when_repeat_off,
sizeof(_prefs->trace_when_repeat_off));
if (file.available() >= (int)sizeof(_prefs->gps_sync_interval_hours)) {
uint16_t hours = 0;
if (file.read((uint8_t *)&hours, sizeof(hours)) == sizeof(hours)) {
_prefs->gps_sync_interval_hours = hours;
if (file.available() >= (int)sizeof(_prefs->usb_debug_enabled)) {
file.read((uint8_t *)&_prefs->usb_debug_enabled,
sizeof(_prefs->usb_debug_enabled));
if (file.available() >= (int)sizeof(_prefs->trace_when_repeat_off)) {
file.read((uint8_t *)&_prefs->trace_when_repeat_off,
sizeof(_prefs->trace_when_repeat_off));
}
}
}
}
}
@@ -1443,6 +1452,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255);
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->gps_sync_interval_hours = constrain(_prefs->gps_sync_interval_hours,
0, mesh::gps::MAX_SYNC_INTERVAL_HOURS);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
@@ -1665,6 +1676,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->gps_sync_interval_hours); // published GPS sync cadence
WRITE_COMMON_PREFS(&prefs->usb_debug_enabled); // appended USB debug intent
WRITE_COMMON_PREFS(&prefs->trace_when_repeat_off); // appended repeater trace exception
@@ -1826,6 +1838,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->gps_sync_interval_hours, sizeof(_prefs->gps_sync_interval_hours)); // published GPS sync cadence
file.write((uint8_t *)&_prefs->usb_debug_enabled, sizeof(_prefs->usb_debug_enabled)); // appended
file.write((uint8_t *)&_prefs->trace_when_repeat_off, sizeof(_prefs->trace_when_repeat_off)); // appended
@@ -3400,6 +3413,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
#if ENV_INCLUDE_GPS == 1
if (strncmp(config, "gps.sync.interval", 17) == 0
&& (config[17] == 0 || config[17] == ' ' || config[17] == '\t')) {
const char* value = config + 17;
while (*value == ' ' || *value == '\t') ++value;
uint16_t hours = 0;
if (!mesh::gps::parseSyncIntervalHours(value, hours)) {
strcpy(reply, "Error: GPS sync interval must be 1..336 hours");
} else if (_sensors->getLocationProvider() == nullptr) {
strcpy(reply, "Error: GPS unavailable");
} else {
const uint16_t previous = _prefs->gps_sync_interval_hours;
_prefs->gps_sync_interval_hours = hours;
if (!trySavePrefs()) {
_prefs->gps_sync_interval_hours = previous;
strcpy(reply, "Error: GPS sync interval could not be saved");
} else {
_sensors->applyGpsTimeSyncInterval(hours);
snprintf(reply, 160, "OK - GPS sync interval %u hours (saved)", (unsigned)hours);
}
}
return;
}
if (strncmp(config, "gps ", 4) == 0) {
if (strcmp(config + 4, "on") != 0 && strcmp(config + 4, "off") != 0) {
strcpy(reply, "Error: use set gps on|off");
@@ -4766,6 +4801,17 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
#if ENV_INCLUDE_GPS == 1
if (strcmp(config, "gps.sync.interval") == 0) {
auto* provider = _sensors->getLocationProvider();
if (provider == nullptr) {
strcpy(reply, "Error: GPS unavailable");
} else {
const uint16_t hours = provider->getTimeSyncIntervalHours();
if (hours == 0) strcpy(reply, "> default (board GPS sync policy)");
else snprintf(reply, 160, "> %u hours", (unsigned)hours);
}
return;
}
if (strcmp(config, "gps") == 0) {
char gps_command[] = "gps";
handleCommand(sender_timestamp, gps_command, reply);
+4 -1
View File
@@ -183,7 +183,7 @@ public:
// 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.
// Appended after ESP-NOW intent in /com_prefs, not beside usb_log.
// Appended after GPS sync cadence in /com_prefs, not beside usb_log.
uint8_t usb_debug_enabled = 0;
// Repeater-only opt-in for routed trace diagnostics while repeat is off.
// Appended after usb_debug in /com_prefs; existing images stay disabled.
@@ -202,6 +202,8 @@ public:
// Old preference images did not have this byte; they default to enabled to
// retain the previous combined-Full behavior after an upgrade.
uint8_t espnow_bridge_enabled = 1;
// Appended after the ESP-NOW intent. Zero keeps the board's legacy cadence.
uint16_t gps_sync_interval_hours = 0;
uint8_t retry_preset = 0;
uint8_t direct_retry_attempts = 0;
uint16_t direct_retry_base_ms = 0;
@@ -352,6 +354,7 @@ private:
void structure() override {
def("en", _parent->gps_enabled);
def("int", _parent->gps_interval);
def("sync_hours", _parent->gps_sync_interval_hours);
def("adv_loc", _parent->advert_loc_policy);
}
+16
View File
@@ -7,6 +7,22 @@ namespace gps {
static const uint32_t DEFAULT_UPDATE_INTERVAL_SEC = 1;
static const uint32_t MAX_UPDATE_INTERVAL_SEC = 24UL * 60UL * 60UL;
static const uint16_t MAX_SYNC_INTERVAL_HOURS = 14U * 24U;
// Saturate while parsing, not after an overflowing atoi/strtoul. Still inspect
// every character so an oversized value with trailing junk is never accepted.
inline bool parseSyncIntervalHours(const char* value, uint16_t& configured_hours) {
if (value == nullptr || *value == 0) return false;
uint16_t parsed = 0;
for (const char* p = value; *p; ++p) {
if (*p < '0' || *p > '9') return false;
const uint16_t next = parsed * 10U + static_cast<uint16_t>(*p - '0');
parsed = next > MAX_SYNC_INTERVAL_HOURS ? MAX_SYNC_INTERVAL_HOURS : next;
}
if (parsed == 0) return false;
configured_hours = parsed;
return true;
}
inline bool parseUpdateInterval(const char* value, uint32_t& configured_sec) {
if (value == nullptr || *value == 0) return false;
+11
View File
@@ -73,6 +73,14 @@ protected:
#endif
public:
void applyGpsTimeSyncInterval(uint16_t hours) {
#if ENV_INCLUDE_GPS
LocationProvider* location = getLocationProvider();
if (location != nullptr) location->setTimeSyncIntervalHours(hours);
#else
(void)hours;
#endif
}
struct VoltageSensorReading {
uint8_t channel;
float voltage;
@@ -108,6 +116,9 @@ public:
// for the manager's established transport ownership.
if (!gps_transport_available || !telemetryGpsDetected()) return false;
LocationProvider* location = getLocationProvider();
if (location != nullptr && location->getTimeSyncIntervalHours() != 0) {
min_interval_secs = static_cast<uint64_t>(location->getTimeSyncIntervalHours()) * 3600UL;
}
return location != nullptr && location->getGPSPowerSaving()
&& location->requestTimeSync(min_interval_secs);
#else
+154 -42
View File
@@ -17,6 +17,8 @@
#endif
#if MESH_ESP32_TINYUSB_NONBLOCKING
#include "esp32-hal-tinyusb.h"
// Build-local Arduino hook queries its actual CDC descriptor's IN endpoint.
extern "C" bool meshEsp32TinyUsbTxPending();
#endif
#if defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
#include <Adafruit_TinyUSB.h>
@@ -29,6 +31,7 @@
(defined(USE_TINYUSB) || defined(ENABLE_USB_INTERFACE) || defined(OTA_FOLDER_SERIAL))
#define MESH_NRF52_PRIMARY_USB_NONBLOCKING 1
#include <Adafruit_TinyUSB.h>
#include <nrf.h>
// Build-local CDC query installed by scripts/nrf52_usb_power_fix.py. It does
// not clear/cancel an endpoint or change its USB bulk data-toggle state.
extern "C" bool mesh_tud_cdc_n_tx_pending(uint8_t instance);
@@ -111,29 +114,50 @@ static std::atomic<bool> esp32_tinyusb_terminal_discard_pending{false};
static std::atomic<uint32_t> esp32_tinyusb_terminal_dropped_bytes{0};
static uint32_t esp32_tinyusb_terminal_reported_dropped_bytes = 0;
static uint32_t esp32_tinyusb_taken_reset_generation = 0;
static bool esp32_tinyusb_event_handler_registered = false;
static bool esp32_tinyusb_was_connected = false;
static std::atomic<bool> esp32_tinyusb_was_connected{false};
// 0: ordinary; 1: detached; 2: awaiting fresh mount; 3: preparing quarantine.
static std::atomic<uint8_t> esp32_tinyusb_reenumeration_state{0};
static std::atomic<bool> esp32_tinyusb_owner_dtr{false};
static std::atomic<uint32_t> esp32_tinyusb_reattach_after{0};
static constexpr uint32_t esp32_tinyusb_detach_millis = 20;
static bool canAccessEsp32TinyUsb(void*) {
return !xPortInIsrContext() && tud_cdc_n_connected(0)
return !xPortInIsrContext()
&& esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire) == 0
&& tud_cdc_n_connected(0)
&& esp32_tinyusb_clean_generation.load(std::memory_order_acquire)
== esp32_tinyusb_reset_generation.load(std::memory_order_acquire);
}
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);
static void requestEsp32TinyUsbReenumeration(bool force_reconnect = false) {
uint8_t expected = 0;
if (!esp32_tinyusb_reenumeration_state.compare_exchange_strong(
expected, 3, std::memory_order_acq_rel)) {
// A later watchdog stage may restart an already reattached device whose
// host never delivered a fresh mount. Owner callbacks never force this.
if (!force_reconnect || expected != 2
|| !esp32_tinyusb_reenumeration_state.compare_exchange_strong(
expected, 3, std::memory_order_acq_rel)) return;
}
// The pinned ESP32 S2/S3 DCD disconnect is one bounded soft-disconnect
// register write, not a send into TinyUSB's owner queue. Isolate an armed
// old packet immediately; only the application loop may reattach later.
(void)tud_disconnect();
esp32_tinyusb_reattach_after.store(millis() + esp32_tinyusb_detach_millis,
std::memory_order_release);
// Publish only a fully prepared detach. The other core must never connect
// before this callback has actually disconnected and started its deadline.
esp32_tinyusb_reenumeration_state.store(1, std::memory_order_release);
}
static void purgeEsp32TinyUsbRx() {
// Owner-task session hooks run before new-host RX callbacks. A fixed snapshot
// bounds this purge even if an application reader consumes bytes concurrently.
int pending = Serial.available();
while (pending-- > 0) (void)Serial.read();
// At unmount the class endpoints have already been reset. read_flush also
// rearms OUT, so it must not address an unopened/reset endpoint there.
if (tud_mounted()) tud_cdc_n_read_flush(0);
}
class Esp32TinyUsbFifoStream : public Stream {
@@ -246,18 +270,59 @@ static Esp32TinyUsbBufferedStream<false> buffered_esp32_tinyusb_terminal_port;
static void clearEsp32TinyUsbTx(void*) {
(void)tud_cdc_n_write_clear(0);
if (tud_mounted() && meshEsp32TinyUsbTxPending()) {
requestEsp32TinyUsbReenumeration();
}
}
static void closeEsp32TinyUsbSession(bool device_boundary) {
clearUsbLoggingClientActivity();
esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
// Do this synchronously in TinyUSB's owner, never in its delayed Arduino
// event handler or the main-loop cleanup (which could purge fresh RX).
purgeEsp32TinyUsbRx();
if (!device_boundary
&& !nonblocking_esp32_tinyusb_port.tryRunExclusive(clearEsp32TinyUsbTx)) {
// A writer already in flight can still arm old bytes after the close.
// Physically quarantine it instead of racing an endpoint abort/FIFO purge.
requestEsp32TinyUsbReenumeration();
}
}
static void purgeEsp32TinyUsbDetachedQueues(void*) {
purgeEsp32TinyUsbRx();
(void)tud_cdc_n_write_clear(0);
}
static void serviceEsp32TinyUsbPorts() {
if (xPortInIsrContext()) return;
if (esp32_tinyusb_queue_busy.test_and_set(std::memory_order_acquire)) return;
const uint8_t reenumeration =
esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire);
if (reenumeration != 0) {
if (reenumeration == 1
&& (int32_t)(millis() - esp32_tinyusb_reattach_after.load(
std::memory_order_acquire)) >= 0
&& (!usb_logging_watchdog_detached
|| (int32_t)(millis() - usb_logging_watchdog_attach_at) >= 0)
&& nonblocking_esp32_tinyusb_port.tryRunExclusive(
purgeEsp32TinyUsbDetachedQueues)) {
// Publish before connect: a fast owner mount callback can run immediately.
esp32_tinyusb_reenumeration_state.store(2, std::memory_order_release);
(void)tud_connect();
usb_logging_watchdog_detached = false;
}
esp32_tinyusb_queue_busy.clear(std::memory_order_release);
return; // Only the fresh owner-task mount hook can reopen this gate.
}
// Polling also catches a physical disconnect without a CDC line-state
// event. Events capture quick close/reopen pairs between service calls.
const bool connected = tud_cdc_n_connected(0);
if (esp32_tinyusb_was_connected && !connected) {
if (esp32_tinyusb_was_connected.exchange(connected,
std::memory_order_acq_rel) && !connected) {
clearUsbLoggingClientActivity();
esp32_tinyusb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
}
esp32_tinyusb_was_connected = connected;
const uint32_t generation =
esp32_tinyusb_reset_generation.load(std::memory_order_acquire);
if (generation != esp32_tinyusb_clean_generation.load(std::memory_order_acquire)) {
@@ -268,8 +333,8 @@ static void serviceEsp32TinyUsbPorts() {
esp32_tinyusb_text_queue.discardPending();
esp32_tinyusb_terminal_reported_dropped_bytes =
esp32_tinyusb_terminal_dropped_bytes.load(std::memory_order_relaxed);
// Do not purge Serial's RX queue here: the new host may already have sent
// its first query. Protocol owners reset their partial parser separately.
// The synchronous owner hook already purged old RX. Keep the new host's
// first query, while protocol owners reset their partial parser separately.
esp32_tinyusb_clean_generation.store(generation, std::memory_order_release);
}
if (esp32_tinyusb_terminal_discard_pending.exchange(false,
@@ -477,6 +542,9 @@ static constexpr uint32_t primary_usb_session_settle_millis = 8;
static std::atomic<uint32_t> primary_usb_reset_settle_until{0};
static std::atomic<bool> nrf52_usb_reenumeration_pending{false};
static std::atomic<uint32_t> nrf52_usb_reattach_after{0};
// Only the application service loop owns this phase. USB callbacks merely
// quarantine the pull-up and publish the atomic request below.
static bool nrf52_usb_stack_detach_issued = false;
static constexpr uint32_t nrf52_usb_detach_millis = 20;
static void requestNrf52UsbSessionReenumeration() {
@@ -490,9 +558,14 @@ static void requestNrf52UsbSessionReenumeration() {
std::memory_order_acq_rel)) return;
primary_usb_line_state_dtr.store(false, std::memory_order_release);
primary_usb_reset_generation.fetch_add(1, std::memory_order_acq_rel);
nrf52_usb_reattach_after.store(millis() + nrf52_usb_detach_millis,
std::memory_order_release);
TinyUSBDevice.detach();
// Match the Nordic driver's bounded physical-disconnect operation now so
// an armed packet cannot reach a rapidly reopened host. Do NOT call
// TinyUSBDevice.detach() from this CDC/SOF callback: dcd_disconnect also
// queues UNPLUGGED with an infinite FreeRTOS send timeout. A full event
// queue would then make its sole consumer wait forever on its own queue.
NRF_USBD->USBPULLUP = 0;
__ISB();
__DSB();
}
static void clearNrf52UsbTxForSession(uint8_t instance) {
@@ -503,8 +576,20 @@ static void clearNrf52UsbTxForSession(uint8_t instance) {
}
static void serviceNrf52UsbSessionReenumeration() {
if (!nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)
|| (int32_t)(millis() - nrf52_usb_reattach_after.load(
if (!nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)) return;
if (!nrf52_usb_stack_detach_issued) {
// Called only from the application loop, never a TinyUSB owner callback.
// The owner can therefore drain a full event queue while this supported
// detach operation enqueues the reset, instead of blocking on itself.
TinyUSBDevice.detach();
nrf52_usb_stack_detach_issued = true;
// Start the quiet interval at the actual stack detach, not at a possibly
// much earlier callback request while the application loop was occupied.
nrf52_usb_reattach_after.store(millis() + nrf52_usb_detach_millis,
std::memory_order_release);
return;
}
if ((int32_t)(millis() - nrf52_usb_reattach_after.load(
std::memory_order_acquire)) < 0
|| tud_mounted()) return;
#if defined(USE_TINYUSB)
@@ -519,6 +604,7 @@ static void serviceNrf52UsbSessionReenumeration() {
#if defined(USE_TINYUSB)
usb_logging_watchdog_detached = false;
#endif
nrf52_usb_stack_detach_issued = false;
nrf52_usb_reenumeration_pending.store(false, std::memory_order_release);
}
@@ -940,9 +1026,6 @@ void setUsbDebugEnabled(bool enabled) {
}
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();
@@ -950,12 +1033,9 @@ void setUsbLoggingEnabled(bool enabled) {
}
usb_logging_preference_known.store(true, std::memory_order_relaxed);
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.
// An in-flight producer is gated immediately and cleaned by the next service.
if (was_enabled && !enabled) discardUsbTerminalOutput();
#endif
// Turning diagnostics off is not a protocol switch. Retain functional
// replies in the shared text queue; actual Binary/mOTA ownership changes
// explicitly call discardUsbTerminalOutput() after their bounded barrier.
}
bool saveUsbLoggingBootPreference(bool enabled) {
@@ -994,12 +1074,6 @@ void beginUsbLoggingPort() {
// ESP32 Companion stream; setUsbLoggingEnabled() restores them only when the
// 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);
esp32_tinyusb_event_handler_registered = true;
}
#endif
#if MESH_ESP32_HWCDC_SESSION_GUARD
// If every pre-begin resize failed, begin() may have recovered by allocating
// HWCDC's built-in ring. Discover it without replacing a live ISR-owned ring.
@@ -1039,6 +1113,9 @@ void serviceUsbLoggingPort() {
// in delay(), Serial.end(), or a host-progress wait.
if (usb_logging_watchdog_detached
&& int32_t(millis() - usb_logging_watchdog_attach_at) >= 0
#if MESH_ESP32_TINYUSB_NONBLOCKING
&& esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire) == 0
#endif
#if defined(NRF52_PLATFORM)
&& !nrf52_usb_reenumeration_pending.load(std::memory_order_acquire)
&& !tud_mounted()
@@ -1561,7 +1638,7 @@ UsbLoggingRecoveryResult recoverUsbLoggingTransport(uint8_t stage) {
probeUsbLoggingTransport();
} else {
if (!usb_logging_watchdog_detached) {
if (!tud_disconnect()) return UsbLoggingRecoveryResult::Deferred;
requestEsp32TinyUsbReenumeration(true);
usb_logging_watchdog_attach_at = millis() + 50;
usb_logging_watchdog_detached = true;
}
@@ -1609,7 +1686,8 @@ bool isUsbLoggingTransportRecoveryPending() {
#elif defined(NRF52_PLATFORM) && defined(ENABLE_USB_INTERFACE)
return nrf52_usb_reenumeration_pending.load(std::memory_order_acquire);
#elif MESH_ESP32_TINYUSB_NONBLOCKING
return usb_logging_watchdog_detached;
const auto state = esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire);
return usb_logging_watchdog_detached || state == 1 || state == 3;
#elif MESH_ESP32_HWCDC_SESSION_GUARD
return esp32_hwcdc_cleanup_pending;
#else
@@ -1629,6 +1707,40 @@ extern "C" void meshTinyUsbLoggingTxComplete(uint8_t instance) {
}
#endif
#if MESH_ESP32_TINYUSB_NONBLOCKING
// These strong bridges are called synchronously by the build-local framework
// copy, not by Arduino's delayed event queue. Do not include weak declarations
// in this translation unit or these definitions would inherit that attribute.
extern "C" void meshEsp32TinyUsbTxComplete() {
mesh::noteUsbLoggingTxComplete();
}
extern "C" void meshEsp32TinyUsbCdcLineState(bool dtr) {
// Never use USBCDC::dtr here: a delayed framework unplug event can overwrite
// that member after a new host opens. These samples come from the owner.
const bool previous = mesh::esp32_tinyusb_owner_dtr.exchange(
dtr, std::memory_order_acq_rel);
if (previous != dtr) mesh::closeEsp32TinyUsbSession(dtr);
}
extern "C" bool meshEsp32TinyUsbAcceptRx() {
// A late old OUT completion while DTR is low is not fresh-host input.
return mesh::esp32_tinyusb_reenumeration_state.load(std::memory_order_acquire) == 0
&& tud_cdc_n_connected(0);
}
extern "C" void meshEsp32TinyUsbDeviceSessionBoundary(bool mounted) {
mesh::closeEsp32TinyUsbSession(true);
mesh::esp32_tinyusb_owner_dtr.store(false, std::memory_order_release);
mesh::esp32_tinyusb_was_connected.store(false, std::memory_order_release);
if (mounted) {
uint8_t expected = 2;
(void)mesh::esp32_tinyusb_reenumeration_state.compare_exchange_strong(
expected, 0, std::memory_order_acq_rel);
}
}
#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
+5 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include <ESP32_CH390.h>
#include <helpers/UsbLogging.h>
/**
* Bring up the repository's CH390 lwIP interface from the board build flags.
@@ -16,7 +17,10 @@ static inline bool beginConfiguredCH390(const char* hostname = nullptr) {
config.int_gpio = ETH_INT_PIN;
if (hostname && hostname[0] != '\0') {
if (!CH390.setHostname(hostname)) {
Serial.printf("Network: invalid Ethernet hostname %s\n", hostname);
if (mesh::isUsbDebugLoggingEnabled()
&& mesh::usbLoggingPort().availableForWrite() > 0) {
mesh::usbLoggingPort().printf("Network: invalid Ethernet hostname %s\n", hostname);
}
}
}
if (!CH390.begin(config)) return false;
@@ -419,7 +419,6 @@ class RAK12500LocationProvider : public LocationProvider {
unsigned long _next_time_check = 0;
unsigned long _last_time_sync = 0;
uint8_t _valid_time_samples = 0;
static const unsigned long TIME_SYNC_INTERVAL = 1800000;
int _pin_en = -1;
public:
void setRTCClock(mesh::RTCClock* clock) { _clock = clock; }
@@ -480,7 +479,8 @@ public:
}
if (!_time_sync_needed && _clock != NULL
&& (unsigned long)(now - _last_time_sync) > TIME_SYNC_INTERVAL) {
&& static_cast<uint32_t>(now - _last_time_sync)
>= periodicTimeSyncIntervalMillis()) {
_time_sync_needed = true;
}
if (_time_sync_needed && _clock != NULL && _valid_time_samples > 2) {
@@ -1401,7 +1401,7 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin,
void EnvironmentSensorManager::armGpsPowerSavingCycle() {
if (!powersaving_enabled || !_location->getGPSPowerSaving()) return;
_location->syncTime();
_location->syncTimeForPowerSavingCycle();
_location->setNextGPSOn(0);
_location->setNextSleep();
}
+39 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "Mesh.h"
#include <helpers/GpsPowerPolicy.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
@@ -22,6 +23,7 @@ protected:
unsigned long _next_gps_on = 0;
unsigned long _gps_on_duration_secs = GPS_POWERSAVING_ON_DURATION_SECS;
unsigned long _gps_off_duration_secs = GPS_POWERSAVING_OFF_DURATION_SECS;
uint16_t _gps_sync_interval_hours = 0; // 0 retains the board's legacy policy
unsigned long _last_valid_time_sync = 0;
uint32_t _last_time_sync_request_ms = 0;
uint32_t _last_time_sync_applied_ms = 0;
@@ -41,6 +43,25 @@ protected:
}
public:
void setTimeSyncIntervalHours(uint16_t hours) {
_gps_sync_interval_hours = hours > mesh::gps::MAX_SYNC_INTERVAL_HOURS
? mesh::gps::MAX_SYNC_INTERVAL_HOURS : hours;
// A sleeping receiver must adopt a shorter/longer setting now, not
// wait for its old (possibly week-long) deadline. No forced GPS wake.
if (_next_gps_on != 0) setNextWake();
}
uint16_t getTimeSyncIntervalHours() const { return _gps_sync_interval_hours; }
uint32_t periodicTimeSyncIntervalMillis() const {
return _gps_sync_interval_hours != 0
? static_cast<uint32_t>(_gps_sync_interval_hours) * 3600000UL
: 1800000UL; // existing always-on receiver policy: 30 minutes
}
// Position telemetry may power the receiver between scheduled clock
// acquisitions. Do not turn that into an early clock-sync request.
void syncTimeForPowerSavingCycle() {
if (_gps_sync_interval_hours == 0) syncTime();
else (void)requestTimeSync(static_cast<uint64_t>(_gps_sync_interval_hours) * 3600UL);
}
virtual void syncTime() { _time_sync_needed = true; }
virtual bool waitingTimeSync() { return _time_sync_needed; }
// Telemetry requests share an in-progress acquisition and rate-limit new
@@ -95,7 +116,24 @@ public:
virtual void enablePowerSaving(bool enabled) { setGPSPowerSaving(enabled); }
virtual bool isPowerSavingEnabled() { return getGPSPowerSaving(); }
virtual void setNextWake() {
setNextGPSOn(millis() + _gps_off_duration_secs * 1000UL);
const uint32_t now = static_cast<uint32_t>(millis());
uint32_t wait_ms = _gps_off_duration_secs * 1000UL;
if (_gps_sync_interval_hours != 0) {
wait_ms = periodicTimeSyncIntervalMillis();
// Anchor to clock acquisition, not an unrelated location query's
// power-off. Repeated position queries must not postpone sync.
uint32_t age = UINT32_MAX;
if (_time_sync_applied_seen) age = now - _last_time_sync_applied_ms;
if (_time_sync_request_seen) {
const uint32_t request_age = now - _last_time_sync_request_ms;
if (request_age < age) age = request_age;
}
if (age < wait_ms) wait_ms -= age;
// A failed/timed-out acquisition gets a full interval backoff,
// rather than immediately powering back on in a retry loop.
}
const uint32_t deadline = now + wait_ms;
setNextGPSOn(deadline == 0 ? 1 : deadline); // 0 means no deadline
}
virtual unsigned long getNextWake() { return getNextGPSOn(); }
virtual void setNextSleep() {
@@ -55,7 +55,6 @@ class MicroNMEALocationProvider : public LocationProvider {
unsigned long next_check = 0;
long time_valid = 0;
unsigned long _last_time_sync = 0;
static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes
#ifdef GPS_L76K
void configureL76K() {
@@ -269,7 +268,9 @@ public :
if ((long)(millis() - next_check) > 0) {
next_check = millis() + 1000;
// Re-enable time sync periodically when GPS has valid fix
if (!_time_sync_needed && _clock != NULL && (millis() - _last_time_sync) > TIME_SYNC_INTERVAL) {
if (!_time_sync_needed && _clock != NULL
&& static_cast<uint32_t>(millis() - _last_time_sync)
>= periodicTimeSyncIntervalMillis()) {
_time_sync_needed = true;
}
const long timestamp = getTimestamp();
+39 -11
View File
@@ -2,6 +2,8 @@
#include <Arduino.h>
#include <Wire.h>
#include <stdarg.h>
#include <stdio.h>
#include <helpers/UsbLogging.h>
#include "TouchTapDetector.h"
@@ -37,18 +39,23 @@ public:
#endif
if (_present) {
if (mesh::isUsbDebugLoggingEnabled())
mesh::usbDebugPort().printf("Touch: CHSC6X found at 0x%02X\n", CHSC6X_I2C_ADDR);
logDiagnostic("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.
mesh::usbDebugPort().printf("Touch: idle or absent at 0x%02X; I2C bus currently answers:",
CHSC6X_I2C_ADDR);
char record[640];
size_t length = snprintf(record, sizeof(record),
"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) mesh::usbDebugPort().printf(" 0x%02X", addr);
if (_wire->endTransmission() == 0 && length + 6 < sizeof(record)) {
length += snprintf(record + length, sizeof(record) - length,
" 0x%02X", addr);
}
}
mesh::usbDebugPort().println();
record[length++] = '\n';
writeDiagnostic(record, length);
}
return _present;
}
@@ -60,12 +67,31 @@ public:
bool pressed = readPressed();
if (pressed && !_present) {
_present = true; // answered late; the boot probe caught it mid-idle
mesh::usbDebugPort().println("Touch: CHSC6X responding");
logDiagnostic("Touch: CHSC6X responding\n");
}
return _detector.update(now_ms, pressed);
}
private:
static void writeDiagnostic(const char* data, size_t length) {
if (!mesh::isUsbDebugLoggingEnabled()) return;
Stream& output = mesh::usbLoggingPort();
if (output.availableForWrite() < static_cast<int>(length)) return;
output.write(reinterpret_cast<const uint8_t*>(data), length);
}
static void logDiagnostic(const char* format, ...) {
if (!mesh::isUsbDebugLoggingEnabled()) return;
char record[128];
va_list args;
va_start(args, format);
const int length = vsnprintf(record, sizeof(record), format, args);
va_end(args);
if (length > 0 && static_cast<size_t>(length) < sizeof(record)) {
writeDiagnostic(record, static_cast<size_t>(length));
}
}
TwoWire* _wire = NULL;
bool _present = false;
TouchTapDetector _detector;
@@ -113,15 +139,17 @@ private:
_logged = key;
if (!buf) {
mesh::usbDebugPort().printf("Touch: short read (%u of %u bytes)\n", got, CHSC6X_READ_LEN);
logDiagnostic("Touch: short read (%u of %u bytes)\n", got, CHSC6X_READ_LEN);
return;
}
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.
mesh::usbDebugPort().printf(" INT=%d", digitalRead(PIN_TOUCH_INT));
logDiagnostic("Touch: raw %02X %02X %02X %02X %02X INT=%d\n",
buf[0], buf[1], buf[2], buf[3], buf[4], digitalRead(PIN_TOUCH_INT));
#else
logDiagnostic("Touch: raw %02X %02X %02X %02X %02X\n",
buf[0], buf[1], buf[2], buf[3], buf[4]);
#endif
mesh::usbDebugPort().println();
}
#else
void logRaw(uint8_t, const uint8_t*) {}