display.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """ESPHome display platform driving a zEPD engine model via the native
  2. replay engine (see engine/engine.h) instead of GxEPD2's own C++ classes.
  3. Models are selected via `model:`, each a drivers/<model>.json merged over
  4. drivers/default.json (see registry.py) and codegen'd at compile time to a
  5. single generated header (see codegen.py) - no per-model panel_*.h, and no
  6. `PANELS` dict here. See claude/DESCRIPTOR_REGISTRY_PLAN.md.
  7. """
  8. import re
  9. import esphome.codegen as cg
  10. import esphome.config_validation as cv
  11. from esphome import pins
  12. from esphome.components import display, spi
  13. from esphome.const import (
  14. CONF_BUSY_PIN,
  15. CONF_CS_PIN,
  16. CONF_DC_PIN,
  17. CONF_ID,
  18. CONF_LAMBDA,
  19. CONF_MODEL,
  20. CONF_RESET_PIN,
  21. CONF_SPI_ID,
  22. Framework,
  23. )
  24. from esphome.core import CORE
  25. from esphome.helpers import write_file_if_changed
  26. from esphome.types import ConfigType
  27. from . import codegen as zepd_codegen
  28. from . import registry as zepd_registry
  29. CODEOWNERS = ["@zib"]
  30. DEPENDENCIES = ["spi"]
  31. # See the matching comment in the legacy components/gxepd2/display.py - same
  32. # mono-inversion tradeoff, carried forward unchanged.
  33. CONF_INVERT_MONO = "invert_mono"
  34. # Compiles in ZepdDisplay's dev-time diagnostic methods (currently
  35. # diag_update_no_reset()), stashed behind this flag so a production build
  36. # doesn't pay flash for bench-only tooling. Off by default.
  37. CONF_DIAGNOSTICS = "diagnostics"
  38. zepd_ns = cg.esphome_ns.namespace("zepd")
  39. ZepdDisplay = zepd_ns.class_("ZepdDisplay", display.DisplayBuffer)
  40. MODELS = zepd_registry.list_models()
  41. # Human-facing label for test_pattern(): the first OEM part number minus its
  42. # GDEx series prefix (GDEQ0583T31 -> 0583T31), same rule as the legacy
  43. # _panel_label. Falls back to the registry key itself when a model carries
  44. # no oem_part_numbers yet.
  45. def _model_label(model_key: str, model: dict) -> str:
  46. parts = model.get("oem_part_numbers") or []
  47. part = parts[0] if parts else model_key
  48. return re.sub(r"^GDE[A-Z]", "", part)
  49. CONFIG_SCHEMA = cv.All(
  50. display.FULL_DISPLAY_SCHEMA.extend(
  51. {
  52. cv.GenerateID(): cv.declare_id(ZepdDisplay),
  53. cv.GenerateID(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
  54. cv.Required(CONF_MODEL): cv.one_of(*MODELS),
  55. cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
  56. cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
  57. cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
  58. cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
  59. cv.Optional(CONF_INVERT_MONO): cv.boolean,
  60. cv.Optional(CONF_DIAGNOSTICS, default=False): cv.boolean,
  61. }
  62. ),
  63. # Same framework support rule as the legacy component - the runtime C++
  64. # only uses portable ESPHome abstractions.
  65. cv.only_with_framework([Framework.ESP_IDF, Framework.ARDUINO]),
  66. )
  67. async def to_code(config: ConfigType) -> None:
  68. var = cg.new_Pvariable(config[CONF_ID])
  69. model = zepd_registry.load_model(config[CONF_MODEL])
  70. # Codegen the one selected model to a fixed-name generated header at
  71. # src/ root - not under this component's own directory, which
  72. # writer.copy_src_tree() would otherwise delete on every build (see
  73. # claude/DESCRIPTOR_REGISTRY_PLAN.md, "Where generated C++ lands").
  74. cpp_body = zepd_codegen.generate_cpp(model, "ZEPD_MODEL")
  75. # Wrapped in esphome::zepd, the same namespace as the adapter
  76. # (ZepdDisplay) and the engine core (EngineModel, Engine) both live in -
  77. # see engine_model.h/engine.h. Keeping a single namespace avoids the
  78. # ambiguity ESPHome's own generated main.cpp would otherwise hit: it
  79. # writes unqualified `zepd::Foo` trusting `using namespace esphome;` to
  80. # resolve it, which breaks if a second, unrelated top-level `zepd`
  81. # namespace also exists.
  82. generated = (
  83. "#pragma once\n"
  84. '#include "esphome/components/zepd/engine_model.h"\n\n'
  85. "namespace esphome::zepd {\n\n"
  86. f"{cpp_body}\n"
  87. "} // namespace esphome::zepd\n"
  88. )
  89. write_file_if_changed(CORE.relative_src_path("zepd_model_generated.h"), generated)
  90. cg.add(var.set_descriptor(cg.RawExpression("&esphome::zepd::ZEPD_MODEL")))
  91. cg.add(var.set_cs_pin(await cg.gpio_pin_expression(config[CONF_CS_PIN])))
  92. cg.add(var.set_dc_pin(await cg.gpio_pin_expression(config[CONF_DC_PIN])))
  93. if reset_pin := config.get(CONF_RESET_PIN):
  94. cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
  95. if busy_pin := config.get(CONF_BUSY_PIN):
  96. cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
  97. spi_parent = await cg.get_variable(config[CONF_SPI_ID])
  98. cg.add(var.set_spi_parent(spi_parent))
  99. cg.add(var.set_invert_mono(config.get(CONF_INVERT_MONO, False)))
  100. cg.add(var.set_model_name(_model_label(config[CONF_MODEL], model)))
  101. cg.add(var.set_verified(bool(model.get("verified", False))))
  102. if config[CONF_DIAGNOSTICS]:
  103. cg.add_define("USE_ZEPD_DIAGNOSTICS")
  104. # A top-level `lambda:` must be processed and attached via set_writer()
  105. # here - register_display() only wires up `pages:`.
  106. if CONF_LAMBDA in config:
  107. lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void)
  108. cg.add(var.set_writer(lambda_))
  109. await display.register_display(var, config)