diff --git a/lib/Logging/Logging.h b/lib/Logging/Logging.h index 8784a341..6ad17323 100644 --- a/lib/Logging/Logging.h +++ b/lib/Logging/Logging.h @@ -27,7 +27,10 @@ won't trigger deprecation warnings. #define LOG_LEVEL 0 #endif -static HWCDC& logSerial = Serial; +// The concrete Serial type differs by MCU: HWCDC (native USB CDC on C3/S3) vs +// HardwareSerial (UART0 on the classic ESP32 / M5Paper). Bind to whatever the +// real Serial is here — this is before the `#define Serial` shim below. +static decltype(Serial)& logSerial = Serial; void logPrintf(const char* level, const char* origin, const char* format, ...); diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index b150dc8c..41026ad0 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -192,7 +192,13 @@ HalGPIO::DeviceType detectDeviceTypeWithFingerprint() { void HalGPIO::begin() { inputMgr.begin(); +#if !(defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER) + // Claim the shared SPI bus with the X4/X3 display+SD pins. On M5Paper the SDK's + // IT8951 driver and SDCardManager each bring up SPI from BoardConfig::ACTIVE + // pins; pre-claiming VSPI here would stick (SPIClass::begin early-returns if the + // bus is already started) and leave the SD card on the wrong pins. SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); +#endif _deviceType = detectDeviceTypeWithFingerprint(); @@ -232,12 +238,29 @@ void HalGPIO::startDeepSleep() { inputMgr.update(); } // Arm the wakeup trigger *after* the button is released +#if SOC_PM_SUPPORT_EXT1_WAKEUP + // Xtensa (classic ESP32 / S3): GPIO deep-sleep wake is via RTC ext1. + esp_sleep_enable_ext1_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_EXT1_WAKEUP_ALL_LOW); +#else + // RISC-V (C3): direct GPIO deep-sleep wakeup source. esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); +#endif // Enter Deep Sleep esp_deep_sleep_start(); } void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) { + if (BoardConfig::ACTIVE.input.power < 0) { + // No readable power-button input pin: can't verify a hold, so don't sleep. + return; + } +#if defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER + // M5Paper: power-on is a hardware latch and the "power button" is the rotary + // push (G38), shared with Confirm. A USB/flash cold boot is indistinguishable + // from an intentional power-on hold here, so skip the X4-style anti-accidental- + // wake check and always boot. G38 still serves as the deep-sleep wake source. + return; +#endif if (shortPressAllowed) { // Fast path - no duration check needed return; diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp index 78f0018f..dec361dc 100644 --- a/lib/hal/HalPowerManager.cpp +++ b/lib/hal/HalPowerManager.cpp @@ -18,7 +18,9 @@ void HalPowerManager::begin() { Wire.setTimeOut(4); _batteryUseI2C = true; } else { - pinMode(BAT_GPIO0, INPUT); + // Battery ADC pin from the active board profile (X4: GPIO0, M5Paper: GPIO35), + // not a hardcoded X4 pin. + pinMode(BoardConfig::ACTIVE.batteryAdc, INPUT); } normalFreq = getCpuFrequencyMhz(); modeMutex = xSemaphoreCreateMutex(); @@ -76,8 +78,10 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { #endif // Pre-sleep routines from the original firmware +#if !SOC_PM_SUPPORT_EXT1_WAKEUP // RISC-V (C3 / X4) // GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep // Note that this means the MCU will be completely powered off during sleep, including RTC + // (X4-specific: on classic ESP32/M5Paper GPIO13 is SD MISO, so this is skipped.) constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13; gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT); gpio_set_level(GPIO_SPIWP, 0); @@ -90,6 +94,10 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { // power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source // configuration esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); +#else // Xtensa (classic ESP32 / S3): RTC ext1 GPIO wake + pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP); + esp_sleep_enable_ext1_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_EXT1_WAKEUP_ALL_LOW); +#endif // Enter Deep Sleep esp_deep_sleep_start(); } @@ -121,7 +129,8 @@ uint16_t HalPowerManager::getBatteryPercentage() const { _batteryLastPollMs = now; return _batteryCachedPercent; } - static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); + // ADC pin from the active profile (X4 GPIO0 / M5Paper GPIO35); default 2:1 divider. + static const BatteryMonitor battery = BatteryMonitor(BoardConfig::ACTIVE.batteryAdc); // smooth the battery %. if (_batteryCachedPercent == 0) { diff --git a/lib/hal/HalSystem.cpp b/lib/hal/HalSystem.cpp index b92c95a1..c806c030 100644 --- a/lib/hal/HalSystem.cpp +++ b/lib/hal/HalSystem.cpp @@ -38,6 +38,13 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) { __real_panic_print_backtrace(frame, core); return; } +#if !__riscv + // RvExcFrame and the flat SP scan below are RISC-V-only (C3). On Xtensa + // (classic ESP32 / S3) the exception frame is XtExcFrame with windowed-register + // unwinding, so fall back to the default backtrace. + __real_panic_print_backtrace(frame, core); + return; +#else for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) { panicStack[i].sp = 0; } @@ -65,6 +72,7 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) { } __real_panic_print_backtrace(frame, core); +#endif // __riscv } } diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 524ed3e0..0cf672c3 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -18,6 +18,13 @@ #include "settings/SettingsActivity.h" #include "util/FullScreenMessageActivity.h" +// taskENTER_CRITICAL needs a real spinlock on dual-core targets (classic ESP32, +// e.g. M5Paper). On the single-core ESP32-C3 a nullptr mux was tolerated, but +// the dual-core port acquires an inter-core spinlock and asserts on a null +// pointer (spinlock_acquire, spinlock.h:84). One shared spinlock guards the +// short waitingTaskHandle critical sections below. +static portMUX_TYPE activityMux = portMUX_INITIALIZER_UNLOCKED; + void ActivityManager::begin() { xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender", 8192, // Stack size @@ -45,10 +52,10 @@ void ActivityManager::renderTaskLoop() { } // Notify any task blocked in requestUpdateAndWait() that the render is done. TaskHandle_t waiter = nullptr; - taskENTER_CRITICAL(nullptr); + taskENTER_CRITICAL(&activityMux); waiter = waitingTaskHandle; waitingTaskHandle = nullptr; - taskEXIT_CRITICAL(nullptr); + taskEXIT_CRITICAL(&activityMux); if (waiter) { xTaskNotify(waiter, 1, eIncrement); } @@ -279,7 +286,7 @@ void ActivityManager::requestUpdateAndWait() { } // Atomic section to perform checks - taskENTER_CRITICAL(nullptr); + taskENTER_CRITICAL(&activityMux); auto currTaskHandler = xTaskGetCurrentTaskHandle(); auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex); bool isRenderTask = (currTaskHandler == renderTaskHandle); @@ -288,7 +295,7 @@ void ActivityManager::requestUpdateAndWait() { if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) { waitingTaskHandle = currTaskHandler; } - taskEXIT_CRITICAL(nullptr); + taskEXIT_CRITICAL(&activityMux); // Render task cannot call requestUpdateAndWait() or it will cause a deadlock assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()"); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 87d5af23..41f273bd 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -132,8 +132,27 @@ void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const si renderer.drawCenteredText(UI_10_FONT_ID, rect.y + rect.height + 15, percentText.c_str()); } +// CROSSPOINT_SHOW_BUTTON_HINTS: build-flag switch (env). Default on. A device with +// no physical button row (touch + rotary, e.g. M5Paper) builds with =0. +#ifndef CROSSPOINT_SHOW_BUTTON_HINTS +#define CROSSPOINT_SHOW_BUTTON_HINTS 1 +#endif +bool BaseTheme::showButtonHints() { return CROSSPOINT_SHOW_BUTTON_HINTS; } + +// Non-virtual gates: one check hides hints across every theme. Themes override the +// *Impl, which only ever runs when hints are enabled. void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const { + if (!showButtonHints()) return; + drawButtonHintsImpl(renderer, btn1, btn2, btn3, btn4); +} +void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { + if (!showButtonHints()) return; + drawSideButtonHintsImpl(renderer, topBtn, bottomBtn); +} + +void BaseTheme::drawButtonHintsImpl(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const { const GfxRenderer::Orientation orig_orientation = renderer.getOrientation(); renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -163,7 +182,7 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c renderer.setOrientation(orig_orientation); } -void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { +void BaseTheme::drawSideButtonHintsImpl(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { const int screenWidth = renderer.getScreenWidth(); constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated) constexpr int buttonHeight = 80; // Height on screen (width when rotated) diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index e140bc26..75c6d1be 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -179,9 +179,19 @@ class BaseTheme { void drawBatteryRight(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const; // Right aligned (UI headers) virtual void fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const; - virtual void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, - const char* btn4) const; - virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const; + // Single switch for on-screen button hints, set by the CROSSPOINT_SHOW_BUTTON_HINTS + // build flag (env; default on). Devices with no physical button row (touch + + // rotary, e.g. M5Paper) set it to 0 in their env. + static bool showButtonHints(); + // Public, NON-virtual gate: returns early when hints are disabled, otherwise + // dispatches to the per-theme *Impl below. Themes override the *Impl, never + // these — so one switch hides hints across every theme. + void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const; + void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const; + virtual void drawButtonHintsImpl(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const; + virtual void drawSideButtonHintsImpl(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const; virtual int getListPageItems(int contentHeight, bool hasSubtitle) const; virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index f04f7de0..a39d0f5e 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -317,8 +317,8 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } } -void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, - const char* btn4) const { +void LyraTheme::drawButtonHintsImpl(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const { const GfxRenderer::Orientation orig_orientation = renderer.getOrientation(); renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -356,7 +356,7 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c renderer.setOrientation(orig_orientation); } -void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { +void LyraTheme::drawSideButtonHintsImpl(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { const int screenWidth = renderer.getScreenWidth(); constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated) constexpr int buttonHeight = 78; // Height on screen (width when rotated) diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index fbed8f42..0cfb3bb0 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -84,9 +84,9 @@ class LyraTheme : public BaseTheme { const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue, bool highlightValue, const std::function& rowDimmed = nullptr) const override; - void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, - const char* btn4) const override; - void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override; + void drawButtonHintsImpl(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const override; + void drawSideButtonHintsImpl(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override; void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const override; diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.cpp b/src/components/themes/roundedraff/RoundedRaffTheme.cpp index 50db2a8c..4a46f459 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.cpp +++ b/src/components/themes/roundedraff/RoundedRaffTheme.cpp @@ -381,8 +381,8 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item drawScrollBar(renderer, rect, itemCount, pageStartIndex, pageItems); } -void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, - const char* btn4) const { +void RoundedRaffTheme::drawButtonHintsImpl(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const { const GfxRenderer::Orientation origOrientation = renderer.getOrientation(); renderer.setOrientation(GfxRenderer::Orientation::Portrait); diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.h b/src/components/themes/roundedraff/RoundedRaffTheme.h index 3f916a26..5de7f138 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.h +++ b/src/components/themes/roundedraff/RoundedRaffTheme.h @@ -92,7 +92,7 @@ class RoundedRaffTheme : public BaseTheme { const std::function& rowIcon = nullptr, const std::function& rowValue = nullptr, bool highlightValue = false, const std::function& rowDimmed = nullptr) const override; - void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, - const char* btn4) const override; + void drawButtonHintsImpl(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const override; bool homeMenuShowsContinueReading() const { return true; } }; diff --git a/src/main.cpp b/src/main.cpp index cb1d9b23..9c71ba13 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -305,6 +305,14 @@ void setupDisplayAndFonts(bool seamless = false) { void setup() { t1 = millis(); +#if defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER + // M5Paper v1.1 latches its own power through a MOSFET on GPIO2. Drive it HIGH + // first thing or the board powers off the instant USB is unplugged. (Board + // responsibility, not the SDK — see freeink-sdk platformio.sample.ini.) + pinMode(2, OUTPUT); + digitalWrite(2, HIGH); +#endif + #ifdef ENABLE_SERIAL_LOG // Earliest possible Serial setup. The 250 ms stall before begin() lets the // USB Serial/JTAG peripheral finish power-on and lets the host complete USB @@ -313,7 +321,11 @@ void setup() { // worked without the delay because USB was already enumerated. delay(250); Serial.begin(115200); +#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT + // setTxTimeoutMs is an HWCDC (native USB CDC) method. On the classic ESP32 + // (M5Paper) Serial is UART0/HardwareSerial, which has no such method. logSerial.setTxTimeoutMs(1); // This is a load-bearing 1. Do not modify. +#endif #endif HalSystem::begin();