diff --git a/Devices/m5stack-papers3/CMakeLists.txt b/Devices/m5stack-papers3/CMakeLists.txt index 87c39fcc8..5f39ba46a 100644 --- a/Devices/m5stack-papers3/CMakeLists.txt +++ b/Devices/m5stack-papers3/CMakeLists.txt @@ -2,5 +2,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register(SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES FastEpdDisplay GT911 TactilityCore + REQUIRES EPDiyDisplay GT911 TactilityCore driver EstimatedPower ) diff --git a/Devices/m5stack-papers3/Source/Configuration.cpp b/Devices/m5stack-papers3/Source/Configuration.cpp index f6c53e556..3754d1437 100644 --- a/Devices/m5stack-papers3/Source/Configuration.cpp +++ b/Devices/m5stack-papers3/Source/Configuration.cpp @@ -1,19 +1,22 @@ #include "devices/Display.h" #include "devices/SdCard.h" +#include "devices/Power.h" #include using namespace tt::hal; +bool initBoot(); + static DeviceVector createDevices() { - auto touch = createTouch(); return { + createPower(), + createDisplay(), createSdCard(), - createDisplay(touch) }; } extern const Configuration hardwareConfiguration = { - .initBoot = nullptr, + .initBoot = initBoot, .createDevices = createDevices }; diff --git a/Devices/m5stack-papers3/Source/Init.cpp b/Devices/m5stack-papers3/Source/Init.cpp new file mode 100644 index 000000000..2b6c6c891 --- /dev/null +++ b/Devices/m5stack-papers3/Source/Init.cpp @@ -0,0 +1,48 @@ +#include +#include + +static const auto LOGGER = tt::Logger("Paper S3"); + +constexpr gpio_num_t VBAT_PIN = GPIO_NUM_3; +constexpr gpio_num_t CHARGE_STATUS_PIN = GPIO_NUM_4; +constexpr gpio_num_t USB_DETECT_PIN = GPIO_NUM_5; + +static bool powerOn() { + if (gpio_reset_pin(CHARGE_STATUS_PIN) != ESP_OK) { + LOGGER.error("Failed to reset CHARGE_STATUS_PIN"); + return false; + } + + if (gpio_set_direction(CHARGE_STATUS_PIN, GPIO_MODE_INPUT) != ESP_OK) { + LOGGER.error("Failed to set direction for CHARGE_STATUS_PIN"); + return false; + } + + if (gpio_reset_pin(USB_DETECT_PIN) != ESP_OK) { + LOGGER.error("Failed to reset USB_DETECT_PIN"); + return false; + } + + if (gpio_set_direction(USB_DETECT_PIN, GPIO_MODE_INPUT) != ESP_OK) { + LOGGER.error("Failed to set direction for USB_DETECT_PIN"); + return false; + } + + // VBAT_PIN is used as ADC input; only reset it here to clear any previous + // configuration. The ADC driver (ChargeFromAdcVoltage) configures it for ADC use. + if (gpio_reset_pin(VBAT_PIN) != ESP_OK) { + LOGGER.error("Failed to reset VBAT_PIN"); + return false; + } + return true; +} + +bool initBoot() { + LOGGER.info("Power on"); + if (!powerOn()) { + LOGGER.error("Power on failed"); + return false; + } + + return true; +} diff --git a/Devices/m5stack-papers3/Source/devices/Display.cpp b/Devices/m5stack-papers3/Source/devices/Display.cpp index af02b558d..42cc2f7fb 100644 --- a/Devices/m5stack-papers3/Source/devices/Display.cpp +++ b/Devices/m5stack-papers3/Source/devices/Display.cpp @@ -1,34 +1,23 @@ #include "Display.h" #include -#include -#include +#include std::shared_ptr createTouch() { auto configuration = std::make_unique( I2C_NUM_0, 540, 960, - false, // swapXy - false, // mirrorX + true, // swapXy + true, // mirrorX false, // mirrorY GPIO_NUM_NC, // pinReset - GPIO_NUM_NC // pinInterrupt + GPIO_NUM_NC //48 pinInterrupt ); - auto touch = std::make_shared(std::move(configuration)); - return std::static_pointer_cast(touch); + return std::make_shared(std::move(configuration)); } -std::shared_ptr createDisplay(std::shared_ptr touch) { - FastEpdDisplay::Configuration configuration = { - .horizontalResolution = 540, - .verticalResolution = 960, - .touch = std::move(touch), - .busSpeedHz = 20000000, - .rotationDegrees = 90, - .use4bppGrayscale = false, - .fullRefreshEveryNFlushes = 40, - }; - - return std::make_shared(configuration, tt::lvgl::getSyncLock()); +std::shared_ptr createDisplay() { + auto touch = createTouch(); + return EpdiyDisplayHelper::createM5PaperS3Display(touch); } diff --git a/Devices/m5stack-papers3/Source/devices/Display.h b/Devices/m5stack-papers3/Source/devices/Display.h index bac7fd008..13a1fe144 100644 --- a/Devices/m5stack-papers3/Source/devices/Display.h +++ b/Devices/m5stack-papers3/Source/devices/Display.h @@ -1,7 +1,6 @@ #pragma once +#include #include -#include -std::shared_ptr createTouch(); -std::shared_ptr createDisplay(std::shared_ptr touch); +std::shared_ptr createDisplay(); diff --git a/Devices/m5stack-papers3/Source/devices/Power.cpp b/Devices/m5stack-papers3/Source/devices/Power.cpp new file mode 100644 index 000000000..7f853fd4a --- /dev/null +++ b/Devices/m5stack-papers3/Source/devices/Power.cpp @@ -0,0 +1,282 @@ +#include "Power.h" + +#include +#include +#include +#include + +using namespace tt::hal::power; + +constexpr auto* TAG = "PaperS3Power"; + +// M5Stack PaperS3 hardware pin definitions +constexpr gpio_num_t VBAT_PIN = GPIO_NUM_3; // Battery voltage with 2x divider +constexpr adc_channel_t VBAT_ADC_CHANNEL = ADC_CHANNEL_2; // GPIO3 = ADC1_CHANNEL_2 + +constexpr gpio_num_t CHARGE_STATUS_PIN = GPIO_NUM_4; // Charge IC status: 0 = charging, 1 = full/no USB +constexpr gpio_num_t USB_DETECT_PIN = GPIO_NUM_5; // USB detect: 1 = USB connected +constexpr gpio_num_t POWER_OFF_PIN = GPIO_NUM_44; // Pull high to trigger shutdown +constexpr gpio_num_t BUZZER_PIN = GPIO_NUM_21; + +// Battery voltage divider ratio (voltage is divided by 2) +constexpr float VOLTAGE_DIVIDER_MULTIPLIER = 2.0f; + +// Battery voltage range for LiPo batteries +constexpr float MIN_BATTERY_VOLTAGE = 3.3f; +constexpr float MAX_BATTERY_VOLTAGE = 4.2f; + +// Power-off signal timing +constexpr int POWER_OFF_PULSE_COUNT = 5; +constexpr int POWER_OFF_PULSE_DURATION_MS = 100; + +constexpr uint32_t BUZZER_DUTY_50_PERCENT = 4096; // 50% of 13-bit (8192) + +PaperS3Power::PaperS3Power( + std::unique_ptr chargeFromAdcVoltage, + gpio_num_t powerOffPin +) + : chargeFromAdcVoltage(std::move(chargeFromAdcVoltage)), + powerOffPin(powerOffPin) { + LOG_I(TAG, "Initialized M5Stack PaperS3 power management"); +} + +void PaperS3Power::buzzerLedcInit() { + if (buzzerInitialized) { + LOG_I(TAG, "Buzzer already initialized"); + return; + } + + ledc_timer_config_t timer_cfg = { + .speed_mode = LEDC_LOW_SPEED_MODE, + .duty_resolution = LEDC_TIMER_13_BIT, + .timer_num = LEDC_TIMER_0, + .freq_hz = 1000, + .clk_cfg = LEDC_AUTO_CLK, + .deconfigure = false + }; + esp_err_t err = ledc_timer_config(&timer_cfg); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC timer config failed: %s", esp_err_to_name(err)); + return; + } + + ledc_channel_config_t channel_cfg = { + .gpio_num = BUZZER_PIN, + .speed_mode = LEDC_LOW_SPEED_MODE, + .channel = LEDC_CHANNEL_0, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = LEDC_TIMER_0, + .duty = 0, + .hpoint = 0, + .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, + .flags = { + .output_invert = 0 + } + }; + err = ledc_channel_config(&channel_cfg); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC channel config failed: %s", esp_err_to_name(err)); + return; + } + + buzzerInitialized = true; +} + +void PaperS3Power::initializePowerOff() { + if (powerOffInitialized) { + return; + } + + gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << powerOffPin), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + esp_err_t err = gpio_config(&io_conf); + if (err != ESP_OK) { + LOG_E(TAG, "Failed to configure power-off pin GPIO%d: %s", powerOffPin, esp_err_to_name(err)); + return; + } + + gpio_set_level(powerOffPin, 0); + powerOffInitialized = true; + LOG_I(TAG, "Power-off control initialized on GPIO%d", powerOffPin); + + buzzerLedcInit(); +} + +// TODO: Fix USB Detection +bool PaperS3Power::isUsbConnected() { + // USB_DETECT_PIN is configured as input with pull-down by initBoot() in Init.cpp. + // Level 1 = USB VBUS present (per M5PaperS3 hardware spec). + bool usbConnected = gpio_get_level(USB_DETECT_PIN) == 1; + LOG_D(TAG, "USB_STATUS(GPIO%d)=%d", USB_DETECT_PIN, (int)usbConnected); + return usbConnected; +} + +bool PaperS3Power::isCharging() { + // CHARGE_STATUS_PIN is configured as GPIO_MODE_INPUT by initBoot() in Init.cpp. + int chargePin = gpio_get_level(CHARGE_STATUS_PIN); + LOG_D(TAG, "CHG_STATUS(GPIO%d)=%d", CHARGE_STATUS_PIN, chargePin); + return chargePin == 0; +} + +bool PaperS3Power::supportsMetric(MetricType type) const { + switch (type) { + using enum MetricType; + case BatteryVoltage: + case ChargeLevel: + case IsCharging: + return true; + default: + return false; + } +} + +bool PaperS3Power::getMetric(MetricType type, MetricData& data) { + switch (type) { + using enum MetricType; + + case BatteryVoltage: + return chargeFromAdcVoltage->readBatteryVoltageSampled(data.valueAsUint32); + + case ChargeLevel: { + uint32_t voltage = 0; + if (chargeFromAdcVoltage->readBatteryVoltageSampled(voltage)) { + data.valueAsUint8 = chargeFromAdcVoltage->estimateChargeLevelFromVoltage(voltage); + return true; + } + return false; + } + + case IsCharging: + // isUsbConnected() is tracked separately but not used as a gate here: + // when USB is absent the charge IC's CHG pin is inactive (high), so + // isCharging() already returns false correctly. + data.valueAsBool = isCharging(); + return true; + + default: + return false; + } +} + +void PaperS3Power::toneOn(int frequency, int duration) { + if (!buzzerInitialized) { + LOG_I(TAG, "Buzzer not initialized"); + return; + } + + if (frequency <= 0) { + LOG_I(TAG, "Invalid frequency: %d", frequency); + return; + } + + esp_err_t err = ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, frequency); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC set freq failed: %s", esp_err_to_name(err)); + return; + } + + err = ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, BUZZER_DUTY_50_PERCENT); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC set duty failed: %s", esp_err_to_name(err)); + return; + } + + err = ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC update duty failed: %s", esp_err_to_name(err)); + return; + } + + if (duration > 0) { + vTaskDelay(pdMS_TO_TICKS(duration)); + toneOff(); + } +} + +void PaperS3Power::toneOff() { + if (!buzzerInitialized) { + LOG_I(TAG, "Buzzer not initialized"); + return; + } + + esp_err_t err = ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC set duty failed: %s", esp_err_to_name(err)); + return; + } + + err = ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); + if (err != ESP_OK) { + LOG_E(TAG, "LEDC update duty failed: %s", esp_err_to_name(err)); + return; + } +} + +void PaperS3Power::powerOff() { + LOG_W(TAG, "Power-off requested"); + // Note: callers are responsible for stopping the display (e.g. EPD refresh) before + // calling powerOff(). The beep sequence below (~500 ms) provides some lead time, + // but a full EPD refresh can take up to ~1500 ms. If a refresh is still in flight + // when GPIO44 cuts power, the current frame will be incomplete; the display will + // recover correctly on next boot via a full-screen clear. + + if (!powerOffInitialized) { + initializePowerOff(); + if (!powerOffInitialized) { + LOG_E(TAG, "Power-off failed: GPIO not initialized"); + return; + } + } + + //beep on + toneOn(440, 200); + vTaskDelay(pdMS_TO_TICKS(100)); + //beep on + toneOn(440, 200); + + LOG_W(TAG, "Triggering shutdown via GPIO%d (sending %d pulses)...", powerOffPin, POWER_OFF_PULSE_COUNT); + + for (int i = 0; i < POWER_OFF_PULSE_COUNT; i++) { + gpio_set_level(powerOffPin, 1); + vTaskDelay(pdMS_TO_TICKS(POWER_OFF_PULSE_DURATION_MS)); + gpio_set_level(powerOffPin, 0); + vTaskDelay(pdMS_TO_TICKS(POWER_OFF_PULSE_DURATION_MS)); + } + + gpio_set_level(powerOffPin, 1); // Final high state + + LOG_W(TAG, "Shutdown signal sent. Waiting for power-off..."); + vTaskDelay(pdMS_TO_TICKS(1000)); + LOG_E(TAG, "Device did not power off as expected"); +} + +std::shared_ptr createPower() { + ChargeFromAdcVoltage::Configuration config = { + .adcMultiplier = VOLTAGE_DIVIDER_MULTIPLIER, + .adcRefVoltage = 3.3f, + .adcChannel = VBAT_ADC_CHANNEL, + .adcConfig = { + .unit_id = ADC_UNIT_1, + .clk_src = ADC_RTC_CLK_SRC_DEFAULT, + .ulp_mode = ADC_ULP_MODE_DISABLE, + }, + .adcChannelConfig = { + .atten = ADC_ATTEN_DB_12, + .bitwidth = ADC_BITWIDTH_DEFAULT, + }, + }; + + auto adc = std::make_unique(config, MIN_BATTERY_VOLTAGE, MAX_BATTERY_VOLTAGE); + if (!adc->isInitialized()) { + LOG_E(TAG, "ADC initialization failed; power monitoring unavailable"); + return nullptr; + } + + return std::make_shared(std::move(adc), POWER_OFF_PIN); +} diff --git a/Devices/m5stack-papers3/Source/devices/Power.h b/Devices/m5stack-papers3/Source/devices/Power.h new file mode 100644 index 000000000..c26138a94 --- /dev/null +++ b/Devices/m5stack-papers3/Source/devices/Power.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +using tt::hal::power::PowerDevice; + +/** + * @brief Power management for M5Stack PaperS3 + * + * Hardware configuration: + * - Battery voltage: GPIO3 (ADC1_CHANNEL_2) with 2x voltage divider + * - Charge status: GPIO4 - digital signal (0 = charging, 1 = not charging) + * - USB detect: GPIO5 - digital signal (1 = USB connected) + * - Power off: GPIO44 - pull high to trigger shutdown + */ +class PaperS3Power final : public PowerDevice { +private: + std::unique_ptr<::ChargeFromAdcVoltage> chargeFromAdcVoltage; + gpio_num_t powerOffPin; + bool powerOffInitialized = false; + bool buzzerInitialized = false; + +public: + explicit PaperS3Power( + std::unique_ptr<::ChargeFromAdcVoltage> chargeFromAdcVoltage, + gpio_num_t powerOffPin + ); + ~PaperS3Power() override = default; + + std::string getName() const override { return "M5Stack PaperS3 Power"; } + std::string getDescription() const override { return "Battery monitoring with charge detection and power-off"; } + + bool supportsMetric(MetricType type) const override; + bool getMetric(MetricType type, MetricData& data) override; + + bool supportsPowerOff() const override { return true; } + void powerOff() override; + +private: + void initializePowerOff(); + bool isCharging(); + // TODO: Fix USB Detection + bool isUsbConnected(); + + // Buzzer functions only used for the power off signal sound. + // So the user actually knows the epaper display is turning off. + void buzzerLedcInit(); + void toneOn(int frequency, int duration); + void toneOff(); +}; + +std::shared_ptr createPower(); diff --git a/Devices/m5stack-papers3/device.properties b/Devices/m5stack-papers3/device.properties index dad6aad9f..c8c46d27e 100644 --- a/Devices/m5stack-papers3/device.properties +++ b/Devices/m5stack-papers3/device.properties @@ -22,6 +22,7 @@ dpi=235 [lvgl] colorDepth=8 +fontSize=24 theme=Mono [sdkconfig] diff --git a/Drivers/FastEpdDisplay/CMakeLists.txt b/Drivers/EPDiyDisplay/CMakeLists.txt similarity index 51% rename from Drivers/FastEpdDisplay/CMakeLists.txt rename to Drivers/EPDiyDisplay/CMakeLists.txt index 19f733756..559cb99b4 100644 --- a/Drivers/FastEpdDisplay/CMakeLists.txt +++ b/Drivers/EPDiyDisplay/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register( SRC_DIRS "Source" INCLUDE_DIRS "Source" - REQUIRES FastEPD TactilityCore Tactility + REQUIRES Tactility epdiy esp_lvgl_port + PRIV_REQUIRES esp_timer ) diff --git a/Drivers/EPDiyDisplay/README.md b/Drivers/EPDiyDisplay/README.md new file mode 100644 index 000000000..5d53d805c --- /dev/null +++ b/Drivers/EPDiyDisplay/README.md @@ -0,0 +1,3 @@ +# EPDiy Display Driver + +A display driver for e-paper/e-ink displays using the EPDiy library. This driver provides LVGL integration and high-level display management for EPD panels. diff --git a/Drivers/EPDiyDisplay/Source/EpdiyDisplay.cpp b/Drivers/EPDiyDisplay/Source/EpdiyDisplay.cpp new file mode 100644 index 000000000..bbe4af0e2 --- /dev/null +++ b/Drivers/EPDiyDisplay/Source/EpdiyDisplay.cpp @@ -0,0 +1,472 @@ +#include "EpdiyDisplay.h" + +#include +#include + +#include +#include + +constexpr const char* TAG = "EpdiyDisplay"; + +bool EpdiyDisplay::s_hlInitialized = false; +EpdiyHighlevelState EpdiyDisplay::s_hlState = {}; + +EpdiyDisplay::EpdiyDisplay(std::unique_ptr inConfiguration) + : configuration(std::move(inConfiguration)) { + check(configuration != nullptr); + check(configuration->board != nullptr); + check(configuration->display != nullptr); +} + +EpdiyDisplay::~EpdiyDisplay() { + if (lvglDisplay != nullptr) { + stopLvgl(); + } + if (initialized) { + stop(); + } +} + +bool EpdiyDisplay::start() { + if (initialized) { + LOG_W(TAG, "Already initialized"); + return true; + } + + // Initialize EPDiy low-level hardware + epd_init( + configuration->board, + configuration->display, + configuration->initOptions + ); + + // Set rotation BEFORE initializing highlevel state + epd_set_rotation(configuration->rotation); + LOG_I(TAG, "Display rotation set to %d", configuration->rotation); + + // Initialize the high-level API only once — epd_hl_init() sets a static flag internally + // and there is no matching epd_hl_deinit(). Reuse the existing state on subsequent starts. + if (!s_hlInitialized) { + s_hlState = epd_hl_init(configuration->waveform); + if (s_hlState.front_fb == nullptr) { + LOG_E(TAG, "Failed to initialize EPDiy highlevel state"); + epd_deinit(); + return false; + } + s_hlInitialized = true; + LOG_I(TAG, "EPDiy highlevel state initialized"); + } else { + LOG_I(TAG, "Reusing existing EPDiy highlevel state"); + } + + highlevelState = s_hlState; + framebuffer = epd_hl_get_framebuffer(&highlevelState); + + initialized = true; + LOG_I(TAG, "EPDiy initialized successfully (%dx%d native, %dx%d rotated)", epd_width(), epd_height(), epd_rotated_display_width(), epd_rotated_display_height()); + + // Perform initial clear to ensure clean state + LOG_I(TAG, "Performing initial screen clear..."); + clearScreen(); + LOG_I(TAG, "Screen cleared"); + + return true; +} + +bool EpdiyDisplay::stop() { + if (!initialized) { + return true; + } + + if (lvglDisplay != nullptr) { + stopLvgl(); + } + + // Power off the display + if (powered) { + setPowerOn(false); + } + + // Deinitialize EPDiy low-level hardware. + // The HL framebuffers (s_hlState) are intentionally kept alive: epd_hl_init() has no + // matching deinit and sets an internal already_initialized flag, so the HL state must + // persist across stop()/start() cycles and be reused on the next start(). + epd_deinit(); + + // Clear instance references to HL state (the static s_hlState still owns the memory) + highlevelState = {}; + framebuffer = nullptr; + + initialized = false; + LOG_I(TAG, "EPDiy deinitialized (HL state preserved for restart)"); + + return true; +} + +void EpdiyDisplay::setPowerOn(bool turnOn) { + if (!initialized) { + LOG_W(TAG, "Cannot change power state - EPD not initialized"); + return; + } + + if (powered == turnOn) { + return; + } + + if (turnOn) { + epd_poweron(); + powered = true; + LOG_D(TAG, "EPD power on"); + } else { + epd_poweroff(); + powered = false; + LOG_D(TAG, "EPD power off"); + } +} + +// LVGL functions +bool EpdiyDisplay::startLvgl() { + if (lvglDisplay != nullptr) { + LOG_W(TAG, "LVGL already initialized"); + return true; + } + + if (!initialized) { + LOG_E(TAG, "EPD not initialized, call start() first"); + return false; + } + + // Get the native display dimensions + uint16_t width = epd_width(); + uint16_t height = epd_height(); + + LOG_I(TAG, "Creating LVGL display: %dx%d (EPDiy rotation: %d)", width, height, configuration->rotation); + + // Create LVGL display with native dimensions + lvglDisplay = lv_display_create(width, height); + if (lvglDisplay == nullptr) { + LOG_E(TAG, "Failed to create LVGL display"); + return false; + } + + // EPD uses 4-bit grayscale (16 levels) + // Map to LVGL's L8 format (8-bit grayscale) + lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_L8); + auto lv_rotation = epdRotationToLvgl(configuration->rotation); + lv_display_set_rotation(lvglDisplay, lv_rotation); + + // Allocate LVGL draw buffer (L8 format: 1 byte per pixel) + size_t draw_buffer_size = static_cast(width) * height; + + lvglDrawBuffer = static_cast(heap_caps_malloc(draw_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + if (lvglDrawBuffer == nullptr) { + LOG_W(TAG, "PSRAM allocation failed for draw buffer, falling back to internal memory"); + lvglDrawBuffer = static_cast(heap_caps_malloc(draw_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL)); + } + + if (lvglDrawBuffer == nullptr) { + LOG_E(TAG, "Failed to allocate draw buffer"); + lv_display_delete(lvglDisplay); + lvglDisplay = nullptr; + return false; + } + + // Pre-allocate 4-bit packed pixel buffer used in flushInternal (avoids per-flush heap allocation) + // Row stride with odd-width padding: (width + 1) / 2 bytes per row + size_t packed_buffer_size = static_cast((width + 1) / 2) * static_cast(height); + packedBuffer = static_cast(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + if (packedBuffer == nullptr) { + LOG_W(TAG, "PSRAM allocation failed for packed buffer, falling back to internal memory"); + packedBuffer = static_cast(heap_caps_malloc(packed_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL)); + } + + if (packedBuffer == nullptr) { + LOG_E(TAG, "Failed to allocate packed pixel buffer"); + heap_caps_free(lvglDrawBuffer); + lvglDrawBuffer = nullptr; + lv_display_delete(lvglDisplay); + lvglDisplay = nullptr; + return false; + } + + // For EPD, we want full refresh mode based on configuration + lv_display_render_mode_t render_mode = configuration->fullRefresh + ? LV_DISPLAY_RENDER_MODE_FULL + : LV_DISPLAY_RENDER_MODE_PARTIAL; + + lv_display_set_buffers(lvglDisplay, lvglDrawBuffer, NULL, draw_buffer_size, render_mode); + + // Set flush callback + lv_display_set_flush_cb(lvglDisplay, flushCallback); + lv_display_set_user_data(lvglDisplay, this); + + // Register rotation change event callback + lv_display_add_event_cb(lvglDisplay, rotationEventCallback, LV_EVENT_RESOLUTION_CHANGED, this); + LOG_D(TAG, "Registered rotation change event callback"); + + // Start touch device if present + auto touch_device = getTouchDevice(); + if (touch_device != nullptr && touch_device->supportsLvgl()) { + LOG_D(TAG, "Starting touch device for LVGL"); + if (!touch_device->startLvgl(lvglDisplay)) { + LOG_W(TAG, "Failed to start touch device for LVGL"); + } + } + + LOG_I(TAG, "LVGL display initialized"); + return true; +} + +bool EpdiyDisplay::stopLvgl() { + if (lvglDisplay == nullptr) { + return true; + } + + LOG_I(TAG, "Stopping LVGL display"); + + // Stop touch device + auto touch_device = getTouchDevice(); + if (touch_device != nullptr) { + touch_device->stopLvgl(); + } + + if (lvglDrawBuffer != nullptr) { + heap_caps_free(lvglDrawBuffer); + lvglDrawBuffer = nullptr; + } + + if (packedBuffer != nullptr) { + heap_caps_free(packedBuffer); + packedBuffer = nullptr; + } + + // Delete the LVGL display object + lv_display_delete(lvglDisplay); + lvglDisplay = nullptr; + + + LOG_I(TAG, "LVGL display stopped"); + return true; +} + +void EpdiyDisplay::flushCallback(lv_display_t* display, const lv_area_t* area, uint8_t* pixelMap) { + auto* instance = static_cast(lv_display_get_user_data(display)); + if (instance != nullptr) { + uint64_t t0 = esp_timer_get_time(); + const bool isLast = lv_display_flush_is_last(display); + instance->flushInternal(area, pixelMap, isLast); + LOG_D(TAG, "flush took %llu us", (unsigned long long)(esp_timer_get_time() - t0)); + } else { + LOG_W(TAG, "flush callback called with null instance"); + } + lv_display_flush_ready(display); +} + + +// EPD functions +void EpdiyDisplay::clearScreen() { + if (!initialized) { + LOG_E(TAG, "EPD not initialized"); + return; + } + + if (!powered) { + setPowerOn(true); + } + + epd_clear(); + + // Also clear the framebuffer + epd_hl_set_all_white(&highlevelState); +} + +void EpdiyDisplay::clearArea(EpdRect area) { + if (!initialized) { + LOG_E(TAG, "EPD not initialized"); + return; + } + + if (!powered) { + setPowerOn(true); + } + + epd_clear_area(area); +} + +enum EpdDrawError EpdiyDisplay::updateScreen(enum EpdDrawMode mode, int temperature) { + if (!initialized) { + LOG_E(TAG, "EPD not initialized"); + return EPD_DRAW_FAILED_ALLOC; + } + + if (!powered) { + setPowerOn(true); + } + + // Use defaults if not specified + if (mode == MODE_UNKNOWN_WAVEFORM) { + mode = configuration->defaultDrawMode; + } + if (temperature == -1) { + temperature = configuration->defaultTemperature; + } + + return epd_hl_update_screen(&highlevelState, mode, temperature); +} + +enum EpdDrawError EpdiyDisplay::updateArea(EpdRect area, enum EpdDrawMode mode, int temperature) { + if (!initialized) { + LOG_E(TAG, "EPD not initialized"); + return EPD_DRAW_FAILED_ALLOC; + } + + if (!powered) { + setPowerOn(true); + } + + // Use defaults if not specified + if (mode == MODE_UNKNOWN_WAVEFORM) { + mode = configuration->defaultDrawMode; + } + if (temperature == -1) { + temperature = configuration->defaultTemperature; + } + + return epd_hl_update_area(&highlevelState, mode, temperature, area); +} + +void EpdiyDisplay::setAllWhite() { + if (!initialized) { + LOG_E(TAG, "EPD not initialized"); + return; + } + + epd_hl_set_all_white(&highlevelState); +} + +// Internal functions +void EpdiyDisplay::flushInternal(const lv_area_t* area, uint8_t* pixelMap, bool isLast) { + if (!initialized) { + LOG_E(TAG, "Cannot flush - EPD not initialized"); + return; + } + + if (!powered) { + setPowerOn(true); + } + + const int x = area->x1; + const int y = area->y1; + const int width = lv_area_get_width(area); + const int height = lv_area_get_height(area); + + LOG_D(TAG, "Flushing area: x=%d, y=%d, w=%d, h=%d isLast=%d", x, y, width, height, (int)isLast); + + // Convert L8 (8-bit grayscale, 0=black/255=white) to EPDiy 4-bit (0=black/15=white). + // Pack 2 pixels per byte: lower nibble = even column, upper nibble = odd column. + // Row stride includes one padding nibble for odd widths to keep rows aligned. + // Threshold at 128 (matching FastEPD BB_MODE_1BPP): pixels > 127 → full white (15), + // pixels ≤ 127 → full black (0). Maximum contrast for the Mono theme and correct for + // MODE_DU which only drives two levels. For greyscale content / MODE_GL16, replace + // the threshold with `src[col] >> 4` to preserve intermediate grey levels. + const int row_stride = (width + 1) / 2; + for (int row = 0; row < height; ++row) { + const uint8_t* src = pixelMap + static_cast(row) * width; + uint8_t* dst = packedBuffer + static_cast(row) * row_stride; + for (int col = 0; col < width; col += 2) { + const uint8_t p0 = (src[col] > 127) ? 15u : 0u; + const uint8_t p1 = (col + 1 < width) ? ((src[col + 1] > 127) ? 15u : 0u) : 0u; + dst[col / 2] = static_cast((p1 << 4) | p0); + } + } + + const EpdRect update_area = { + .x = x, + .y = y, + .width = static_cast(width), + .height = static_cast(height) + }; + + // Write pixels into EPDiy's framebuffer (no hardware I/O, just memory) + epd_draw_rotated_image(update_area, packedBuffer, framebuffer); + + // Only trigger EPD hardware update on the last flush of this render cycle. + // EPDiy's epd_prep tasks run at configMAX_PRIORITIES-1 with busy-wait loops; calling + // epd_hl_update_area on every partial flush starves IDLE and triggers the task watchdog + // during scroll animations. Batching to one hardware update per LVGL render cycle fixes this. + if (isLast) { + epd_hl_update_screen( + &highlevelState, + static_cast(configuration->defaultDrawMode | MODE_PACKING_2PPB), + configuration->defaultTemperature + ); + } +} + +lv_display_rotation_t EpdiyDisplay::epdRotationToLvgl(enum EpdRotation epdRotation) { + // Static lookup table for EPD -> LVGL rotation mapping + // EPDiy: LANDSCAPE = 0°, PORTRAIT = 90° CW, INVERTED_LANDSCAPE = 180°, INVERTED_PORTRAIT = 270° CW + // LVGL: 0 = 0°, 90 = 90° CW, 180 = 180°, 270 = 270° CW + static const lv_display_rotation_t rotationMap[] = { + LV_DISPLAY_ROTATION_0, // EPD_ROT_LANDSCAPE (0) + LV_DISPLAY_ROTATION_270, // EPD_ROT_PORTRAIT (1) - 90° CW in EPD is 270° in LVGL + LV_DISPLAY_ROTATION_180, // EPD_ROT_INVERTED_LANDSCAPE (2) + LV_DISPLAY_ROTATION_90 // EPD_ROT_INVERTED_PORTRAIT (3) - 270° CW in EPD is 90° in LVGL + }; + + // Validate input and return mapped value + if (epdRotation >= 0 && epdRotation < 4) { + return rotationMap[epdRotation]; + } + + // Default to landscape if invalid + return LV_DISPLAY_ROTATION_0; +} + +enum EpdRotation EpdiyDisplay::lvglRotationToEpd(lv_display_rotation_t lvglRotation) { + // Static lookup table for LVGL -> EPD rotation mapping + static const enum EpdRotation rotationMap[] = { + EPD_ROT_LANDSCAPE, // LV_DISPLAY_ROTATION_0 (0) + EPD_ROT_INVERTED_PORTRAIT, // LV_DISPLAY_ROTATION_90 (1) + EPD_ROT_INVERTED_LANDSCAPE, // LV_DISPLAY_ROTATION_180 (2) + EPD_ROT_PORTRAIT // LV_DISPLAY_ROTATION_270 (3) + }; + + // Validate input and return mapped value + if (lvglRotation >= LV_DISPLAY_ROTATION_0 && lvglRotation <= LV_DISPLAY_ROTATION_270) { + return rotationMap[lvglRotation]; + } + + // Default to landscape if invalid + return EPD_ROT_LANDSCAPE; +} + +void EpdiyDisplay::rotationEventCallback(lv_event_t* event) { + auto* display = static_cast(lv_event_get_user_data(event)); + if (display == nullptr) { + return; + } + + lv_display_t* lvgl_display = static_cast(lv_event_get_target(event)); + if (lvgl_display == nullptr) { + return; + } + + lv_display_rotation_t rotation = lv_display_get_rotation(lvgl_display); + display->handleRotationChange(rotation); +} + +void EpdiyDisplay::handleRotationChange(lv_display_rotation_t lvgl_rotation) { + // Map LVGL rotation to EPDiy rotation using lookup table + enum EpdRotation epd_rotation = lvglRotationToEpd(lvgl_rotation); + + // Update EPDiy rotation + LOG_I(TAG, "LVGL rotation changed to %d, setting EPDiy rotation to %d", lvgl_rotation, epd_rotation); + epd_set_rotation(epd_rotation); + + // Update configuration to keep it in sync + configuration->rotation = epd_rotation; + + // Log the new dimensions + LOG_I(TAG, "Display dimensions after rotation: %dx%d", epd_rotated_display_width(), epd_rotated_display_height()); +} diff --git a/Drivers/EPDiyDisplay/Source/EpdiyDisplay.h b/Drivers/EPDiyDisplay/Source/EpdiyDisplay.h new file mode 100644 index 000000000..4fdf8907d --- /dev/null +++ b/Drivers/EPDiyDisplay/Source/EpdiyDisplay.h @@ -0,0 +1,163 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +class EpdiyDisplay final : public tt::hal::display::DisplayDevice { + +public: + + class Configuration { + public: + + Configuration( + const EpdBoardDefinition* board, + const EpdDisplay_t* display, + std::shared_ptr touch = nullptr, + enum EpdInitOptions initOptions = EPD_OPTIONS_DEFAULT, + const EpdWaveform* waveform = EPD_BUILTIN_WAVEFORM, + int defaultTemperature = 25, + enum EpdDrawMode defaultDrawMode = MODE_GL16, + bool fullRefresh = false, + enum EpdRotation rotation = EPD_ROT_LANDSCAPE + ) : board(board), + display(display), + touch(std::move(touch)), + initOptions(initOptions), + waveform(waveform), + defaultTemperature(defaultTemperature), + defaultDrawMode(defaultDrawMode), + fullRefresh(fullRefresh), + rotation(rotation) { + check(board != nullptr); + check(display != nullptr); + } + + const EpdBoardDefinition* board; + const EpdDisplay_t* display; + std::shared_ptr touch; + enum EpdInitOptions initOptions; + const EpdWaveform* waveform; + int defaultTemperature; + enum EpdDrawMode defaultDrawMode; + bool fullRefresh; + enum EpdRotation rotation; + }; + +private: + + std::unique_ptr configuration; + lv_display_t* _Nullable lvglDisplay = nullptr; + EpdiyHighlevelState highlevelState = {}; + uint8_t* framebuffer = nullptr; + uint8_t* lvglDrawBuffer = nullptr; + uint8_t* packedBuffer = nullptr; // Pre-allocated 4-bit packed pixel buffer for flushInternal + bool initialized = false; + bool powered = false; + + // epd_hl_init() sets an internal already_initialized flag and has no matching deinit. + // We track first-time init statically and keep the HL state alive across stop()/start() cycles. + static bool s_hlInitialized; + static EpdiyHighlevelState s_hlState; + + static void flushCallback(lv_display_t* display, const lv_area_t* area, uint8_t* pixelMap); + void flushInternal(const lv_area_t* area, uint8_t* pixelMap, bool isLast); + + static void rotationEventCallback(lv_event_t* event); + void handleRotationChange(lv_display_rotation_t rotation); + + // Rotation mapping helpers + static lv_display_rotation_t epdRotationToLvgl(enum EpdRotation epdRotation); + static enum EpdRotation lvglRotationToEpd(lv_display_rotation_t lvglRotation); + +public: + + explicit EpdiyDisplay(std::unique_ptr inConfiguration); + + ~EpdiyDisplay() override; + + std::string getName() const override { return "EPDiy"; } + + std::string getDescription() const override { + return "E-Ink display powered by EPDiy library"; + } + + // Device lifecycle + bool start() override; + bool stop() override; + + // Power control + void setPowerOn(bool turnOn) override; + bool isPoweredOn() const override { return powered; } + bool supportsPowerControl() const override { return true; } + + // Touch device + std::shared_ptr _Nullable getTouchDevice() override { + return configuration->touch; + } + + // LVGL support + bool supportsLvgl() const override { return true; } + bool startLvgl() override; + bool stopLvgl() override; + lv_display_t* _Nullable getLvglDisplay() const override { return lvglDisplay; } + + // DisplayDriver (not supported for EPD) + bool supportsDisplayDriver() const override { return false; } + std::shared_ptr _Nullable getDisplayDriver() override { + return nullptr; + } + + // EPD specific functions + + /** + * Get a reference to the framebuffer + */ + uint8_t* getFramebuffer() { + return epd_hl_get_framebuffer(&highlevelState); + } + + /** + * Clear the screen by flashing it + */ + void clearScreen(); + + /** + * Clear an area by flashing it + */ + void clearArea(EpdRect area); + + /** + * Manually trigger a screen update + * @param mode The draw mode to use (defaults to configuration default) + * @param temperature Temperature in °C (defaults to configuration default) + */ + enum EpdDrawError updateScreen( + enum EpdDrawMode mode = MODE_UNKNOWN_WAVEFORM, + int temperature = -1 + ); + + /** + * Update a specific area of the screen + * @param area The area to update + * @param mode The draw mode to use (defaults to configuration default) + * @param temperature Temperature in °C (defaults to configuration default) + */ + enum EpdDrawError updateArea( + EpdRect area, + enum EpdDrawMode mode = MODE_UNKNOWN_WAVEFORM, + int temperature = -1 + ); + + /** + * Set the display to all white + */ + void setAllWhite(); +}; diff --git a/Drivers/EPDiyDisplay/Source/EpdiyDisplayHelper.h b/Drivers/EPDiyDisplay/Source/EpdiyDisplayHelper.h new file mode 100644 index 000000000..31bc3c8a5 --- /dev/null +++ b/Drivers/EPDiyDisplay/Source/EpdiyDisplayHelper.h @@ -0,0 +1,43 @@ +#pragma once + +#include "EpdiyDisplay.h" +#include +#include +#include + +/** + * Helper class to create EPDiy displays with common configurations + */ +class EpdiyDisplayHelper { +public: + + /** + * Create a display for M5Paper S3 + * @param touch Optional touch device + * @param temperature Display temperature in °C (default: 20) + * @param drawMode Default draw mode (default: MODE_DU) + * @param fullRefresh Use full refresh mode (default: false for partial updates) + * @param rotation Display rotation (default: EPD_ROT_PORTRAIT) + */ + static std::shared_ptr createM5PaperS3Display( + std::shared_ptr touch = nullptr, + int temperature = 20, + enum EpdDrawMode drawMode = MODE_DU, + bool fullRefresh = false, + enum EpdRotation rotation = EPD_ROT_PORTRAIT + ) { + auto config = std::make_unique( + &epd_board_m5papers3, + &ED047TC1, + touch, + static_cast(EPD_LUT_1K | EPD_FEED_QUEUE_32), + static_cast(EPD_BUILTIN_WAVEFORM), + temperature, + drawMode, + fullRefresh, + rotation + ); + + return std::make_shared(std::move(config)); + } +}; diff --git a/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.cpp b/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.cpp index d747bce83..324f4fa43 100644 --- a/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.cpp +++ b/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.cpp @@ -18,6 +18,7 @@ ChargeFromAdcVoltage::ChargeFromAdcVoltage( LOGGER.error("ADC channel config failed"); adc_oneshot_del_unit(adcHandle); + adcHandle = nullptr; return; } } @@ -29,6 +30,9 @@ ChargeFromAdcVoltage::~ChargeFromAdcVoltage() { } bool ChargeFromAdcVoltage::readBatteryVoltageOnce(uint32_t& output) const { + if (adcHandle == nullptr) { + return false; + } int raw; if (adc_oneshot_read(adcHandle, configuration.adcChannel, &raw) == ESP_OK) { output = configuration.adcMultiplier * ((1000.f * configuration.adcRefVoltage) / 4096.f) * (float)raw; diff --git a/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.h b/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.h index 3e96f3737..ed40347ec 100644 --- a/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.h +++ b/Drivers/EstimatedPower/Source/ChargeFromAdcVoltage.h @@ -34,6 +34,8 @@ class ChargeFromAdcVoltage { ~ChargeFromAdcVoltage(); + bool isInitialized() const { return adcHandle != nullptr; } + bool readBatteryVoltageSampled(uint32_t& output) const; bool readBatteryVoltageOnce(uint32_t& output) const; diff --git a/Drivers/FastEpdDisplay/Source/FastEpdDisplay.cpp b/Drivers/FastEpdDisplay/Source/FastEpdDisplay.cpp deleted file mode 100644 index 600d25e2a..000000000 --- a/Drivers/FastEpdDisplay/Source/FastEpdDisplay.cpp +++ /dev/null @@ -1,256 +0,0 @@ -#include "FastEpdDisplay.h" - -#include - -#include - -#ifdef ESP_PLATFORM -#include -#endif - -#define TAG "FastEpdDisplay" - -FastEpdDisplay::~FastEpdDisplay() { - stop(); -} - -void FastEpdDisplay::flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { - auto* self = static_cast(lv_display_get_user_data(disp)); - - static uint32_t s_flush_log_counter = 0; - - const int32_t width = area->x2 - area->x1 + 1; - const bool grayscale4bpp = self->configuration.use4bppGrayscale; - - // LVGL logical resolution is portrait (540x960). FastEPD PaperS3 native is landscape (960x540). - // Keep FastEPD at rotation 0 and do the coordinate transform ourselves. - // For a 90° clockwise transform: - // x_native = y - // y_native = (native_height - 1) - x - const int native_width = self->epd.width(); - const int native_height = self->epd.height(); - - // Compute the native line range that will be affected by this flush. - // With our mapping y_native = (native_height - 1) - x - // So x range maps to y_native range. - int start_line = (native_height - 1) - (int)area->x2; - int end_line = (native_height - 1) - (int)area->x1; - if (start_line > end_line) { - const int tmp = start_line; - start_line = end_line; - end_line = tmp; - } - if (start_line < 0) start_line = 0; - if (end_line >= native_height) end_line = native_height - 1; - - for (int32_t y = area->y1; y <= area->y2; y++) { - for (int32_t x = area->x1; x <= area->x2; x++) { - const uint8_t gray8 = px_map[(y - area->y1) * width + (x - area->x1)]; - const uint8_t color = grayscale4bpp ? (uint8_t)(gray8 >> 4) : (uint8_t)((gray8 > 127) ? BBEP_BLACK : BBEP_WHITE); - - const int x_native = y; - const int y_native = (native_height - 1) - x; - - // Be defensive: any out-of-range drawPixelFast will corrupt memory. - if (x_native < 0 || x_native >= native_width || y_native < 0 || y_native >= native_height) { - continue; - } - self->epd.drawPixelFast(x_native, y_native, color); - } - } - - if (start_line <= end_line) { - (void)self->epd.einkPower(1); - self->flushCount++; - const uint32_t cadence = self->configuration.fullRefreshEveryNFlushes; - const bool requested_full = self->forceNextFullRefresh.exchange(false); - const bool do_full = requested_full || ((cadence > 0) && (self->flushCount % cadence == 0)); - - const bool should_log = ((++s_flush_log_counter % 25U) == 0U); - if (should_log) { - LOG_I(TAG, "flush #%lu area=(%ld,%ld)-(%ld,%ld) lines=[%d..%d] full=%d", - (unsigned long)self->flushCount, - (long)area->x1, (long)area->y1, (long)area->x2, (long)area->y2, - start_line, end_line, (int)do_full); - } - - if (do_full) { - const int rc = self->epd.fullUpdate(CLEAR_FAST, true, nullptr); - if (should_log) { - LOG_I(TAG, "fullUpdate rc=%d", rc); - } - - // After a full update, keep FastEPD's previous/current buffers in sync so that - // subsequent partial updates compute correct diffs. - const int w = self->epd.width(); - const int h = self->epd.height(); - const size_t bytes_per_row = grayscale4bpp - ? (size_t)(w + 1) / 2 - : (size_t)(w + 7) / 8; - const size_t plane_size = bytes_per_row * (size_t)h; - if (self->epd.currentBuffer() && self->epd.previousBuffer()) { - memcpy(self->epd.previousBuffer(), self->epd.currentBuffer(), plane_size); - } - } else { - if (grayscale4bpp) { - // FastEPD partialUpdate only supports 1bpp mode. - // For 4bpp we currently do a fullUpdate. Region-based updates are tricky here because - // we also manually rotate/transform pixels in flush_cb; a mismatched rect can refresh - // the wrong strip of the panel (seen as "split" buttons on the final refresh). - const int rc = self->epd.fullUpdate(CLEAR_FAST, true, nullptr); - if (should_log) { - LOG_I(TAG, "fullUpdate(4bpp) rc=%d", rc); - } - } else { - const int rc = self->epd.partialUpdate(true, start_line, end_line); - - // Keep FastEPD's previous/current buffers in sync after partial updates as well. - // This avoids stale diffs where subsequent updates don't visibly apply. - const int w = self->epd.width(); - const int h = self->epd.height(); - const size_t bytes_per_row = (size_t)(w + 7) / 8; - const size_t plane_size = bytes_per_row * (size_t)h; - if (rc == BBEP_SUCCESS && self->epd.currentBuffer() && self->epd.previousBuffer()) { - memcpy(self->epd.previousBuffer(), self->epd.currentBuffer(), plane_size); - } - - if (should_log) { - LOG_I(TAG, "partialUpdate rc=%d", rc); - } - } - } - } - - lv_display_flush_ready(disp); -} - -bool FastEpdDisplay::start() { - if (initialized) { - return true; - } - - const int rc = epd.initPanel(BB_PANEL_M5PAPERS3, configuration.busSpeedHz); - if (rc != BBEP_SUCCESS) { - LOG_E(TAG, "FastEPD initPanel failed rc=%d", rc); - return false; - } - - LOG_I(TAG, "FastEPD native size %dx%d", epd.width(), epd.height()); - - const int desired_mode = configuration.use4bppGrayscale ? BB_MODE_4BPP : BB_MODE_1BPP; - if (epd.setMode(desired_mode) != BBEP_SUCCESS) { - LOG_E(TAG, "FastEPD setMode(%d) failed", desired_mode); - epd.deInit(); - return false; - } - - // Keep FastEPD at rotation 0. LVGL-to-native mapping is handled in flush_cb. - - // Ensure previous/current buffers are in sync and the panel starts from a known state. - if (epd.einkPower(1) != BBEP_SUCCESS) { - LOG_W(TAG, "FastEPD einkPower(1) failed"); - } else { - epd.fillScreen(configuration.use4bppGrayscale ? 0x0F : BBEP_WHITE); - - const int native_width = epd.width(); - const int native_height = epd.height(); - const size_t bytes_per_row = configuration.use4bppGrayscale - ? (size_t)(native_width + 1) / 2 - : (size_t)(native_width + 7) / 8; - const size_t plane_size = bytes_per_row * (size_t)native_height; - - if (epd.currentBuffer() && epd.previousBuffer()) { - memcpy(epd.previousBuffer(), epd.currentBuffer(), plane_size); - } - - if (epd.fullUpdate(CLEAR_FAST, true, nullptr) != BBEP_SUCCESS) { - LOG_W(TAG, "FastEPD fullUpdate failed"); - } - } - - initialized = true; - return true; -} - -bool FastEpdDisplay::stop() { - if (lvglDisplay) { - stopLvgl(); - } - - if (initialized) { - epd.deInit(); - initialized = false; - } - - return true; -} - -bool FastEpdDisplay::startLvgl() { - if (lvglDisplay != nullptr) { - return true; - } - - lvglDisplay = lv_display_create(configuration.horizontalResolution, configuration.verticalResolution); - if (lvglDisplay == nullptr) { - return false; - } - - lv_display_set_color_format(lvglDisplay, LV_COLOR_FORMAT_L8); - - if (lv_display_get_rotation(lvglDisplay) != LV_DISPLAY_ROTATION_0) { - lv_display_set_rotation(lvglDisplay, LV_DISPLAY_ROTATION_0); - } - - const uint32_t pixel_count = (uint32_t)(configuration.horizontalResolution * configuration.verticalResolution / 10); - const uint32_t buf_size = pixel_count * (uint32_t)lv_color_format_get_size(LV_COLOR_FORMAT_L8); - lvglBufSize = buf_size; - -#ifdef ESP_PLATFORM - lvglBuf1 = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); -#else - lvglBuf1 = malloc(buf_size); -#endif - - if (lvglBuf1 == nullptr) { - lv_display_delete(lvglDisplay); - lvglDisplay = nullptr; - return false; - } - - lvglBuf2 = nullptr; - lv_display_set_buffers(lvglDisplay, lvglBuf1, lvglBuf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL); - - lv_display_set_user_data(lvglDisplay, this); - lv_display_set_flush_cb(lvglDisplay, FastEpdDisplay::flush_cb); - - if (configuration.touch && configuration.touch->supportsLvgl()) { - configuration.touch->startLvgl(lvglDisplay); - } - - return true; -} - -bool FastEpdDisplay::stopLvgl() { - if (lvglDisplay) { - if (configuration.touch) { - configuration.touch->stopLvgl(); - } - - lv_display_delete(lvglDisplay); - lvglDisplay = nullptr; - } - - if (lvglBuf1 != nullptr) { - free(lvglBuf1); - lvglBuf1 = nullptr; - } - - if (lvglBuf2 != nullptr) { - free(lvglBuf2); - lvglBuf2 = nullptr; - } - - lvglBufSize = 0; - - return true; -} diff --git a/Drivers/FastEpdDisplay/Source/FastEpdDisplay.h b/Drivers/FastEpdDisplay/Source/FastEpdDisplay.h deleted file mode 100644 index 22d9218d9..000000000 --- a/Drivers/FastEpdDisplay/Source/FastEpdDisplay.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -class FastEpdDisplay final : public tt::hal::display::DisplayDevice { -public: - struct Configuration final { - int horizontalResolution; - int verticalResolution; - std::shared_ptr touch; - uint32_t busSpeedHz = 20000000; - int rotationDegrees = 90; - bool use4bppGrayscale = false; - uint32_t fullRefreshEveryNFlushes = 0; - }; - -private: - Configuration configuration; - std::shared_ptr lock; - - lv_display_t* lvglDisplay = nullptr; - void* lvglBuf1 = nullptr; - void* lvglBuf2 = nullptr; - uint32_t lvglBufSize = 0; - - FASTEPD epd; - bool initialized = false; - uint32_t flushCount = 0; - std::atomic_bool forceNextFullRefresh{false}; - - static void flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map); - -public: - FastEpdDisplay(Configuration configuration, std::shared_ptr lock) - : configuration(std::move(configuration)), lock(std::move(lock)) {} - - ~FastEpdDisplay() override; - - void requestFullRefresh() override { forceNextFullRefresh.store(true); } - - std::string getName() const override { return "FastEpdDisplay"; } - std::string getDescription() const override { return "FastEPD (bitbank2) E-Ink display driver"; } - - bool start() override; - bool stop() override; - - bool supportsLvgl() const override { return true; } - bool startLvgl() override; - bool stopLvgl() override; - - lv_display_t* getLvglDisplay() const override { return lvglDisplay; } - - bool supportsDisplayDriver() const override { return false; } - std::shared_ptr getDisplayDriver() override { return nullptr; } - - std::shared_ptr getTouchDevice() override { - return configuration.touch; - } -}; diff --git a/Firmware/idf_component.yml b/Firmware/idf_component.yml index 0f1db8ffd..4221d0c4b 100644 --- a/Firmware/idf_component.yml +++ b/Firmware/idf_component.yml @@ -56,9 +56,9 @@ dependencies: - if: "target == esp32s3" espressif/esp_lvgl_port: "2.5.0" lvgl/lvgl: "9.3.0" - FastEPD: - git: https://github.com/bitbank2/FastEPD.git - version: 1.4.2 + epdiy: + git: https://github.com/Shadowtrance/epdiy.git + version: 2.0.1 rules: # More hardware might be supported - enable as needed - if: "target in [esp32s3]" diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index 9ef79ff03..8cd2a04b6 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -414,5 +414,13 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lv_anim_path_ease_out), // lv_async DEFINE_MODULE_SYMBOL(lv_async_call), + // lv_span + DEFINE_MODULE_SYMBOL(lv_spangroup_create), + DEFINE_MODULE_SYMBOL(lv_spangroup_set_align), + DEFINE_MODULE_SYMBOL(lv_spangroup_set_mode), + DEFINE_MODULE_SYMBOL(lv_spangroup_add_span), + DEFINE_MODULE_SYMBOL(lv_spangroup_refresh), + DEFINE_MODULE_SYMBOL(lv_span_get_style), + DEFINE_MODULE_SYMBOL(lv_span_set_text), MODULE_SYMBOL_TERMINATOR }; \ No newline at end of file diff --git a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp index 8be2305f9..36b1a854e 100644 --- a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp +++ b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp @@ -204,7 +204,11 @@ class AppHubDetailsApp final : public App { lv_obj_set_width(description_label, LV_PCT(100)); lv_label_set_long_mode(description_label, LV_LABEL_LONG_MODE_WRAP); if (!entry.appDescription.empty()) { - lv_label_set_text(description_label, entry.appDescription.c_str()); + std::string description = entry.appDescription; + for (size_t pos = 0; (pos = description.find("\\n", pos)) != std::string::npos;) { + description.replace(pos, 2, "\n"); + } + lv_label_set_text(description_label, description.c_str()); } else { lv_label_set_text(description_label, "This app has no description yet."); } diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 96784f445..1e4423b46 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -275,6 +275,10 @@ void View::onDirEntryPressed(uint32_t index) { } void View::onDirEntryLongPressed(int32_t index) { + if (state->getCurrentPath() == "/") { + return; + } + dirent dir_entry; if (!resolveDirentFromListIndex(index, dir_entry)) { return; @@ -452,7 +456,7 @@ void View::update(size_t start_index) { if (!is_root && last_loaded_index < total_entries) { if (total_entries > current_start_index && -+ (total_entries - current_start_index) > MAX_BATCH) { + (total_entries - current_start_index) > MAX_BATCH) { auto* next_btn = lv_list_add_btn(dir_entry_list, LV_SYMBOL_RIGHT, "Next"); lv_obj_add_event_cb(next_btn, [](lv_event_t* event) { auto* view = static_cast(lv_event_get_user_data(event)); @@ -549,7 +553,7 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu } else if (file::isFile(filepath)) { auto lock = file::getLock(filepath); lock->lock(); - if (remove(filepath.c_str()) <= 0) { + if (remove(filepath.c_str()) != 0) { LOGGER.warn("Failed to delete {}", filepath); } lock->unlock(); diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 79efc4fd6..4d362778f 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -48,18 +48,8 @@ class LauncherApp final : public App { lv_obj_set_style_text_font(button_image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT); lv_image_set_src(button_image, imageFile); lv_obj_set_style_text_color(button_image, lv_theme_get_color_primary(button_image), LV_STATE_DEFAULT); - - // Recolor handling: - // For color builds use theme primary color - // For 1-bit/monochrome builds force a visible color (black) - #if LV_COLOR_DEPTH == 1 - // Try forcing black recolor on monochrome builds - lv_obj_set_style_image_recolor(button_image, lv_color_black(), LV_STATE_DEFAULT); - lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT); - #else lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT); lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT); - #endif // Ensure it's square (Material Symbols are slightly wider than tall) lv_obj_set_size(button_image, button_size, button_size); @@ -157,7 +147,7 @@ class LauncherApp final : public App { createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "Settings", margin, is_landscape_display); if (shouldShowPowerButton()) { - auto* power_button = lv_btn_create(parent); + auto* power_button = lv_button_create(parent); lv_obj_set_style_pad_all(power_button, 8, 0); lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10); lv_obj_add_event_cb(power_button, onPowerOffPressed, LV_EVENT_SHORT_CLICKED, nullptr); diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index 42ebb6d0a..18155f128 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -192,8 +192,8 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) { auto* image = lv_image_create(obj); lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT); + lv_obj_set_style_text_color(image, lv_color_white(), LV_STATE_DEFAULT); lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT); - obj_set_style_bg_blacken(image); statusbar->icons[i] = image; update_icon(image, &(statusbar_data.icons[i])); diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index 6c34b7c78..6aa3335f7 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -400,6 +401,9 @@ bool WebServerService::startApMode() { void WebServerService::stopApMode() { if (apWifiInitialized) { esp_err_t err; + if (apNetif != nullptr) { + esp_wifi_clear_default_wifi_driver_and_handlers(apNetif); + } err = esp_wifi_stop(); if (err != ESP_OK && err != ESP_ERR_WIFI_NOT_STARTED) { LOGGER.warn("esp_wifi_stop() in cleanup: {}", esp_err_to_name(err)); diff --git a/Tactility/Source/service/wifi/WifiEsp.cpp b/Tactility/Source/service/wifi/WifiEsp.cpp index 2840f9e05..c4ed39c06 100644 --- a/Tactility/Source/service/wifi/WifiEsp.cpp +++ b/Tactility/Source/service/wifi/WifiEsp.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -537,6 +538,7 @@ static void dispatchEnable(std::shared_ptr wifi) { publish_event(wifi, WifiEvent::RadioStateOnPending); if (wifi->netif != nullptr) { + esp_wifi_clear_default_wifi_driver_and_handlers(wifi->netif); esp_netif_destroy(wifi->netif); } wifi->netif = esp_netif_create_default_wifi_sta(); @@ -633,11 +635,21 @@ static void dispatchDisable(std::shared_ptr wifi) { // Free up scan list memory scan_list_free_safely(wifi_singleton); + // Detach netif from the internal WiFi event handlers before stopping. + // Those handlers call esp_netif_action_stop on WIFI_EVENT_STA_STOP, which + // queues esp_netif_stop_api to the lwIP thread. esp_netif_destroy later + // queues a second one; the first zeroes lwip_netif, and the second then + // crashes in netif_ip6_addr_set_parts (null pointer + 0x263 offset). + if (wifi->netif != nullptr) { + esp_wifi_clear_default_wifi_driver_and_handlers(wifi->netif); + } + + // Note: handlers are already detached above, so we cannot safely return to + // RadioState::On from here — the netif would be missing its default WiFi + // event handlers and subsequent disable attempts would behave incorrectly. + // If stop fails, continue the teardown anyway so we end in a clean Off state. if (esp_wifi_stop() != ESP_OK) { - LOGGER.error("Failed to stop radio"); - wifi->setRadioState(RadioState::On); - publish_event(wifi, WifiEvent::RadioStateOn); - return; + LOGGER.error("Failed to stop radio - continuing teardown"); } if (esp_wifi_set_mode(WIFI_MODE_NULL) != ESP_OK) { diff --git a/TactilityC/Source/symbols/freertos.cpp b/TactilityC/Source/symbols/freertos.cpp index f3829e1d8..fbc943c0e 100644 --- a/TactilityC/Source/symbols/freertos.cpp +++ b/TactilityC/Source/symbols/freertos.cpp @@ -35,6 +35,7 @@ const esp_elfsym freertos_symbols[] = { ESP_ELFSYM_EXPORT(xTaskCreateStatic), ESP_ELFSYM_EXPORT(xTaskCreateStaticPinnedToCore), ESP_ELFSYM_EXPORT(xTaskCreateWithCaps), + ESP_ELFSYM_EXPORT(xTaskCreatePinnedToCoreWithCaps), ESP_ELFSYM_EXPORT(xTaskDelayUntil), ESP_ELFSYM_EXPORT(xTaskGenericNotify), ESP_ELFSYM_EXPORT(xTaskGenericNotifyFromISR), diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index a065045a2..3dc5079bc 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -408,6 +409,8 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(i2s_channel_reconfig_std_clock), ESP_ELFSYM_EXPORT(i2s_channel_reconfig_std_slot), ESP_ELFSYM_EXPORT(i2s_channel_reconfig_std_gpio), + // esp_heap_caps.h + ESP_ELFSYM_EXPORT(heap_caps_get_total_size), // delimiter ESP_ELFSYM_END }; diff --git a/device.py b/device.py index 486ce9a48..599007194 100644 --- a/device.py +++ b/device.py @@ -227,8 +227,7 @@ def write_lvgl_variables(output_file, device_properties: ConfigParser): elif theme == "DefaultLight": output_file.write("CONFIG_LV_THEME_DEFAULT_LIGHT=y\n") elif theme == "Mono": - output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n") - output_file.write("CONFIG_LV_THEME_MONO=y\n") + output_file.write("CONFIG_LV_USE_THEME_MONO=y\n") else: exit_with_error(f"Unknown theme: {theme}") font_height_text = get_property_or_default(device_properties, "lvgl", "fontSize", "14")