Files
MeshCore/src/helpers/ESP32Board.h
T
mikecarper 5833ac2f1f Add compressed self-serve OTA and resilient native USB sleep
Use a bounded uzlib-derived block encoder with the existing strict tinf decoder, qualify infrastructure roles, budget temporary RAM, and cache running-image metadata. Add self-serve CLI and policy, USB host-loss sleep grace, hardware test harnesses, regression tests, and CI coverage.
2026-10-03 01:14:23 -07:00

381 lines
11 KiB
C++

#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
#if defined(ESP_PLATFORM)
#include <rom/rtc.h>
#include <sys/time.h>
#include <Wire.h>
#include "soc/rtc.h"
#include "esp_system.h"
#include <driver/rtc_io.h>
#include "ESP32TrueRandom.h"
#include "UsbLogging.h"
#include "UsbHostSleepPolicy.h"
#ifndef MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS
#define MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS 120000UL
#endif
static_assert(MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS <= 0x7fffffffUL,
"USB host-loss sleep grace must fit a bounded millis interval");
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT && \
(!defined(ARDUINO_USB_MODE) || !ARDUINO_USB_MODE)
#include <USB.h>
#endif
#if !defined(LIGHTWEIGHT_WIFI_OTA)
class AsyncWebServer;
#endif
#include <helpers/KeyValueStore.h>
class ESP32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
bool inhibit_sleep = false;
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::UsbHostSleepPolicy usb_host_sleep_policy;
#endif
#if defined(LIGHTWEIGHT_WIFI_OTA)
void* ota_server = nullptr;
#else
AsyncWebServer* ota_server = nullptr;
#endif
static inline portMUX_TYPE sleepMux = portMUX_INITIALIZER_UNLOCKED;
public:
void begin() {
// Arduino's early init hook normally captured this before initVariant().
// Keep this idempotent fallback before this class touches ADC peripherals.
mesh::initializeESP32TrueRandom();
// for future use, sub-classes SHOULD call this from their begin()
startup_reason = BD_STARTUP_NORMAL;
#ifdef ESP32_CPU_FREQ
setCpuFrequencyMhz(ESP32_CPU_FREQ);
#endif
#ifdef PIN_VBAT_READ
// battery read support
pinMode(PIN_VBAT_READ, INPUT);
#if ESP_ARDUINO_VERSION_MAJOR < 3
adcAttachPin(PIN_VBAT_READ);
#endif
#endif
#ifdef P_LORA_TX_LED
pinMode(P_LORA_TX_LED, OUTPUT);
digitalWrite(P_LORA_TX_LED, LOW);
#endif
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
#if PIN_BOARD_SDA >= 0 && PIN_BOARD_SCL >= 0
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif
#else
Wire.begin();
#endif
}
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
// Temperature from ESP32 MCU
float getMCUTemperature() override {
uint32_t raw = 0;
// To get and average the temperature so it is more accurate, especially in low temperature
for (int i = 0; i < 4; i++) {
raw += temperatureRead();
}
return raw / 4;
}
virtual void shutdownPeripherals();
virtual void powerOff() override;
virtual void enterDeepSleep(uint32_t secs) override;
uint32_t getIRQGpio() override {
#ifdef P_LORA_DIO_1
return P_LORA_DIO_1; // default for SX1262
#else
return -1;
#endif
}
void sleep(uint32_t secs) override {
// Native USB loses its connection in light sleep. An enumerated host
// still needs USB serviced when no terminal asserts CDC DTR (for example,
// after a Pi closes its serial port). Guard every caller here.
// Sample even when another blocker is active, so an open console/OTA does
// not leave the last positive native USB host signal stale.
const bool usb_host_connected = isUsbHostConnected();
bool keep_awake = inhibit_sleep || usb_host_connected || isRadioTestActive();
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
// HWCDC loses its SOF host signal after about 5 ms. A warm Pi reboot,
// especially on a USB 1.1 bus, must not put us in a 30-second light sleep
// while the host is returning and trying to enumerate this same radio.
keep_awake = keep_awake || usb_host_sleep_policy.shouldKeepAwake(
millis(), MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS);
#endif
#if MESH_USB_LOGGING_AVAILABLE
// A live logging stream must also remain available before a host opens
// it and across host disconnects. Compiled-out logging is not a blocker.
keep_awake = keep_awake || mesh::isUsbLoggingEnabled();
#endif
if (keep_awake) {
delay(1); // Keep USB and OTA tasks running.
return;
}
// Configure timer wakeup
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000ULL); // Wake up periodically to do scheduled jobs
}
const uint32_t irqGpio = getIRQGpio();
const bool radio_wakeup = irqGpio != static_cast<uint32_t>(-1);
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP && defined(PIN_USER_BTN)
const int button_gpio = PIN_USER_BTN;
#else
const int button_gpio = -1;
#endif
if (!radio_wakeup && button_gpio < 0) {
if (secs > 0) {
esp_light_sleep_start();
}
return;
}
// Set GPIO wakeup
gpio_num_t wakeupPin = (gpio_num_t)irqGpio;
// Disable CPU interrupt servicing
portENTER_CRITICAL(&sleepMux);
// Do not sleep over a pending packet or a press that needs polling.
if ((radio_wakeup && gpio_get_level(wakeupPin) == HIGH)
|| (button_gpio >= 0
&& gpio_get_level((gpio_num_t)button_gpio) == USER_BTN_PRESSED)) {
portEXIT_CRITICAL(&sleepMux);
delay(1);
return;
}
// Configure GPIO wakeup
esp_sleep_enable_gpio_wakeup();
if (radio_wakeup) {
gpio_wakeup_enable(wakeupPin, GPIO_INTR_HIGH_LEVEL);
}
if (button_gpio >= 0) {
gpio_wakeup_enable((gpio_num_t)button_gpio,
USER_BTN_PRESSED == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
}
// MCU enters light sleep
esp_light_sleep_start();
// Avoid ISR flood during wakeup due to HIGH LEVEL interrupt
if (radio_wakeup) {
gpio_wakeup_disable(wakeupPin);
gpio_set_intr_type(wakeupPin, GPIO_INTR_POSEDGE);
}
if (button_gpio >= 0) {
gpio_wakeup_disable((gpio_num_t)button_gpio);
// ESP32 MomentaryButton polls; only its sleep wake source is temporary.
gpio_set_intr_type((gpio_num_t)button_gpio, GPIO_INTR_DISABLE);
}
// Enable CPU interrupt servicing
portEXIT_CRITICAL(&sleepMux);
}
uint8_t getStartupReason() const override { return startup_reason; }
bool isUserGpioAvailable(uint8_t pin) const override;
#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off
}
#elif defined(P_LORA_TX_NEOPIXEL_LED)
#define NEOPIXEL_BRIGHTNESS 64 // white brightness (max 255)
void onBeforeTransmit() override {
neopixelWrite(P_LORA_TX_NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White)
}
void onAfterTransmit() override {
neopixelWrite(P_LORA_TX_NEOPIXEL_LED, 0, 0, 0); // turn TX neopixel off
}
#endif
uint16_t getBattMilliVolts() override {
#ifdef PIN_VBAT_READ
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 4; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 4;
return (2 * raw);
#else
return 0; // not supported
#endif
}
const char* getManufacturerName() const override {
return "Generic ESP32";
}
void reboot() override {
esp_restart();
}
bool startOTAUpdate(const char* id, char reply[], bool force_ap = false) override;
bool stopOTAUpdate(char reply[]) override;
bool isOTAUpdateRunning() const override { return ota_server != nullptr; }
bool otaFromManifest(const char* current_ver, bool dry_run, char reply[]) override;
// Heavy body (TLS + JSON + flash streaming). Runs in a dedicated large-stack task
// spawned by otaFromManifest() - public only so that task entry point can call
// it; not meant to be invoked directly.
bool otaFromManifestImpl(const char* current_ver, bool dry_run, char reply[]);
bool isUsbDataConnected() override {
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
// Role loops may never call sleep() while a CDC client is open. Keep the
// native-host signal history current here too; host detection never calls us.
(void)isUsbHostConnected();
#endif
return (bool)Serial;
#else
return false;
#endif
}
bool isUsbHostConnected() override {
bool host_connected = false;
#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
#if defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE
host_connected = Serial.isPlugged();
#elif defined(CONFIG_TINYUSB_ENABLED) && CONFIG_TINYUSB_ENABLED
host_connected = (bool)USB;
#else
host_connected = (bool)Serial;
#endif
#endif
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
usb_host_sleep_policy.observe(host_connected, millis());
#endif
// The grace is a sleep policy, not proof that a USB host is still present.
return host_connected;
}
void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
uint32_t getResetReason() const override {
return esp_reset_reason();
}
// https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32/api-reference/system/system.html
const char* getResetReasonString(uint32_t reason) {
switch (reason) {
case ESP_RST_UNKNOWN:
return "Unknown or first boot";
case ESP_RST_POWERON:
return "Power-on reset";
case ESP_RST_EXT:
return "External reset";
case ESP_RST_SW:
return "Software reset";
case ESP_RST_PANIC:
return "Panic / exception reset";
case ESP_RST_INT_WDT:
return "Interrupt watchdog reset";
case ESP_RST_TASK_WDT:
return "Task watchdog reset";
case ESP_RST_WDT:
return "Other watchdog reset";
case ESP_RST_DEEPSLEEP:
return "Wake from deep sleep";
case ESP_RST_BROWNOUT:
return "Brownout (low voltage)";
case ESP_RST_SDIO:
return "SDIO reset";
default:
static char buf[40];
snprintf(buf, sizeof(buf), "Unknown reset reason (%d)", reason);
return buf;
}
}
};
namespace mesh {
namespace esp32_clock {
extern uint32_t rtc_backup_time;
extern uint32_t rtc_backup_magic;
} // namespace esp32_clock
} // namespace mesh
#define RTC_BACKUP_MAGIC 0xAA55CC33
#define RTC_TIME_MIN 1772323200 // 1 Mar 2026
class ESP32RTCClock : public mesh::RTCClock {
public:
ESP32RTCClock() { }
void begin() {
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
return; // ESP-IDF preserves system time across deep sleep
}
// All other resets (power-on, crash, WDT, brownout) lose system time.
// Restore from RTC backup if valid, otherwise use hardcoded seed.
struct timeval tv;
if (mesh::esp32_clock::rtc_backup_magic == RTC_BACKUP_MAGIC
&& mesh::esp32_clock::rtc_backup_time > RTC_TIME_MIN) {
tv.tv_sec = mesh::esp32_clock::rtc_backup_time;
} else {
tv.tv_sec = RTC_TIME_MIN;
}
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
uint32_t getCurrentTime() override {
time_t _now;
time(&_now);
return _now;
}
void setCurrentTime(uint32_t time) override {
struct timeval tv;
tv.tv_sec = time;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
mesh::esp32_clock::rtc_backup_time = time;
mesh::esp32_clock::rtc_backup_magic = RTC_BACKUP_MAGIC;
}
void tick() override {
time_t now;
time(&now);
if (now > RTC_TIME_MIN
&& (uint32_t)now != mesh::esp32_clock::rtc_backup_time) {
mesh::esp32_clock::rtc_backup_time = (uint32_t)now;
mesh::esp32_clock::rtc_backup_magic = RTC_BACKUP_MAGIC;
}
}
};
#endif