display.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. """ESPHome display platform that dispatches to GxEPD2's panel driver classes.
  2. model: is validated against components/gxepd2/catalog.json, generated by
  3. tools/gxepd2_catalog/generate_catalog.py.
  4. """
  5. import json
  6. from pathlib import Path
  7. import esphome.codegen as cg
  8. import esphome.config_validation as cv
  9. from esphome import pins
  10. from esphome.components import display, spi
  11. from esphome.components.esp32 import add_idf_component
  12. from esphome.const import (
  13. CONF_BUSY_PIN,
  14. CONF_CS_PIN,
  15. CONF_DC_PIN,
  16. CONF_ID,
  17. CONF_MODEL,
  18. CONF_RESET_PIN,
  19. CONF_SPI_ID,
  20. Framework,
  21. )
  22. from esphome.core import CORE
  23. from esphome.types import ConfigType
  24. CODEOWNERS = ["@zib"]
  25. DEPENDENCIES = ["spi"]
  26. # Not in esphome/const.py: only the dual-CS "master+slave" panels need it.
  27. CONF_CS2_PIN = "cs2_pin"
  28. VENDOR_ROOT = Path(__file__).parent / "vendor"
  29. CATALOG_PATH = Path(__file__).parent / "catalog.json"
  30. CATALOG = json.loads(CATALOG_PATH.read_text())
  31. # gxepd2::GxEPD2ColorMode enumerator name for each catalog wrapper class.
  32. COLOR_MODES = {
  33. "GxEPD2_BW": "BW",
  34. "GxEPD2_3C": "THREE_COLOR",
  35. "GxEPD2_4C": "FOUR_COLOR",
  36. "GxEPD2_7C": "SEVEN_COLOR",
  37. }
  38. # USE_GXEPD2_* define gating which wrapper header gxepd2_display.h includes.
  39. WRAPPER_DEFINES = {
  40. "GxEPD2_BW": "USE_GXEPD2_BW",
  41. "GxEPD2_3C": "USE_GXEPD2_3C",
  42. "GxEPD2_4C": "USE_GXEPD2_4C",
  43. "GxEPD2_7C": "USE_GXEPD2_7C",
  44. }
  45. # IT8951 host-controller panels need genuine bidirectional SPI (they read
  46. # status/data back over MISO); arduino_compat/SPI.h's transfer() is
  47. # write-only - see the comment there - so these can never work through it.
  48. MODELS = {
  49. model_id: entry
  50. for model_id, entry in CATALOG.items()
  51. if entry["wrapper"] in COLOR_MODES and not entry["source"].startswith("it8951/")
  52. }
  53. gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
  54. GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)
  55. # GxEPD2's plain int16_t pin ids (PinRegistry keys), assigned at codegen time.
  56. _PIN_ID_KEY = "gxepd2_next_pin_id"
  57. def _allocate_pin_id() -> int:
  58. pin_id = CORE.data.get(_PIN_ID_KEY, 0)
  59. CORE.data[_PIN_ID_KEY] = pin_id + 1
  60. return pin_id
  61. # .cpp files this config needs, accumulated across every gxepd2 instance.
  62. _SOURCES_KEY = "gxepd2_needed_sources"
  63. _SHARED_SOURCE = "GxEPD2_EPD.cpp"
  64. def _write_sources_manifest(source: str) -> None:
  65. needed: set[str] = CORE.data.setdefault(_SOURCES_KEY, {_SHARED_SOURCE})
  66. needed.add(source)
  67. manifest = CORE.relative_src_path("gxepd2_sources.cmake")
  68. manifest.parent.mkdir(parents=True, exist_ok=True)
  69. lines = ["set(GXEPD2_SOURCES"]
  70. lines += [f' "${{CMAKE_CURRENT_SOURCE_DIR}}/src/{path}"' for path in sorted(needed)]
  71. lines.append(")")
  72. manifest.write_text("\n".join(lines) + "\n")
  73. def validate_cs2_pin_required(config: ConfigType) -> ConfigType:
  74. needs_cs2 = MODELS[config[CONF_MODEL]]["ctor_args"] == 5
  75. has_cs2 = CONF_CS2_PIN in config
  76. if needs_cs2 and not has_cs2:
  77. raise cv.Invalid(f"'{CONF_CS2_PIN}' is required for model {config[CONF_MODEL]} (a dual-CS panel)")
  78. if has_cs2 and not needs_cs2:
  79. raise cv.Invalid(f"'{CONF_CS2_PIN}' is not used by model {config[CONF_MODEL]}")
  80. return config
  81. CONFIG_SCHEMA = cv.All(
  82. display.FULL_DISPLAY_SCHEMA.extend(
  83. {
  84. cv.GenerateID(): cv.declare_id(GxEPD2Display),
  85. cv.GenerateID(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
  86. cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=False),
  87. cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
  88. cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
  89. cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
  90. cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
  91. cv.Optional(CONF_CS2_PIN): pins.gpio_output_pin_schema,
  92. }
  93. ),
  94. cv.only_with_framework(Framework.ESP_IDF),
  95. validate_cs2_pin_required,
  96. )
  97. async def to_code(config: ConfigType) -> None:
  98. entry = MODELS[config[CONF_MODEL]]
  99. driver_t = cg.global_ns.class_(entry["driver"])
  100. page_height = f"{entry['driver']}::HEIGHT / {entry['height_divisor']}"
  101. display_t = cg.RawExpression(f"{entry['wrapper']}<{entry['driver']}, {page_height}>")
  102. color_mode = cg.RawExpression(f"gxepd2::GxEPD2ColorMode::{COLOR_MODES[entry['wrapper']]}")
  103. dual_cs = entry["ctor_args"] == 5
  104. specific_type = GxEPD2Display.template(driver_t, display_t, color_mode, dual_cs)
  105. cg.add_define(WRAPPER_DEFINES[entry["wrapper"]])
  106. add_idf_component(name=VENDOR_ROOT.name, path=str(VENDOR_ROOT))
  107. _write_sources_manifest(entry["source"])
  108. cs_id, dc_id = _allocate_pin_id(), _allocate_pin_id()
  109. reset_id = _allocate_pin_id() if CONF_RESET_PIN in config else -1
  110. busy_id = _allocate_pin_id() if CONF_BUSY_PIN in config else -1
  111. ctor_args = [cs_id, dc_id, reset_id, busy_id]
  112. if entry["ctor_args"] == 5:
  113. cs2_id = _allocate_pin_id()
  114. ctor_args.append(cs2_id)
  115. rhs = specific_type.new(*ctor_args)
  116. var = cg.Pvariable(config[CONF_ID], rhs, specific_type)
  117. cg.add(var.set_pin(cs_id, await cg.gpio_pin_expression(config[CONF_CS_PIN])))
  118. cg.add(var.set_pin(dc_id, await cg.gpio_pin_expression(config[CONF_DC_PIN])))
  119. if reset_pin := config.get(CONF_RESET_PIN):
  120. cg.add(var.set_pin(reset_id, await cg.gpio_pin_expression(reset_pin)))
  121. if busy_pin := config.get(CONF_BUSY_PIN):
  122. cg.add(var.set_pin(busy_id, await cg.gpio_pin_expression(busy_pin)))
  123. if entry["ctor_args"] == 5:
  124. cg.add(var.set_pin(cs2_id, await cg.gpio_pin_expression(config[CONF_CS2_PIN])))
  125. spi_parent = await cg.get_variable(config[CONF_SPI_ID])
  126. cg.add(var.set_spi_parent(spi_parent))
  127. await display.register_display(var, config)