Merge branch 'main' into develop

This commit is contained in:
Ken Van Hoeylandt 2026-03-07 01:14:29 +01:00
commit 9331422a66
42 changed files with 1537 additions and 372 deletions

View File

@ -53,6 +53,7 @@ jobs:
{ id: guition-jc8048w550c, arch: esp32s3 },
{ id: heltec-wifi-lora-32-v3, arch: esp32s3 },
{ id: lilygo-tdeck, arch: esp32s3 },
{ id: lilygo-thmi-s3, arch: esp32s3 },
{ id: lilygo-tdongle-s3, arch: esp32s3 },
{ id: lilygo-tdisplay-s3, arch: esp32s3 },
{ id: lilygo-tlora-pager, arch: esp32s3 },

View File

@ -0,0 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility ButtonControl XPT2046SoftSPI PwmBacklight EstimatedPower ST7789-i8080 driver vfs fatfs
)

View File

@ -0,0 +1,24 @@
#include "devices/Power.h"
#include "devices/SdCard.h"
#include "devices/Display.h"
#include <ButtonControl.h>
#include <Tactility/hal/Configuration.h>
bool initBoot();
using namespace tt::hal;
static std::vector<std::shared_ptr<tt::hal::Device>> createDevices() {
return {
createSdCard(),
createDisplay(),
std::make_shared<Power>(),
ButtonControl::createOneButtonControl(0)
};
}
extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};

View File

@ -0,0 +1,47 @@
#include "devices/Power.h"
#include "devices/Display.h"
#include "PwmBacklight.h"
#include "Tactility/kernel/SystemEvents.h"
#include <Tactility/TactilityCore.h>
#define TAG "thmi-s3"
static bool powerOn() {
gpio_config_t power_signal_config = {
.pin_bit_mask = (1ULL << THMI_S3_POWERON_GPIO) | (1ULL << THMI_S3_POWEREN_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&power_signal_config) != ESP_OK) {
return false;
}
if (gpio_set_level(THMI_S3_POWERON_GPIO, 1) != ESP_OK) {
return false;
}
if (gpio_set_level(THMI_S3_POWEREN_GPIO, 1) != ESP_OK) {
return false;
}
return true;
}
bool initBoot() {
ESP_LOGI(TAG, "Powering on the board...");
if (!powerOn()) {
ESP_LOGE(TAG, "Failed to power on the board.");
return false;
}
if (!driver::pwmbacklight::init(DISPLAY_BL, 30000)) {
ESP_LOGE(TAG, "Failed to initialize backlight.");
return false;
}
return true;
}

View File

@ -0,0 +1,45 @@
#include <Xpt2046SoftSpi.h>
#include <Tactility/hal/touch/TouchDevice.h>
#include "Display.h"
#include "PwmBacklight.h"
#include "St7789i8080Display.h"
static bool touchSpiInitialized = false;
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto config = std::make_unique<Xpt2046SoftSpi::Configuration>(
TOUCH_MOSI_PIN,
TOUCH_MISO_PIN,
TOUCH_SCK_PIN,
TOUCH_CS_PIN,
DISPLAY_HORIZONTAL_RESOLUTION,
DISPLAY_VERTICAL_RESOLUTION
);
return std::make_shared<Xpt2046SoftSpi>(std::move(config));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
// Create configuration
auto config = St7789i8080Display::Configuration(
DISPLAY_CS, // CS
DISPLAY_DC, // DC
DISPLAY_WR, // WR
DISPLAY_RD, // RD
{ DISPLAY_I80_D0, DISPLAY_I80_D1, DISPLAY_I80_D2, DISPLAY_I80_D3,
DISPLAY_I80_D4, DISPLAY_I80_D5, DISPLAY_I80_D6, DISPLAY_I80_D7 }, // D0..D7
DISPLAY_RST, // RST
DISPLAY_BL // BL
);
// Set resolution explicitly
config.horizontalResolution = DISPLAY_HORIZONTAL_RESOLUTION;
config.verticalResolution = DISPLAY_VERTICAL_RESOLUTION;
config.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty;
config.touch = createTouch();
config.invertColor = false;
auto display = std::make_shared<St7789i8080Display>(config);
return display;
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <driver/gpio.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include "driver/spi_common.h"
class St7789i8080Display;
constexpr auto DISPLAY_CS = GPIO_NUM_6;
constexpr auto DISPLAY_DC = GPIO_NUM_7;
constexpr auto DISPLAY_WR = GPIO_NUM_8;
constexpr auto DISPLAY_RD = GPIO_NUM_NC;
constexpr auto DISPLAY_RST = GPIO_NUM_NC;
constexpr auto DISPLAY_BL = GPIO_NUM_38;
constexpr auto DISPLAY_I80_D0 = GPIO_NUM_48;
constexpr auto DISPLAY_I80_D1 = GPIO_NUM_47;
constexpr auto DISPLAY_I80_D2 = GPIO_NUM_39;
constexpr auto DISPLAY_I80_D3 = GPIO_NUM_40;
constexpr auto DISPLAY_I80_D4 = GPIO_NUM_41;
constexpr auto DISPLAY_I80_D5 = GPIO_NUM_42;
constexpr auto DISPLAY_I80_D6 = GPIO_NUM_45;
constexpr auto DISPLAY_I80_D7 = GPIO_NUM_46;
constexpr auto DISPLAY_HORIZONTAL_RESOLUTION = 240;
constexpr auto DISPLAY_VERTICAL_RESOLUTION = 320;
// Touch (XPT2046, resistive)
constexpr auto TOUCH_SPI_HOST = SPI2_HOST;
constexpr auto TOUCH_MISO_PIN = GPIO_NUM_4;
constexpr auto TOUCH_MOSI_PIN = GPIO_NUM_3;
constexpr auto TOUCH_SCK_PIN = GPIO_NUM_1;
constexpr auto TOUCH_CS_PIN = GPIO_NUM_2;
constexpr auto TOUCH_IRQ_PIN = GPIO_NUM_9;
// Factory function for registration
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -0,0 +1,90 @@
#include "Power.h"
#include <Tactility/Logger.h>
#include <driver/adc.h>
static const auto LOGGER = tt::Logger("Power");
bool Power::adcInitCalibration() {
bool calibrated = false;
esp_err_t efuse_read_result = esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP_FIT);
if (efuse_read_result == ESP_ERR_NOT_SUPPORTED) {
LOGGER.warn("Calibration scheme not supported, skip software calibration");
} else if (efuse_read_result == ESP_ERR_INVALID_VERSION) {
LOGGER.warn("eFuse not burnt, skip software calibration");
} else if (efuse_read_result == ESP_OK) {
calibrated = true;
LOGGER.info("Calibration success");
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, static_cast<adc_bits_width_t>(ADC_WIDTH_BIT_DEFAULT), 0, &adcCharacteristics);
} else {
LOGGER.warn("eFuse read failed, skipping calibration");
}
return calibrated;
}
uint32_t Power::adcReadValue() const {
int adc_raw = adc1_get_raw(ADC1_CHANNEL_4);
LOGGER.debug("Raw data: {}", adc_raw);
uint32_t voltage;
if (calibrated) {
voltage = esp_adc_cal_raw_to_voltage(adc_raw, &adcCharacteristics);
LOGGER.debug("Calibrated data: {} mV", voltage);
} else {
voltage = (adc_raw * 3300) / 4095; // fallback
LOGGER.debug("Estimated data: {} mV", voltage);
}
return voltage;
}
bool Power::ensureInitialized() {
if (!initialized) {
if (adc1_config_width(ADC_WIDTH_BIT_12) != ESP_OK) {
LOGGER.error("ADC1 config width failed");
return false;
}
if (adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11) != ESP_OK) {
LOGGER.error("ADC1 config attenuation failed");
return false;
}
calibrated = adcInitCalibration();
initialized = true;
}
return true;
}
bool Power::supportsMetric(MetricType type) const {
switch (type) {
using enum MetricType;
case BatteryVoltage:
case ChargeLevel:
return true;
default:
return false;
}
}
bool Power::getMetric(MetricType type, MetricData& data) {
if (!ensureInitialized()) {
return false;
}
switch (type) {
case MetricType::BatteryVoltage:
data.valueAsUint32 = adcReadValue() * 2;
return true;
case MetricType::ChargeLevel:
data.valueAsUint8 = chargeFromAdcVoltage.estimateCharge(adcReadValue() * 2);
return true;
default:
return false;
}
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <esp_adc_cal.h>
#include <driver/gpio.h>
#include <ChargeFromVoltage.h>
#include <Tactility/hal/power/PowerDevice.h>
constexpr auto THMI_S3_POWEREN_GPIO = GPIO_NUM_10;
constexpr auto THMI_S3_POWERON_GPIO = GPIO_NUM_14;
using tt::hal::power::PowerDevice;
class Power final : public PowerDevice {
ChargeFromVoltage chargeFromAdcVoltage = ChargeFromVoltage(3.3f, 4.2f);
bool initialized = false;
esp_adc_cal_characteristics_t adcCharacteristics;
bool calibrated = false;
bool adcInitCalibration();
uint32_t adcReadValue() const;
bool ensureInitialized();
public:
std::string getName() const override { return "T-hmi Power"; }
std::string getDescription() const override { return "Power measurement via ADC"; }
bool supportsMetric(MetricType type) const override;
bool getMetric(MetricType type, MetricData& data) override;
};

View File

@ -0,0 +1,23 @@
#include "SdCard.h"
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/hal/sdcard/SdmmcDevice.h>
using tt::hal::sdcard::SdmmcDevice;
std::shared_ptr<SdCardDevice> createSdCard() {
auto configuration = std::make_unique<SdmmcDevice::Config>(
SD_DIO_SCLK, //CLK
SD_DIO_CMD, //CMD
SD_DIO_DATA0, //D0
SD_DIO_NC, //D1
SD_DIO_NC, //D2
SD_DIO_NC, //D3
SdCardDevice::MountBehaviour::AtBoot,
SD_DIO_BUS_WIDTH
);
return std::make_shared<SdmmcDevice>(
std::move(configuration)
);
}

View File

@ -0,0 +1,15 @@
#pragma once
#include <driver/gpio.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
using tt::hal::sdcard::SdCardDevice;
constexpr auto SD_DIO_CMD = GPIO_NUM_11;
constexpr auto SD_DIO_SCLK = GPIO_NUM_12;
constexpr auto SD_DIO_DATA0 = GPIO_NUM_13;
constexpr auto SD_DIO_NC = GPIO_NUM_NC;
constexpr auto SD_DIO_BUS_WIDTH = 1;
std::shared_ptr<SdCardDevice> createSdCard();

View File

@ -0,0 +1,23 @@
#include <tactility/module.h>
extern "C" {
static error_t start() {
// Empty for now
return ERROR_NONE;
}
static error_t stop() {
// Empty for now
return ERROR_NONE;
}
struct Module lilygo_thmi_s3_module = {
.name = "lilygo-thmi-s3",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}

View File

@ -0,0 +1,23 @@
[general]
vendor=LilyGO
name=T-HMI S3
[apps]
launcherAppId=Launcher
[hardware]
target=ESP32S3
flashSize=16MB
spiRam=true
spiRamMode=OCT
spiRamSpeed=120M
tinyUsb=true
esptoolFlashFreq=120M
[display]
size=2.8"
shape=rectangle
dpi=125
[lvgl]
colorDepth=16

View File

@ -0,0 +1,3 @@
dependencies:
- Platforms/platform-esp32
dts: lilygo,thmi-s3.dts

View File

@ -0,0 +1,24 @@
/dts-v1/;
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h>
/ {
compatible = "root";
model = "LilyGO T-HMI S3";
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
pin-mosi = <&gpio0 3 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 4 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 1 GPIO_FLAG_NONE>;
};
};

View File

@ -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
)

View File

@ -1,19 +1,22 @@
#include "devices/Display.h"
#include "devices/SdCard.h"
#include "devices/Power.h"
#include <Tactility/hal/Configuration.h>
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
};

View File

@ -0,0 +1,48 @@
#include <Tactility/Logger.h>
#include <driver/gpio.h>
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;
}

View File

@ -1,34 +1,23 @@
#include "Display.h"
#include <Gt911Touch.h>
#include <FastEpdDisplay.h>
#include <Tactility/lvgl/LvglSync.h>
#include <EpdiyDisplayHelper.h>
std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto configuration = std::make_unique<Gt911Touch::Configuration>(
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<Gt911Touch>(std::move(configuration));
return std::static_pointer_cast<tt::hal::touch::TouchDevice>(touch);
return std::make_shared<Gt911Touch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay(std::shared_ptr<tt::hal::touch::TouchDevice> touch) {
FastEpdDisplay::Configuration configuration = {
.horizontalResolution = 540,
.verticalResolution = 960,
.touch = std::move(touch),
.busSpeedHz = 20000000,
.rotationDegrees = 90,
.use4bppGrayscale = false,
.fullRefreshEveryNFlushes = 40,
};
return std::make_shared<FastEpdDisplay>(configuration, tt::lvgl::getSyncLock());
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto touch = createTouch();
return EpdiyDisplayHelper::createM5PaperS3Display(touch);
}

View File

@ -1,7 +1,6 @@
#pragma once
#include <memory>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/touch/TouchDevice.h>
std::shared_ptr<tt::hal::touch::TouchDevice> createTouch();
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay(std::shared_ptr<tt::hal::touch::TouchDevice> touch);
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -0,0 +1,282 @@
#include "Power.h"
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/ledc.h>
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> 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<PowerDevice> 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<ChargeFromAdcVoltage>(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<PaperS3Power>(std::move(adc), POWER_OFF_PIN);
}

View File

@ -0,0 +1,55 @@
#pragma once
#include <memory>
#include <ChargeFromAdcVoltage.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <driver/gpio.h>
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<tt::hal::power::PowerDevice> createPower();

View File

@ -22,6 +22,7 @@ dpi=235
[lvgl]
colorDepth=8
fontSize=24
theme=Mono
[sdkconfig]

View File

@ -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
)

View File

@ -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.

View File

@ -0,0 +1,472 @@
#include "EpdiyDisplay.h"
#include <tactility/check.h>
#include <tactility/log.h>
#include <esp_heap_caps.h>
#include <esp_timer.h>
constexpr const char* TAG = "EpdiyDisplay";
bool EpdiyDisplay::s_hlInitialized = false;
EpdiyHighlevelState EpdiyDisplay::s_hlState = {};
EpdiyDisplay::EpdiyDisplay(std::unique_ptr<Configuration> 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<size_t>(width) * height;
lvglDrawBuffer = static_cast<uint8_t*>(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<uint8_t*>(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<size_t>((width + 1) / 2) * static_cast<size_t>(height);
packedBuffer = static_cast<uint8_t*>(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<uint8_t*>(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<EpdiyDisplay*>(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<size_t>(row) * width;
uint8_t* dst = packedBuffer + static_cast<size_t>(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<uint8_t>((p1 << 4) | p0);
}
}
const EpdRect update_area = {
.x = x,
.y = y,
.width = static_cast<uint16_t>(width),
.height = static_cast<uint16_t>(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<EpdDrawMode>(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<EpdiyDisplay*>(lv_event_get_user_data(event));
if (display == nullptr) {
return;
}
lv_display_t* lvgl_display = static_cast<lv_display_t*>(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());
}

View File

@ -0,0 +1,163 @@
#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/touch/TouchDevice.h>
#include <epd_highlevel.h>
#include <epdiy.h>
#include <lvgl.h>
#include <memory>
#include <cassert>
#include <cstdlib>
class EpdiyDisplay final : public tt::hal::display::DisplayDevice {
public:
class Configuration {
public:
Configuration(
const EpdBoardDefinition* board,
const EpdDisplay_t* display,
std::shared_ptr<tt::hal::touch::TouchDevice> 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<tt::hal::touch::TouchDevice> touch;
enum EpdInitOptions initOptions;
const EpdWaveform* waveform;
int defaultTemperature;
enum EpdDrawMode defaultDrawMode;
bool fullRefresh;
enum EpdRotation rotation;
};
private:
std::unique_ptr<Configuration> 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<Configuration> 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<tt::hal::touch::TouchDevice> _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<tt::hal::display::DisplayDriver> _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();
};

View File

@ -0,0 +1,43 @@
#pragma once
#include "EpdiyDisplay.h"
#include <epd_board.h>
#include <epd_display.h>
#include <memory>
/**
* 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<EpdiyDisplay> createM5PaperS3Display(
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
int temperature = 20,
enum EpdDrawMode drawMode = MODE_DU,
bool fullRefresh = false,
enum EpdRotation rotation = EPD_ROT_PORTRAIT
) {
auto config = std::make_unique<EpdiyDisplay::Configuration>(
&epd_board_m5papers3,
&ED047TC1,
touch,
static_cast<EpdInitOptions>(EPD_LUT_1K | EPD_FEED_QUEUE_32),
static_cast<const EpdWaveform*>(EPD_BUILTIN_WAVEFORM),
temperature,
drawMode,
fullRefresh,
rotation
);
return std::make_shared<EpdiyDisplay>(std::move(config));
}
};

View File

@ -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;

View File

@ -34,6 +34,8 @@ public:
~ChargeFromAdcVoltage();
bool isInitialized() const { return adcHandle != nullptr; }
bool readBatteryVoltageSampled(uint32_t& output) const;
bool readBatteryVoltageOnce(uint32_t& output) const;

View File

@ -1,256 +0,0 @@
#include "FastEpdDisplay.h"
#include <tactility/log.h>
#include <cstring>
#ifdef ESP_PLATFORM
#include <esp_heap_caps.h>
#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<FastEpdDisplay*>(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;
}

View File

@ -1,65 +0,0 @@
#pragma once
#include <Tactility/Lock.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/touch/TouchDevice.h>
#include <atomic>
#include <FastEPD.h>
#include <lvgl.h>
class FastEpdDisplay final : public tt::hal::display::DisplayDevice {
public:
struct Configuration final {
int horizontalResolution;
int verticalResolution;
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
uint32_t busSpeedHz = 20000000;
int rotationDegrees = 90;
bool use4bppGrayscale = false;
uint32_t fullRefreshEveryNFlushes = 0;
};
private:
Configuration configuration;
std::shared_ptr<tt::Lock> 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<tt::Lock> 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<tt::hal::display::DisplayDriver> getDisplayDriver() override { return nullptr; }
std::shared_ptr<tt::hal::touch::TouchDevice> getTouchDevice() override {
return configuration.touch;
}
};

View File

@ -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]"

View File

@ -424,6 +424,14 @@ 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),
// lv_binfont
DEFINE_MODULE_SYMBOL(lv_binfont_create),
DEFINE_MODULE_SYMBOL(lv_binfont_destroy),

View File

@ -204,7 +204,11 @@ public:
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.");
}

View File

@ -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<View*>(lv_event_get_user_data(event));
@ -549,7 +553,7 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> 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();

View File

@ -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 @@ public:
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);

View File

@ -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]));

View File

@ -18,6 +18,7 @@
#include <Tactility/app/App.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/service/wifi/Wifi.h>
#include <esp_wifi_default.h>
#include <Tactility/network/HttpdReq.h>
#include <Tactility/network/Url.h>
#include <Tactility/Paths.h>
@ -401,6 +402,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));

View File

@ -19,6 +19,7 @@
#include <Tactility/service/wifi/WifiGlobals.h>
#include <Tactility/service/wifi/WifiSettings.h>
#include <esp_wifi_default.h>
#include <lwip/esp_netif_net_stack.h>
#include <freertos/FreeRTOS.h>
#include <atomic>
@ -537,6 +538,7 @@ static void dispatchEnable(std::shared_ptr<Wifi> 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> 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) {

View File

@ -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),

View File

@ -40,6 +40,7 @@
#include <esp_random.h>
#include <esp_sntp.h>
#include <esp_netif.h>
#include <esp_heap_caps.h>
#include <fcntl.h>
#include <lwip/sockets.h>
#include <lwip/netdb.h>
@ -428,6 +429,11 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(ledc_timer_pause),
ESP_ELFSYM_EXPORT(ledc_timer_resume),
ESP_ELFSYM_EXPORT(ledc_timer_rst),
// esp_heap_caps.h
ESP_ELFSYM_EXPORT(heap_caps_get_total_size),
ESP_ELFSYM_EXPORT(heap_caps_get_allocated_size),
ESP_ELFSYM_EXPORT(heap_caps_get_free_size),
ESP_ELFSYM_EXPORT(heap_caps_get_largest_free_block),
// delimiter
ESP_ELFSYM_END
};

View File

@ -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")