display.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """ESPHome display platform driving a GxEPD2 panel via the native replay
  2. engine (see cycle_engine.h) instead of GxEPD2's own C++ classes.
  3. Panels are selected via `model:`, each backed by an EpdPanelDescriptor
  4. (panel_*.h) generated by the capture emitter - see CONTRIBUTING.md.
  5. """
  6. import re
  7. from collections import namedtuple
  8. import esphome.codegen as cg
  9. import esphome.config_validation as cv
  10. from esphome import pins
  11. from esphome.components import display, spi
  12. from esphome.const import (
  13. CONF_BUSY_PIN,
  14. CONF_CS_PIN,
  15. CONF_DC_PIN,
  16. CONF_ID,
  17. CONF_LAMBDA,
  18. CONF_MODEL,
  19. CONF_RESET_PIN,
  20. CONF_SPI_ID,
  21. Framework,
  22. )
  23. from esphome.types import ConfigType
  24. CODEOWNERS = ["@zib"]
  25. DEPENDENCIES = ["spi"]
  26. # When true, the 1bpp (mono / BW+accent) draw path treats a *bright* color as
  27. # ink and a *dark* color as background, instead of the literal RGB mapping
  28. # (bright = white = blank). This exists only to ease conversion from
  29. # ESPHome's LCD-rooted color model: its drawing primitives default to
  30. # COLOR_ON = white, so with the literal mapping a default-color it.print()
  31. # draws nothing. We can't tell a defaulted COLOR_ON from an explicit white
  32. # (the default is compiled into the user's lambda as a constant), so the only
  33. # lever is this wholesale mono inversion - the same thing waveshare_epaper
  34. # bakes in. Default false (native, literal RGB); a future waveshare model
  35. # alias will flip the *default* to true, but an explicit setting always wins.
  36. # No effect on 2bpp/4bpp packed panels - there the palette maps literal RGB.
  37. CONF_INVERT_MONO = "invert_mono"
  38. # Compiles in GxEPD2Display's dev-time diagnostic methods (currently
  39. # diag_update_no_reset() - see gxepd2_display.h), stashed behind this flag so
  40. # a production build doesn't pay flash for bench-only tooling. Off by
  41. # default; set true only on the device you're actively debugging.
  42. CONF_DIAGNOSTICS = "diagnostics"
  43. gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
  44. GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)
  45. # The panel registry - the single source of truth for which panels ship, their
  46. # metadata, and whether each is hardware-verified. tools/panel_table.py renders
  47. # the supported-panels table from this, and set_verified() surfaces the status
  48. # in the device's boot log.
  49. #
  50. # descriptor: the C++ EpdPanelDescriptor global declared in the matching
  51. # panel_*.h (all `#include`d by gxepd2_display.h, so no extra wiring here).
  52. # part: the Good Display part number on the silkscreen - the source of truth for
  53. # the on-screen label (GxEPD2's class name drops the GDEx prefix and size, so
  54. # GxEPD2_213_Z98C alone can't reconstruct GDEY0213Z98).
  55. # status: "verified" (rendered correctly on real hardware), "structural"
  56. # (descriptor captured and compiles, not yet bench-confirmed), or "diagnostic".
  57. Panel = namedtuple("Panel", "descriptor part controller fmt status")
  58. PANELS = {
  59. "GxEPD2_154_Z90c": Panel("PANEL_154_Z90C", "GDEH0154Z90", "SSD1681", "3-color", "verified"),
  60. "GxEPD2_213_B72": Panel("PANEL_213_B72", "GDEH0213B72", "SSD1675A", "mono", "structural"),
  61. "GxEPD2_213_Z98C": Panel("PANEL_213_Z98C", "GDEY0213Z98", "SSD1680", "3-color", "verified"),
  62. "GxEPD2_213c_GDEY0213F51": Panel("PANEL_213C_GDEY0213F51", "GDEY0213F51", "JD79661", "4-color", "structural"),
  63. "GxEPD2_213c_GDEY0213F52": Panel("PANEL_213C_GDEY0213F52", "GDEY0213F52", "JD79676", "4-color", "verified"),
  64. "GxEPD2_270": Panel("PANEL_270", "GDEW027W3", "EK79652", "mono", "structural"),
  65. "GxEPD2_290_T5D": Panel("PANEL_290_T5D", "GDEW029T5D", "UC8151D", "mono", "structural"),
  66. "GxEPD2_420": Panel("PANEL_420", "GDEW042T2", "UC8176", "mono", "structural"),
  67. "GxEPD2_426_GDEQ0426T82": Panel("PANEL_426_GDEQ0426T82", "GDEQ0426T82", "SSD1677", "mono", "verified"),
  68. "GxEPD2_583": Panel("PANEL_583", "GDEW0583T7", "UC8159c", "mono", "structural"),
  69. "GxEPD2_583_GDEQ0583T31": Panel("PANEL_583_GDEQ0583T31", "GDEQ0583T31", "UC8179", "mono", "verified"),
  70. "GxEPD2_730c_GDEP073E01": Panel("PANEL_730C_GDEP073E01", "GDEP073E01", "unknown", "7-color", "verified"),
  71. "GxEPD2_750_GDEY075T7": Panel("PANEL_750_GDEY075T7", "GDEY075T7", "UC8179", "mono", "verified"),
  72. "GxEPD2_290c_GDEY029F51": Panel("PANEL_290C_GDEY029F51", "GDEY029F51", "JD79667", "4-color", "structural"),
  73. "GxEPD2_579c_GDEY0579F51": Panel("PANEL_579C_GDEY0579F51", "GDEY0579F51", "HX8717", "4-color", "verified"),
  74. "GxEPD2_154": Panel("PANEL_154", "TODO", "TODO", "mono", "structural"),
  75. "GxEPD2_1020_GDEM102T91": Panel("PANEL_1020_GDEM102T91", "GDEM102T91", "TODO", "mono", "structural"),
  76. "GxEPD2_266_BN": Panel("PANEL_266_BN", "TODO", "TODO", "mono", "structural"),
  77. "GxEPD2_102": Panel("PANEL_102", "GDEW0102T4", "UC8175", "mono", "structural"),
  78. "GxEPD2_1160_T91": Panel("PANEL_1160_T91", "GDEH116T91", "TODO", "mono", "structural"),
  79. "GxEPD2_1330_GDEM133T91": Panel("PANEL_1330_GDEM133T91", "GDEM133T91", "TODO", "mono", "structural"),
  80. "GxEPD2_150_BN": Panel("PANEL_150_BN", "TODO", "TODO", "mono", "structural"),
  81. "GxEPD2_154_D67": Panel("PANEL_154_D67", "GDEH0154D67", "TODO", "mono", "structural"),
  82. "GxEPD2_154_GDEY0154D67": Panel("PANEL_154_GDEY0154D67", "GDEY0154D67", "TODO", "mono", "structural"),
  83. "GxEPD2_154_M09": Panel("PANEL_154_M09", "GDEW0154M09", "TODO", "mono", "structural"),
  84. "GxEPD2_154_M10": Panel("PANEL_154_M10", "GDEW0154M10", "TODO", "mono", "structural"),
  85. "GxEPD2_154_T8": Panel("PANEL_154_T8", "GDEW0154T8", "IL0373", "mono", "structural"),
  86. "GxEPD2_213": Panel("PANEL_213", "TODO", "IL3895", "mono", "structural"),
  87. "GxEPD2_213_B73": Panel("PANEL_213_B73", "GDEH0213B73", "SSD1675B", "mono", "structural"),
  88. "GxEPD2_213_B74": Panel("PANEL_213_B74", "GDEM0213B74", "TODO", "mono", "structural"),
  89. "GxEPD2_213_BN": Panel("PANEL_213_BN", "TODO", "TODO", "mono", "structural"),
  90. "GxEPD2_213_GDEY0213B74": Panel("PANEL_213_GDEY0213B74", "GDEY0213B74", "TODO", "mono", "structural"),
  91. "GxEPD2_213_M21": Panel("PANEL_213_M21", "TODO", "UC8151", "mono", "structural"),
  92. "GxEPD2_213_T5D": Panel("PANEL_213_T5D", "TODO", "UC8151D", "mono", "structural"),
  93. "GxEPD2_213_flex": Panel("PANEL_213_FLEX", "TODO", "IL0373", "mono", "structural"),
  94. "GxEPD2_260": Panel("PANEL_260", "GDEW026T0", "IL0373", "mono", "structural"),
  95. "GxEPD2_260_M01": Panel("PANEL_260_M01", "TODO", "UC8151", "mono", "structural"),
  96. "GxEPD2_270_GDEY027T91": Panel("PANEL_270_GDEY027T91", "GDEY027T91", "TODO", "mono", "structural"),
  97. "GxEPD2_290": Panel("PANEL_290", "TODO", "IL3820", "mono", "structural"),
  98. "GxEPD2_290_BS": Panel("PANEL_290_BS", "TODO", "TODO", "mono", "structural"),
  99. "GxEPD2_290_GDEY029T71H": Panel("PANEL_290_GDEY029T71H", "GDEY029T71H", "TODO", "mono", "structural"),
  100. "GxEPD2_290_GDEY029T94": Panel("PANEL_290_GDEY029T94", "GDEY029T94", "TODO", "mono", "structural"),
  101. "GxEPD2_290_I6FD": Panel("PANEL_290_I6FD", "GDEW029I6FD", "UC8151D", "mono", "structural"),
  102. "GxEPD2_290_M06": Panel("PANEL_290_M06", "TODO", "UC8151D", "mono", "structural"),
  103. "GxEPD2_290_T5": Panel("PANEL_290_T5", "TODO", "IL0373", "mono", "structural"),
  104. "GxEPD2_290_T94": Panel("PANEL_290_T94", "GDEM029T94", "TODO", "mono", "structural"),
  105. "GxEPD2_290_T94_V2": Panel("PANEL_290_T94_V2", "GDEM029T94", "TODO", "mono", "structural"),
  106. "GxEPD2_310_GDEQ031T10": Panel("PANEL_310_GDEQ031T10", "GDEQ031T10", "UC8253", "mono", "structural"),
  107. "GxEPD2_370_TC1": Panel("PANEL_370_TC1", "TODO", "TODO", "mono", "structural"),
  108. "GxEPD2_371": Panel("PANEL_371", "GDEW0371W7", "IL0324", "mono", "structural"),
  109. "GxEPD2_397_GDEM0397T81": Panel("PANEL_397_GDEM0397T81", "GDEM0397T81", "TODO", "mono", "structural"),
  110. "GxEPD2_420_GDEY042T81": Panel("PANEL_420_GDEY042T81", "GDEY042T81", "TODO", "mono", "structural"),
  111. "GxEPD2_420_GYE042A87": Panel("PANEL_420_GYE042A87", "TODO", "TODO", "mono", "structural"),
  112. "GxEPD2_420_M01": Panel("PANEL_420_M01", "TODO", "UC8176", "mono", "structural"),
  113. "GxEPD2_420_SE0420NQ04": Panel("PANEL_420_SE0420NQ04", "TODO", "UC8276C", "mono", "structural"),
  114. "GxEPD2_576_GDEH0576T81_BW": Panel("PANEL_576_GDEH0576T81_BW", "GDEH0576T81", "SSD2677", "mono", "structural"),
  115. "GxEPD2_576_GDEH0576T81_BW_SHM": Panel("PANEL_576_GDEH0576T81_BW_SHM", "GDEH0576T81", "SSD2677", "mono", "structural"),
  116. "GxEPD2_579_GDEY0579T93": Panel("PANEL_579_GDEY0579T93", "GDEY0579T93", "TODO", "mono", "structural"),
  117. "GxEPD2_583_T8": Panel("PANEL_583_T8", "GDEW0583T8", "GD7965", "mono", "structural"),
  118. "GxEPD2_750": Panel("PANEL_750", "TODO", "IL0371", "mono", "structural"),
  119. "GxEPD2_750_T7": Panel("PANEL_750_T7", "GDEW075T7", "GD7965", "mono", "structural"),
  120. }
  121. # Good Display part number -> shelf label: drop the 4-char GDEx series prefix,
  122. # keeping just the size + type (GDEQ0583T31 -> 0583T31, GDEY0213Z98 ->
  123. # 0213Z98). The GDEx series/generation letter encodes nothing we act on and
  124. # costs scarce width on small panels; size stays encoded once, in the digits.
  125. # Anything not starting with GDE<letter> is returned untouched.
  126. def _panel_label(part: str) -> str:
  127. return re.sub(r"^GDE[A-Z]", "", part)
  128. CONFIG_SCHEMA = cv.All(
  129. display.FULL_DISPLAY_SCHEMA.extend(
  130. {
  131. cv.GenerateID(): cv.declare_id(GxEPD2Display),
  132. cv.GenerateID(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
  133. cv.Required(CONF_MODEL): cv.one_of(*PANELS),
  134. cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
  135. cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
  136. cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
  137. cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
  138. # No schema default: absence falls to the alias-derived default in
  139. # to_code() (just False today; true for a waveshare alias once that
  140. # lands), while an explicit value always wins.
  141. cv.Optional(CONF_INVERT_MONO): cv.boolean,
  142. cv.Optional(CONF_DIAGNOSTICS, default=False): cv.boolean,
  143. }
  144. ),
  145. # The runtime C++ uses only framework-portable ESPHome abstractions, so
  146. # both IDF and ESP32-Arduino are supported. ESP8266-Arduino compiles too
  147. # but is resolution-restricted at 1bpp and lacks the PSRAM that >1bpp
  148. # (color) panels need, so those fail allocation at runtime.
  149. cv.only_with_framework([Framework.ESP_IDF, Framework.ARDUINO]),
  150. )
  151. async def to_code(config: ConfigType) -> None:
  152. var = cg.new_Pvariable(config[CONF_ID])
  153. panel = PANELS[config[CONF_MODEL]]
  154. cg.add(var.set_descriptor(cg.RawExpression(f"&esphome::gxepd2::{panel.descriptor}")))
  155. cg.add(var.set_cs_pin(await cg.gpio_pin_expression(config[CONF_CS_PIN])))
  156. cg.add(var.set_dc_pin(await cg.gpio_pin_expression(config[CONF_DC_PIN])))
  157. if reset_pin := config.get(CONF_RESET_PIN):
  158. cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
  159. if busy_pin := config.get(CONF_BUSY_PIN):
  160. cg.add(var.set_busy_pin(await cg.gpio_pin_expression(busy_pin)))
  161. spi_parent = await cg.get_variable(config[CONF_SPI_ID])
  162. cg.add(var.set_spi_parent(spi_parent))
  163. # Alias-derived default is just False for now; the waveshare-compat layer
  164. # will compute it from the (aliased) model. An explicit invert_mono wins.
  165. cg.add(var.set_invert_mono(config.get(CONF_INVERT_MONO, False)))
  166. # Panel label for test_pattern(): the Good Display part number minus its
  167. # series prefix (GDEQ0583T31 -> 0583T31), so a tested panel shows what it
  168. # is on the shelf, not the GxEPD2 C++ class id.
  169. cg.add(var.set_model_name(_panel_label(panel.part)))
  170. cg.add(var.set_verified(panel.status == "verified"))
  171. # Compiles in GxEPD2Display/CycleEngine's dev-time diagnostic methods -
  172. # see CONF_DIAGNOSTICS above. Not set at all (not just defined false) for
  173. # every other build, so a production build's flash never sees them.
  174. if config[CONF_DIAGNOSTICS]:
  175. cg.add_define("USE_GXEPD2_DIAGNOSTICS")
  176. # A top-level `lambda:` must be processed and attached via set_writer()
  177. # here - register_display() only wires up `pages:` (same pattern as every
  178. # other display component's to_code(), e.g. epaper_spi).
  179. if CONF_LAMBDA in config:
  180. lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void)
  181. cg.add(var.set_writer(lambda_))
  182. await display.register_display(var, config)