| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- """ESPHome display platform driving a GxEPD2 panel via a native chunked
- refresh engine (see cycle_engine.h) instead of GxEPD2's own C++ classes.
- Panels are selected via `model:`, each backed by a hand-written (later,
- extracted) EpdPanelDescriptor - see panel_213_z98c.h / panel_426_gdeq0426t82.h
- / panel_583_gdeq0583t31.h. catalog.json/vendor/arduino_compat stay in the
- tree as reference material for panels not yet ported to this engine.
- """
- import esphome.codegen as cg
- import esphome.config_validation as cv
- from esphome import pins
- from esphome.components import display, spi
- from esphome.const import (
- CONF_BUSY_PIN,
- CONF_CS_PIN,
- CONF_DC_PIN,
- CONF_ID,
- CONF_LAMBDA,
- CONF_MODEL,
- CONF_RESET_PIN,
- CONF_SPI_ID,
- Framework,
- )
- from esphome.types import ConfigType
- CODEOWNERS = ["@zib"]
- DEPENDENCIES = ["spi"]
- # When true, the 1bpp (mono / BW+accent) draw path treats a *bright* color as
- # ink and a *dark* color as background, instead of the literal RGB mapping
- # (bright = white = blank). This exists only to ease conversion from
- # ESPHome's LCD-rooted color model: its drawing primitives default to
- # COLOR_ON = white, so with the literal mapping a default-color it.print()
- # draws nothing. We can't tell a defaulted COLOR_ON from an explicit white
- # (the default is compiled into the user's lambda as a constant), so the only
- # lever is this wholesale mono inversion - the same thing waveshare_epaper
- # bakes in. Default false (native, literal RGB); a future waveshare model
- # alias will flip the *default* to true, but an explicit setting always wins.
- # No effect on 2bpp/4bpp packed panels - there the palette maps literal RGB.
- CONF_INVERT_MONO = "invert_mono"
- gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
- GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)
- # model name -> C++ descriptor global (EpdPanelDescriptor) declared in the
- # matching panel_*.h header, which gxepd2_display.h includes unconditionally
- # - so any of these names can be selected without extra include wiring here.
- MODELS = {
- "GxEPD2_213_Z98C": "PANEL_213_Z98C",
- "GxEPD2_426_GDEQ0426T82": "PANEL_426_GDEQ0426T82",
- "GxEPD2_583_GDEQ0583T31": "PANEL_583_GDEQ0583T31",
- "GxEPD2_290c_GDEY029F51": "PANEL_290C_GDEY029F51",
- "GxEPD2_579c_GDEY0579F51": "PANEL_579C_GDEY0579F51",
- # Debug variant: master half only, as if it were a self-contained
- # 396x272 panel - see panel_579c_gdey0579f51_master_only.h. Kept
- # around as a diagnostic tool, not the panel's real mode.
- "GxEPD2_579c_GDEY0579F51_MASTER_ONLY": "PANEL_579C_GDEY0579F51_MASTER_ONLY",
- }
- CONFIG_SCHEMA = cv.All(
- display.FULL_DISPLAY_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(GxEPD2Display),
- cv.GenerateID(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
- cv.Required(CONF_MODEL): cv.one_of(*MODELS),
- cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
- cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
- cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
- cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
- # No schema default: absence falls to the alias-derived default in
- # to_code() (just False today; true for a waveshare alias once that
- # lands), while an explicit value always wins.
- cv.Optional(CONF_INVERT_MONO): cv.boolean,
- }
- ),
- # IDF-first, but the runtime C++ uses only framework-portable ESPHome
- # abstractions (spi::SPIDevice, GPIOPin, hal.h, RAMAllocator, ESP_LOG*) -
- # the old IDF-only gate was a leftover from the superseded FreeRTOS-task
- # design. ESP32-Arduino is a supported accommodation; ESP8266-Arduino
- # compiles too but is resolution-restricted at 1bpp and lacks the PSRAM
- # that >1bpp (color) panels need, so those fail allocation at runtime.
- cv.only_with_framework([Framework.ESP_IDF, Framework.ARDUINO]),
- )
- async def to_code(config: ConfigType) -> None:
- var = cg.new_Pvariable(config[CONF_ID])
- cg.add(var.set_descriptor(cg.RawExpression(f"&esphome::gxepd2::{MODELS[config[CONF_MODEL]]}")))
- cg.add(var.set_cs_pin(await cg.gpio_pin_expression(config[CONF_CS_PIN])))
- cg.add(var.set_dc_pin(await cg.gpio_pin_expression(config[CONF_DC_PIN])))
- if reset_pin := config.get(CONF_RESET_PIN):
- cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
- if busy_pin := config.get(CONF_BUSY_PIN):
- cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
- spi_parent = await cg.get_variable(config[CONF_SPI_ID])
- cg.add(var.set_spi_parent(spi_parent))
- # Alias-derived default is just False for now; the waveshare-compat layer
- # will compute it from the (aliased) model. An explicit invert_mono wins.
- cg.add(var.set_invert_mono(config.get(CONF_INVERT_MONO, False)))
- # display.register_display() only wires up `pages:` - a bare top-level
- # `lambda:` (what our CONFIG_SCHEMA actually supports) has to be
- # processed and attached via set_writer() by the platform itself, same
- # as every other display component's to_code() (see e.g. epaper_spi's
- # display.py). Missing this meant the configured lambda never ran at
- # all - the only thing ever drawn was auto_clear's default
- # fill(COLOR_OFF), which is why every real-hardware run came out solid
- # black regardless of any protocol-level change.
- 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_))
- await display.register_display(var, config)
|