| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- """ESPHome display platform driving a zEPD engine model via the native
- replay engine (see engine/engine.h) instead of GxEPD2's own C++ classes.
- Models are selected via `model:`, each a drivers/<model>.json merged over
- drivers/default.json (see registry.py) and codegen'd at compile time to a
- single generated header (see codegen.py) - no per-model panel_*.h, and no
- `PANELS` dict here. See claude/DESCRIPTOR_REGISTRY_PLAN.md.
- """
- import re
- 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.core import CORE
- from esphome.helpers import write_file_if_changed
- from esphome.types import ConfigType
- from . import codegen as zepd_codegen
- from . import registry as zepd_registry
- CODEOWNERS = ["@zib"]
- DEPENDENCIES = ["spi"]
- # See the matching comment in the legacy components/gxepd2/display.py - same
- # mono-inversion tradeoff, carried forward unchanged.
- CONF_INVERT_MONO = "invert_mono"
- # Compiles in ZepdDisplay's dev-time diagnostic methods (currently
- # diag_update_no_reset()), stashed behind this flag so a production build
- # doesn't pay flash for bench-only tooling. Off by default.
- CONF_DIAGNOSTICS = "diagnostics"
- zepd_ns = cg.esphome_ns.namespace("zepd")
- ZepdDisplay = zepd_ns.class_("ZepdDisplay", display.DisplayBuffer)
- MODELS = zepd_registry.list_models()
- # Human-facing label for test_pattern(): the first OEM part number minus its
- # GDEx series prefix (GDEQ0583T31 -> 0583T31), same rule as the legacy
- # _panel_label. Falls back to the registry key itself when a model carries
- # no oem_part_numbers yet.
- def _model_label(model_key: str, model: dict) -> str:
- parts = model.get("oem_part_numbers") or []
- part = parts[0] if parts else model_key
- return re.sub(r"^GDE[A-Z]", "", part)
- CONFIG_SCHEMA = cv.All(
- display.FULL_DISPLAY_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(ZepdDisplay),
- 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,
- cv.Optional(CONF_INVERT_MONO): cv.boolean,
- cv.Optional(CONF_DIAGNOSTICS, default=False): cv.boolean,
- }
- ),
- # Same framework support rule as the legacy component - the runtime C++
- # only uses portable ESPHome abstractions.
- cv.only_with_framework([Framework.ESP_IDF, Framework.ARDUINO]),
- )
- async def to_code(config: ConfigType) -> None:
- var = cg.new_Pvariable(config[CONF_ID])
- model = zepd_registry.load_model(config[CONF_MODEL])
- # Codegen the one selected model to a fixed-name generated header at
- # src/ root - not under this component's own directory, which
- # writer.copy_src_tree() would otherwise delete on every build (see
- # claude/DESCRIPTOR_REGISTRY_PLAN.md, "Where generated C++ lands").
- cpp_body = zepd_codegen.generate_cpp(model, "ZEPD_MODEL")
- # Wrapped in esphome::zepd, the same namespace as the adapter
- # (ZepdDisplay) and the engine core (EngineModel, Engine) both live in -
- # see engine_model.h/engine.h. Keeping a single namespace avoids the
- # ambiguity ESPHome's own generated main.cpp would otherwise hit: it
- # writes unqualified `zepd::Foo` trusting `using namespace esphome;` to
- # resolve it, which breaks if a second, unrelated top-level `zepd`
- # namespace also exists.
- generated = (
- "#pragma once\n"
- '#include "esphome/components/zepd/engine_model.h"\n\n'
- "namespace esphome::zepd {\n\n"
- f"{cpp_body}\n"
- "} // namespace esphome::zepd\n"
- )
- write_file_if_changed(CORE.relative_src_path("zepd_model_generated.h"), generated)
- cg.add(var.set_descriptor(cg.RawExpression("&esphome::zepd::ZEPD_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))
- cg.add(var.set_invert_mono(config.get(CONF_INVERT_MONO, False)))
- cg.add(var.set_model_name(_model_label(config[CONF_MODEL], model)))
- cg.add(var.set_verified(bool(model.get("verified", False))))
- if config[CONF_DIAGNOSTICS]:
- cg.add_define("USE_ZEPD_DIAGNOSTICS")
- # A top-level `lambda:` must be processed and attached via set_writer()
- # here - register_display() only wires up `pages:`.
- 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)
|