From 6d427bec495b99b4fc504728eb1e4b2de97a4cad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 13 Sep 2025 10:01:42 +1200 Subject: [PATCH] [epdiy] ePaper display(s) --- CODEOWNERS | 1 + esphome/components/epdiy/__init__.py | 0 esphome/components/epdiy/display.py | 106 +++++++++++++++++++++ esphome/components/epdiy/epdiy_display.cpp | 76 +++++++++++++++ esphome/components/epdiy/epdiy_display.h | 63 ++++++++++++ esphome/idf_component.yml | 3 + 6 files changed, 249 insertions(+) create mode 100644 esphome/components/epdiy/__init__.py create mode 100644 esphome/components/epdiy/display.py create mode 100644 esphome/components/epdiy/epdiy_display.cpp create mode 100644 esphome/components/epdiy/epdiy_display.h diff --git a/CODEOWNERS b/CODEOWNERS index dc567ca5c0..8cb78c1239 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/epdiy/* @jesserockz esphome/components/es7210/* @kahrendt esphome/components/es7243e/* @kbx81 esphome/components/es8156/* @kbx81 diff --git a/esphome/components/epdiy/__init__.py b/esphome/components/epdiy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/epdiy/display.py b/esphome/components/epdiy/display.py new file mode 100644 index 0000000000..ce14c0ed3b --- /dev/null +++ b/esphome/components/epdiy/display.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +from esphome.components import display, esp32 +import esphome.config_validation as cv +from esphome.const import ( + CONF_FULL_UPDATE_EVERY, + CONF_ID, + CONF_LAMBDA, + CONF_MODEL, + CONF_PAGES, +) +from esphome.cpp_generator import MockObj + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32", "psram"] + +CONF_POWER_OFF_DELAY_ENABLED = "power_off_delay_enabled" + +epdiy_ns = cg.esphome_ns.namespace("epdiy") +EPDiyDisplay = epdiy_ns.class_("EPDiyDisplay", display.Display) + + +class EpdBoardDefinition(MockObj): + def __str__(self): + return f"&{self.base}" + + +class EpdDisplay_t(MockObj): + def __str__(self): + return f"&{self.base}" + + +EpdInitOptions = cg.global_ns.enum("EpdInitOptions") + + +class Model: + def __init__( + self, + *, + board_definition: MockObj, + display_t: MockObj, + init_options: MockObj, + width: int, + height: int, + vcom_mv: int = 0, + ): + self.board_definition = board_definition + self.display_t = display_t + self.init_options = init_options + self.width = width + self.height = height + self.vcom_mv = vcom_mv + + +MODELS: dict[str, Model] = { + "lilygo_t5_4.7": Model( + board_definition=EpdBoardDefinition("epd_board_lilygo_t5_47"), + display_t=EpdDisplay_t("ED047TC2"), + init_options=(EpdInitOptions.EPD_LUT_64K, EpdInitOptions.EPD_FEED_QUEUE_8), + width=960, + height=540, + ), +} + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EPDiyDisplay), + cv.Required(CONF_MODEL): cv.one_of(*MODELS.keys()), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, + cv.Optional(CONF_POWER_OFF_DELAY_ENABLED, default=False): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await display.register_display(var, config) + + model = MODELS[config[CONF_MODEL]] + cg.add( + var.set_model_details( + model.board_definition, + model.display_t, + cg.RawExpression( + f"static_cast({'|'.join(str(o) for o in model.init_options)})" + ), + model.vcom_mv, + ) + ) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + cg.add(var.set_power_off_delay_enabled(config[CONF_POWER_OFF_DELAY_ENABLED])) + + esp32.add_idf_component( + name="vroland/epdiy", + repo="https://github.com/vroland/epdiy", + ref="c61e9e923ce2418150d54f88cea5d196cdc40c54", + ) diff --git a/esphome/components/epdiy/epdiy_display.cpp b/esphome/components/epdiy/epdiy_display.cpp new file mode 100644 index 0000000000..5e1b6ac13a --- /dev/null +++ b/esphome/components/epdiy/epdiy_display.cpp @@ -0,0 +1,76 @@ +#include "epdiy_display.h" + +#include "esphome/core/application.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +namespace esphome::epdiy { + +static const char *const TAG = "epdiy"; + +static constexpr uint8_t TEMPERATURE = 23; // default temperature for e-paper displays + +float EPDiyDisplay::get_setup_priority() const { return esphome::setup_priority::LATE; } + +void EPDiyDisplay::setup() { + epd_init(this->board_definition_, this->display_t_, this->init_options_); + if (this->vcom_mv_ != 0) { + epd_set_vcom(this->vcom_mv_); + } + this->state_ = epd_hl_init(nullptr); + this->framebuffer_ = epd_hl_get_framebuffer(&this->state_); +} + +void EPDiyDisplay::update() { + this->do_update_(); + this->defer([this]() { this->flush_screen_changes_(); }); +} + +void EPDiyDisplay::fill(Color color) { + if (color == display::COLOR_OFF) { + memset(this->framebuffer_, 0xFF, this->get_buffer_length()); + + epd_poweron(); + epd_hl_update_screen(&this->state_, MODE_GC16, TEMPERATURE); + epd_clear(); + + epd_poweroff(); + App.feed_wdt(); + } else { + Display::fill(color); + } +} + +void EPDiyDisplay::flush_screen_changes_() { + epd_poweron(); + + epd_hl_update_screen(&this->state_, MODE_GC16, TEMPERATURE); + memset(this->state_.back_fb, 0xFF, this->get_buffer_length()); + + uint16_t delay = 0; + if (this->power_off_delay_enabled_) { + delay = 700; + } + this->set_timeout("poweroff", delay, []() { epd_poweroff(); }); +} + +void EPDiyDisplay::on_shutdown() { + epd_poweroff(); + epd_deinit(); +} + +void HOT EPDiyDisplay::draw_pixel_at(int x, int y, Color color) { + if (color.red == 255 && color.green == 255 && color.blue == 255) { + epd_draw_pixel(x, y, 0, this->framebuffer_); + } else { + int col = (0.2126 * color.red) + (0.7152 * color.green) + (0.0722 * color.blue); + int cl = 255 - col; + epd_draw_pixel(x, y, cl, this->framebuffer_); + } +} + +} // namespace esphome::epdiy + +#endif // USE_ESP32 diff --git a/esphome/components/epdiy/epdiy_display.h b/esphome/components/epdiy/epdiy_display.h new file mode 100644 index 0000000000..301516485b --- /dev/null +++ b/esphome/components/epdiy/epdiy_display.h @@ -0,0 +1,63 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" + +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display_color_utils.h" +#include "esphome/core/hal.h" +#include "esphome/core/version.h" + +#include "epd_display.h" +#include "epd_highlevel.h" + +namespace esphome::epdiy { + +class EPDiyDisplay : public display::Display { + public: + float get_setup_priority() const override; + void setup() override; + void update() override; + void on_shutdown() override; + + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; } + + int get_width_internal() override { return this->display_t_->width; }; + int get_height_internal() override { return this->display_t_->height; }; + + size_t get_buffer_length() const { return this->display_t_->width / 2 * this->display_t_->height; } + + void set_power_off_delay_enabled(bool power_off_delay_enabled) { + this->power_off_delay_enabled_ = power_off_delay_enabled; + } + + void set_model_details(const EpdBoardDefinition *board_definition, const EpdDisplay_t *display_t, + enum EpdInitOptions init_options, uint16_t vcom) { + this->board_definition_ = board_definition; + this->display_t_ = display_t; + this->init_options_ = init_options; + this->vcom_mv_ = vcom; + } + + void fill(Color color) override; + + void draw_pixel_at(int x, int y, Color color) override; + + protected: + void flush_screen_changes_(); + EpdiyHighlevelState state_; + + uint8_t *framebuffer_; + + const EpdBoardDefinition *board_definition_; + const EpdDisplay_t *display_t_; + enum EpdInitOptions init_options_; + uint16_t vcom_mv_; + + bool power_off_delay_enabled_; +}; + +} // namespace esphome::epdiy + +#endif // USE_ESP32 diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 687efd2b49..2c5ac43987 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -19,3 +19,6 @@ dependencies: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: version: 1.0.1 + vroland/epdiy: + git: https://github.com/vroland/epdiy.git + version: c61e9e923ce2418150d54f88cea5d196cdc40c54