display.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """ESPHome display platform driving a GxEPD2 panel via a native chunked
  2. refresh engine (see cycle_engine.h) instead of GxEPD2's own C++ classes.
  3. Panels are selected via `model:`, each backed by a hand-written (later,
  4. extracted) EpdPanelDescriptor - see panel_213_z98c.h / panel_426_gdeq0426t82.h
  5. / panel_583_gdeq0583t31.h. catalog.json/vendor/arduino_compat stay in the
  6. tree as reference material for panels not yet ported to this engine.
  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, font, 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.types import ConfigType
  25. CODEOWNERS = ["@zib"]
  26. DEPENDENCIES = ["spi"]
  27. # When true, the 1bpp (mono / BW+accent) draw path treats a *bright* color as
  28. # ink and a *dark* color as background, instead of the literal RGB mapping
  29. # (bright = white = blank). This exists only to ease conversion from
  30. # ESPHome's LCD-rooted color model: its drawing primitives default to
  31. # COLOR_ON = white, so with the literal mapping a default-color it.print()
  32. # draws nothing. We can't tell a defaulted COLOR_ON from an explicit white
  33. # (the default is compiled into the user's lambda as a constant), so the only
  34. # lever is this wholesale mono inversion - the same thing waveshare_epaper
  35. # bakes in. Default false (native, literal RGB); a future waveshare model
  36. # alias will flip the *default* to true, but an explicit setting always wins.
  37. # No effect on 2bpp/4bpp packed panels - there the palette maps literal RGB.
  38. CONF_INVERT_MONO = "invert_mono"
  39. # Optional font for the on-screen panel label test_pattern() draws (the model
  40. # trailer, e.g. "GDEQ0583T31"). A normal ESPHome font:, so the builder can
  41. # pull one from gfonts:// - no font, no label. See GxEPD2Display::test_pattern.
  42. CONF_NAME_FONT = "name_font"
  43. gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
  44. GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)
  45. # model name -> (C++ descriptor global declared in the matching panel_*.h
  46. # header - gxepd2_display.h includes them all, so no extra include wiring here;
  47. # Good Display part number on the panel's silkscreen).
  48. #
  49. # The part number, not the GxEPD2 class name, is the source of truth for the
  50. # on-screen label (see _panel_label / test_pattern): GxEPD2's older naming
  51. # style drops the GDEx prefix and the 4-digit size - GxEPD2_213_Z98C is really
  52. # GDEY0213Z98 - so the class name alone can't reconstruct what's on the tag.
  53. MODELS = {
  54. "GxEPD2_154_Z90c": ("PANEL_154_Z90C", "GDEH0154Z90"),
  55. "GxEPD2_213_Z98C": ("PANEL_213_Z98C", "GDEY0213Z98"),
  56. "GxEPD2_213c_GDEY0213F51": ("PANEL_213C_GDEY0213F51", "GDEY0213F51"),
  57. "GxEPD2_426_GDEQ0426T82": ("PANEL_426_GDEQ0426T82", "GDEQ0426T82"),
  58. "GxEPD2_583_GDEQ0583T31": ("PANEL_583_GDEQ0583T31", "GDEQ0583T31"),
  59. "GxEPD2_290c_GDEY029F51": ("PANEL_290C_GDEY029F51", "GDEY029F51"),
  60. "GxEPD2_579c_GDEY0579F51": ("PANEL_579C_GDEY0579F51", "GDEY0579F51"),
  61. # Debug variant: master half only, as if it were a self-contained
  62. # 396x272 panel - see panel_579c_gdey0579f51_master_only.h. Kept
  63. # around as a diagnostic tool, not the panel's real mode. Same physical
  64. # part as the full panel, so same label.
  65. "GxEPD2_579c_GDEY0579F51_MASTER_ONLY": ("PANEL_579C_GDEY0579F51_MASTER_ONLY", "GDEY0579F51"),
  66. }
  67. # Good Display part number -> shelf label: drop the 4-char GDEx series prefix,
  68. # keeping just the size + type (GDEQ0583T31 -> 0583T31, GDEY0213Z98 ->
  69. # 0213Z98). The GDEx series/generation letter encodes nothing we act on and
  70. # costs scarce width on small panels; size stays encoded once, in the digits.
  71. # Anything not starting with GDE<letter> is returned untouched.
  72. def _panel_label(part: str) -> str:
  73. return re.sub(r"^GDE[A-Z]", "", part)
  74. CONFIG_SCHEMA = cv.All(
  75. display.FULL_DISPLAY_SCHEMA.extend(
  76. {
  77. cv.GenerateID(): cv.declare_id(GxEPD2Display),
  78. cv.GenerateID(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
  79. cv.Required(CONF_MODEL): cv.one_of(*MODELS),
  80. cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
  81. cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
  82. cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
  83. cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
  84. # No schema default: absence falls to the alias-derived default in
  85. # to_code() (just False today; true for a waveshare alias once that
  86. # lands), while an explicit value always wins.
  87. cv.Optional(CONF_INVERT_MONO): cv.boolean,
  88. cv.Optional(CONF_NAME_FONT): cv.use_id(font.Font),
  89. }
  90. ),
  91. # IDF-first, but the runtime C++ uses only framework-portable ESPHome
  92. # abstractions (spi::SPIDevice, GPIOPin, hal.h, RAMAllocator, ESP_LOG*) -
  93. # the old IDF-only gate was a leftover from the superseded FreeRTOS-task
  94. # design. ESP32-Arduino is a supported accommodation; ESP8266-Arduino
  95. # compiles too but is resolution-restricted at 1bpp and lacks the PSRAM
  96. # that >1bpp (color) panels need, so those fail allocation at runtime.
  97. cv.only_with_framework([Framework.ESP_IDF, Framework.ARDUINO]),
  98. )
  99. async def to_code(config: ConfigType) -> None:
  100. var = cg.new_Pvariable(config[CONF_ID])
  101. descriptor, part_number = MODELS[config[CONF_MODEL]]
  102. cg.add(var.set_descriptor(cg.RawExpression(f"&esphome::gxepd2::{descriptor}")))
  103. cg.add(var.set_cs_pin(await cg.gpio_pin_expression(config[CONF_CS_PIN])))
  104. cg.add(var.set_dc_pin(await cg.gpio_pin_expression(config[CONF_DC_PIN])))
  105. if reset_pin := config.get(CONF_RESET_PIN):
  106. cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
  107. if busy_pin := config.get(CONF_BUSY_PIN):
  108. cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
  109. spi_parent = await cg.get_variable(config[CONF_SPI_ID])
  110. cg.add(var.set_spi_parent(spi_parent))
  111. # Alias-derived default is just False for now; the waveshare-compat layer
  112. # will compute it from the (aliased) model. An explicit invert_mono wins.
  113. cg.add(var.set_invert_mono(config.get(CONF_INVERT_MONO, False)))
  114. # Panel label for test_pattern(): the Good Display part number in the
  115. # vendor's dashed silkscreen form (GDEQ-0583T31), so a tested panel shows
  116. # what it is on the shelf, not the GxEPD2 C++ class id.
  117. cg.add(var.set_model_name(_panel_label(part_number)))
  118. if name_font := config.get(CONF_NAME_FONT):
  119. cg.add(var.set_name_font(await cg.get_variable(name_font)))
  120. # display.register_display() only wires up `pages:` - a bare top-level
  121. # `lambda:` (what our CONFIG_SCHEMA actually supports) has to be
  122. # processed and attached via set_writer() by the platform itself, same
  123. # as every other display component's to_code() (see e.g. epaper_spi's
  124. # display.py). Missing this meant the configured lambda never ran at
  125. # all - the only thing ever drawn was auto_clear's default
  126. # fill(COLOR_OFF), which is why every real-hardware run came out solid
  127. # black regardless of any protocol-level change.
  128. if CONF_LAMBDA in config:
  129. lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void)
  130. cg.add(var.set_writer(lambda_))
  131. await display.register_display(var, config)