Proof of concept: 2bpp greyscale LVGL display
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebSearch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# ESPHome build cache and secrets
|
||||||
|
.esphome/
|
||||||
|
secrets.yaml
|
||||||
|
|
||||||
|
# Python bytecode from custom components
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Waveshare 2.9" E-Paper LVGL Project
|
||||||
|
|
||||||
|
## Goal Summary
|
||||||
|
- Extend the native waveshare_epaper component of esphome to support 2-bit grayscale
|
||||||
|
- Only concerned with this one target board: `2.90inv2-r2`
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
- ESP32-WROOM-32 (nodemcu-32s board)
|
||||||
|
- Waveshare 2.9inch Touch ePaper HAT (296x128, 2-bit grayscale)
|
||||||
|
- Display model string: `2.90inv2-r2` (SSD1680 controller)
|
||||||
|
- Pin mapping: SCLK=GPIO23, MOSI=GPIO22, CS=GPIO19, DC=GPIO18, BUSY=GPIO4, RST=GPIO5
|
||||||
|
|
||||||
|
## Software
|
||||||
|
- ESPHome 2026.1.0
|
||||||
|
- Framework: esp-idf
|
||||||
|
- Project directory: ~/Projects/waveshare-panel
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
- Display working correctly via ESPHome's native display library
|
||||||
|
- LVGL rendering confirmed working (black background visible)
|
||||||
|
- Custom component `waveshare_epaper_2bit` created and flashed, working identically to built-in driver
|
||||||
|
- Custom model name: `2.90inv2-r2-2bpp`
|
||||||
|
|
||||||
|
## Known Issues / Quirks
|
||||||
|
- LVGL color depth is hardcoded to 16-bit (RGB565) in ESPHome — only supported value
|
||||||
|
- Display renders in 1-bit monochrome despite panel supporting 2-bit grayscale
|
||||||
|
- Colors are inverted by default (bg_color: 0x000000 produces white background)
|
||||||
|
- Resolved: original timeout errors were caused by on_draw_end hammering the display
|
||||||
|
|
||||||
|
## Working LVGL Config
|
||||||
|
- refer to the file waveshare-test.yaml
|
||||||
|
|
||||||
|
## Goal: 2-bit Grayscale Support
|
||||||
|
The SSD1680 controller supports 4-shade grayscale via two bitplanes:
|
||||||
|
- Register 0x24: bitplane 1
|
||||||
|
- Register 0x26: bitplane 2
|
||||||
|
|
||||||
|
| 0x24 | 0x26 | Result |
|
||||||
|
|------|------|------------|
|
||||||
|
| 1 | 1 | White |
|
||||||
|
| 1 | 0 | Light grey |
|
||||||
|
| 0 | 1 | Dark grey |
|
||||||
|
| 0 | 0 | Black |
|
||||||
|
|
||||||
|
The ESPHome driver only writes to 0x24, discarding grayscale information.
|
||||||
|
The real problem is upstream — RGB565 is converted to 1-bit before display() is
|
||||||
|
called, so grayscale data is lost before the driver even sees it. Next step is to
|
||||||
|
find where this conversion happens in the ESPHome display pipeline so the patch
|
||||||
|
can preserve grayscale information through to the two-bitplane write.
|
||||||
|
|
||||||
|
## Relevant Files
|
||||||
|
- Custom component: `custom_components/waveshare_epaper_2bit/`
|
||||||
|
- `__init__.py` — CODEOWNERS only
|
||||||
|
- `display.py` — component schema and model registration
|
||||||
|
- `waveshare_epaper_2bit.cpp` — driver implementation
|
||||||
|
- `waveshare_epaper_2bit.h` — header
|
||||||
|
- Build cache: `.esphome/build/waveshare-epaper-test/src/esphome/components/`
|
||||||
|
- ESPHome source (venv): ~/Code/esphome-2026.1.0/
|
||||||
|
- Applicable ESPHome source is copied into .esphome/build by the build system at compile time
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@clydebarrow"]
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
from esphome import core, pins
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import display, spi
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BUSY_PIN,
|
||||||
|
CONF_DC_PIN,
|
||||||
|
CONF_FULL_UPDATE_EVERY,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_LAMBDA,
|
||||||
|
CONF_MODEL,
|
||||||
|
CONF_PAGES,
|
||||||
|
CONF_RESET_DURATION,
|
||||||
|
CONF_RESET_PIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPENDENCIES = ["spi"]
|
||||||
|
|
||||||
|
waveshare_epaper_ns = cg.esphome_ns.namespace("waveshare_epaper_2bit")
|
||||||
|
WaveshareEPaperBase = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||||
|
)
|
||||||
|
WaveshareEPaper = waveshare_epaper_ns.class_("WaveshareEPaper", WaveshareEPaperBase)
|
||||||
|
WaveshareEPaperBWR = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaperBWR", WaveshareEPaperBase
|
||||||
|
)
|
||||||
|
WaveshareEPaper7C = waveshare_epaper_ns.class_("WaveshareEPaper7C", WaveshareEPaperBase)
|
||||||
|
WaveshareEPaperTypeA = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaperTypeA", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEpaper1P54INBV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper1P54InBV2", WaveshareEPaperBWR
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P7In = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P7In", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P7InB = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P7InB", WaveshareEPaperBWR
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P7InBV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P7InBV2", WaveshareEPaperBWR
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P7InV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P7InV2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P9InB = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P9InB", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P9InBV3 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P9InBV3", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P9InV2R2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P9InV2R2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P9InV2R22Bpp = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P9InV2R22Bpp", WaveshareEPaper
|
||||||
|
)
|
||||||
|
GDEW029T5 = waveshare_epaper_ns.class_("GDEW029T5", WaveshareEPaper)
|
||||||
|
GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper)
|
||||||
|
WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P9InDKE", WaveshareEPaper
|
||||||
|
)
|
||||||
|
GDEY042T81 = waveshare_epaper_ns.class_("GDEY042T81", WaveshareEPaper)
|
||||||
|
WaveshareEPaper2P9InD = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P9InD", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper4P2In = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper4P2In", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper4P2InBV2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper4P2InBV2BWR = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper4P2InBV2BWR", WaveshareEPaperBWR
|
||||||
|
)
|
||||||
|
WaveshareEPaper5P65InF = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper5P65InF", WaveshareEPaper7C
|
||||||
|
)
|
||||||
|
WaveshareEPaper5P8In = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper5P8In", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper5P8InV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper5P8InV2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
GDEY0583T81 = waveshare_epaper_ns.class_("GDEY0583T81", WaveshareEPaper)
|
||||||
|
WaveshareEPaper7P3InF = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P3InF", WaveshareEPaper7C
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5In = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5In", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InBC = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InBC", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InBV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InBV2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InBV3 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InBV3", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InBV3BWR = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InBV3BWR", WaveshareEPaperBWR
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InV2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InV2alt = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InV2alt", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InV2P = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InV2P", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper7P5InHDB", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P13InDKE", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P13InV2 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P13InV2", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper2P13InV3 = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper2P13InV3", WaveshareEPaper
|
||||||
|
)
|
||||||
|
WaveshareEPaper13P3InK = waveshare_epaper_ns.class_(
|
||||||
|
"WaveshareEPaper13P3InK", WaveshareEPaper
|
||||||
|
)
|
||||||
|
GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper)
|
||||||
|
|
||||||
|
WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel")
|
||||||
|
WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeBModel")
|
||||||
|
|
||||||
|
MODELS = {
|
||||||
|
"1.54in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN),
|
||||||
|
"1.54inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN_V2),
|
||||||
|
"1.54inv2-b": ("b", WaveshareEpaper1P54INBV2),
|
||||||
|
"2.13in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN),
|
||||||
|
"2.13inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN_V2),
|
||||||
|
"2.13in-ttgo": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN),
|
||||||
|
"2.13in-ttgo-b1": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B1),
|
||||||
|
"2.13in-ttgo-b73": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B73),
|
||||||
|
"2.13in-ttgo-b74": ("a", WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B74),
|
||||||
|
"2.90in": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN),
|
||||||
|
"2.90inv2": ("a", WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN_V2),
|
||||||
|
"gdew029t5": ("c", GDEW029T5),
|
||||||
|
"2.70in": ("b", WaveshareEPaper2P7In),
|
||||||
|
"2.70in-b": ("b", WaveshareEPaper2P7InB),
|
||||||
|
"2.70in-bv2": ("b", WaveshareEPaper2P7InBV2),
|
||||||
|
"2.70inv2": ("b", WaveshareEPaper2P7InV2),
|
||||||
|
"2.90in-b": ("b", WaveshareEPaper2P9InB),
|
||||||
|
"2.90in-bv3": ("b", WaveshareEPaper2P9InBV3),
|
||||||
|
"gdey029t94": ("c", GDEY029T94),
|
||||||
|
"2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2),
|
||||||
|
"2.90inv2-r2-2bpp": ("c", WaveshareEPaper2P9InV2R22Bpp),
|
||||||
|
"2.90in-d": ("b", WaveshareEPaper2P9InD),
|
||||||
|
"2.90in-dke": ("c", WaveshareEPaper2P9InDKE),
|
||||||
|
"gdey042t81": ("c", GDEY042T81),
|
||||||
|
"4.20in": ("b", WaveshareEPaper4P2In),
|
||||||
|
"4.20in-bv2": ("b", WaveshareEPaper4P2InBV2),
|
||||||
|
"4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR),
|
||||||
|
"5.65in-f": ("b", WaveshareEPaper5P65InF),
|
||||||
|
"5.83in": ("b", WaveshareEPaper5P8In),
|
||||||
|
"5.83inv2": ("b", WaveshareEPaper5P8InV2),
|
||||||
|
"gdey0583t81": ("c", GDEY0583T81),
|
||||||
|
"7.30in-f": ("b", WaveshareEPaper7P3InF),
|
||||||
|
"7.50in": ("b", WaveshareEPaper7P5In),
|
||||||
|
"7.50in-bv2": ("b", WaveshareEPaper7P5InBV2),
|
||||||
|
"7.50in-bv3": ("b", WaveshareEPaper7P5InBV3),
|
||||||
|
"7.50in-bv3-bwr": ("b", WaveshareEPaper7P5InBV3BWR),
|
||||||
|
"7.50in-bc": ("b", WaveshareEPaper7P5InBC),
|
||||||
|
"7.50inv2": ("b", WaveshareEPaper7P5InV2),
|
||||||
|
"7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt),
|
||||||
|
"7.50inv2p": ("c", WaveshareEPaper7P5InV2P),
|
||||||
|
"7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB),
|
||||||
|
"2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE),
|
||||||
|
"2.13inv3": ("c", WaveshareEPaper2P13InV3),
|
||||||
|
"1.54in-m5coreink-m09": ("b", GDEW0154M09),
|
||||||
|
"13.3in-k": ("b", WaveshareEPaper13P3InK),
|
||||||
|
}
|
||||||
|
|
||||||
|
RESET_PIN_REQUIRED_MODELS = ("2.13inv2", "2.13in-ttgo-b74")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_full_update_every_only_types_ac(value):
|
||||||
|
if CONF_FULL_UPDATE_EVERY not in value:
|
||||||
|
return value
|
||||||
|
if MODELS[value[CONF_MODEL]][0] == "b":
|
||||||
|
full_models = []
|
||||||
|
for key, val in sorted(MODELS.items()):
|
||||||
|
if val[0] != "b":
|
||||||
|
full_models.append(key)
|
||||||
|
raise cv.Invalid(
|
||||||
|
"The 'full_update_every' option is only available for models "
|
||||||
|
+ ", ".join(full_models)
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_reset_pin_required(config):
|
||||||
|
if config[CONF_MODEL] in RESET_PIN_REQUIRED_MODELS and CONF_RESET_PIN not in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"'{CONF_RESET_PIN}' is required for model {config[CONF_MODEL]}"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
display.FULL_DISPLAY_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(WaveshareEPaperBase),
|
||||||
|
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True),
|
||||||
|
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||||
|
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
|
||||||
|
cv.Optional(CONF_FULL_UPDATE_EVERY): cv.int_range(min=1, max=4294967295),
|
||||||
|
cv.Optional(CONF_RESET_DURATION): cv.All(
|
||||||
|
cv.positive_time_period_milliseconds,
|
||||||
|
cv.Range(max=core.TimePeriod(milliseconds=500)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("1s"))
|
||||||
|
.extend(spi.spi_device_schema()),
|
||||||
|
validate_full_update_every_only_types_ac,
|
||||||
|
validate_reset_pin_required,
|
||||||
|
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||||
|
)
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||||
|
"waveshare_epaper_2bit", require_miso=False, require_mosi=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
model_type, model = MODELS[config[CONF_MODEL]]
|
||||||
|
if model_type == "a":
|
||||||
|
rhs = WaveshareEPaperTypeA.new(model)
|
||||||
|
var = cg.Pvariable(config[CONF_ID], rhs, WaveshareEPaperTypeA)
|
||||||
|
elif model_type in ("b", "c"):
|
||||||
|
rhs = model.new()
|
||||||
|
var = cg.Pvariable(config[CONF_ID], rhs, model)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
await display.register_display(var, config)
|
||||||
|
await spi.register_spi_device(var, config)
|
||||||
|
|
||||||
|
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||||
|
cg.add(var.set_dc_pin(dc))
|
||||||
|
|
||||||
|
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_))
|
||||||
|
if CONF_RESET_PIN in config:
|
||||||
|
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
|
||||||
|
cg.add(var.set_reset_pin(reset))
|
||||||
|
if CONF_BUSY_PIN in config:
|
||||||
|
reset = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
|
||||||
|
cg.add(var.set_busy_pin(reset))
|
||||||
|
if CONF_FULL_UPDATE_EVERY in config:
|
||||||
|
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
|
||||||
|
if CONF_RESET_DURATION in config:
|
||||||
|
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#include "waveshare_epaper_2bit.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace waveshare_epaper_2bit {
|
||||||
|
|
||||||
|
static const char *const TAG = "waveshare_2.13v3";
|
||||||
|
|
||||||
|
static const uint8_t PARTIAL_LUT[] = {
|
||||||
|
0x32, // cmd
|
||||||
|
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t FULL_LUT[] = {
|
||||||
|
0x32, // CMD
|
||||||
|
0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||||
|
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t SW_RESET = 0x12;
|
||||||
|
static const uint8_t ACTIVATE = 0x20;
|
||||||
|
static const uint8_t WRITE_BUFFER = 0x24;
|
||||||
|
static const uint8_t WRITE_BASE = 0x26;
|
||||||
|
|
||||||
|
static const uint8_t DRV_OUT_CTL[] = {0x01, 0x27, 0x01, 0x00}; // driver output control
|
||||||
|
static const uint8_t GATEV[] = {0x03, 0x17};
|
||||||
|
static const uint8_t SRCV[] = {0x04, 0x41, 0x0C, 0x32};
|
||||||
|
static const uint8_t SLEEP[] = {0x10, 0x01};
|
||||||
|
static const uint8_t DATA_ENTRY[] = {0x11, 0x03}; // data entry mode
|
||||||
|
static const uint8_t TEMP_SENS[] = {0x18, 0x80}; // Temp sensor
|
||||||
|
static const uint8_t DISPLAY_UPDATE[] = {0x21, 0x00, 0x80}; // Display update control
|
||||||
|
static const uint8_t UPSEQ[] = {0x22, 0xC0};
|
||||||
|
static const uint8_t ON_FULL[] = {0x22, 0xC7};
|
||||||
|
static const uint8_t ON_PARTIAL[] = {0x22, 0x0F};
|
||||||
|
static const uint8_t VCOM[] = {0x2C, 0x36};
|
||||||
|
static const uint8_t CMD5[] = {0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
static const uint8_t BORDER_PART[] = {0x3C, 0x80}; // border waveform
|
||||||
|
static const uint8_t BORDER_FULL[] = {0x3C, 0x05}; // border waveform
|
||||||
|
static const uint8_t CMD1[] = {0x3F, 0x22};
|
||||||
|
static const uint8_t RAM_X_START[] = {0x44, 0x00, 121 / 8}; // set ram_x_address_start_end
|
||||||
|
static const uint8_t RAM_Y_START[] = {0x45, 0x00, 0x00, 250 - 1, 0}; // set ram_y_address_start_end
|
||||||
|
static const uint8_t RAM_X_POS[] = {0x4E, 0x00}; // set ram_x_address_counter
|
||||||
|
// static const uint8_t RAM_Y_POS[] = {0x4F, 0x00, 0x00}; // set ram_y_address_counter
|
||||||
|
#define SEND(x) this->cmd_data(x, sizeof(x))
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::write_lut_(const uint8_t *lut) {
|
||||||
|
this->wait_until_idle_();
|
||||||
|
this->cmd_data(lut, sizeof(PARTIAL_LUT));
|
||||||
|
SEND(CMD1);
|
||||||
|
SEND(GATEV);
|
||||||
|
SEND(SRCV);
|
||||||
|
SEND(VCOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the buffer starting on line top, up to line bottom.
|
||||||
|
void WaveshareEPaper2P13InV3::write_buffer_(uint8_t cmd, int top, int bottom) {
|
||||||
|
this->wait_until_idle_();
|
||||||
|
this->set_window_(top, bottom);
|
||||||
|
this->command(cmd);
|
||||||
|
this->start_data_();
|
||||||
|
|
||||||
|
auto width_bytes = this->get_width_controller() / 8;
|
||||||
|
this->write_array(this->buffer_ + top * width_bytes, (bottom - top) * width_bytes);
|
||||||
|
this->end_data_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::send_reset_() {
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
delay(2);
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::setup() {
|
||||||
|
this->init_internal_(this->get_buffer_length_());
|
||||||
|
this->setup_pins_();
|
||||||
|
this->spi_setup();
|
||||||
|
this->reset_();
|
||||||
|
|
||||||
|
delay(20);
|
||||||
|
this->send_reset_();
|
||||||
|
// as a one-off delay this is not worth working around.
|
||||||
|
delay(100); // NOLINT
|
||||||
|
this->wait_until_idle_();
|
||||||
|
this->command(SW_RESET);
|
||||||
|
this->wait_until_idle_();
|
||||||
|
|
||||||
|
SEND(DRV_OUT_CTL);
|
||||||
|
SEND(DATA_ENTRY);
|
||||||
|
SEND(CMD5);
|
||||||
|
this->set_window_(0, this->get_height_internal());
|
||||||
|
SEND(BORDER_FULL);
|
||||||
|
SEND(DISPLAY_UPDATE);
|
||||||
|
SEND(TEMP_SENS);
|
||||||
|
this->wait_until_idle_();
|
||||||
|
this->write_lut_(FULL_LUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// t and b are y positions, i.e. line numbers.
|
||||||
|
void WaveshareEPaper2P13InV3::set_window_(int t, int b) {
|
||||||
|
uint8_t buffer[3];
|
||||||
|
|
||||||
|
SEND(RAM_X_START);
|
||||||
|
SEND(RAM_Y_START);
|
||||||
|
SEND(RAM_X_POS);
|
||||||
|
buffer[0] = 0x4F;
|
||||||
|
buffer[1] = (uint8_t) t;
|
||||||
|
buffer[2] = (uint8_t) (t >> 8);
|
||||||
|
SEND(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// must implement, but we override setup to have more control
|
||||||
|
void WaveshareEPaper2P13InV3::initialize() {}
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::partial_update_() {
|
||||||
|
this->send_reset_();
|
||||||
|
this->set_timeout(100, [this] {
|
||||||
|
this->write_lut_(PARTIAL_LUT);
|
||||||
|
SEND(BORDER_PART);
|
||||||
|
SEND(UPSEQ);
|
||||||
|
this->command(ACTIVATE);
|
||||||
|
this->set_timeout(100, [this] {
|
||||||
|
this->wait_until_idle_();
|
||||||
|
this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal());
|
||||||
|
SEND(ON_PARTIAL);
|
||||||
|
this->command(ACTIVATE); // Activate Display Update Sequence
|
||||||
|
this->is_busy_ = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::full_update_() {
|
||||||
|
ESP_LOGI(TAG, "Performing full e-paper update.");
|
||||||
|
this->write_lut_(FULL_LUT);
|
||||||
|
this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal());
|
||||||
|
this->write_buffer_(WRITE_BASE, 0, this->get_height_internal());
|
||||||
|
SEND(ON_FULL);
|
||||||
|
this->command(ACTIVATE); // don't wait here
|
||||||
|
this->is_busy_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::display() {
|
||||||
|
if (this->is_busy_ || (this->busy_pin_ != nullptr && this->busy_pin_->digital_read()))
|
||||||
|
return;
|
||||||
|
this->is_busy_ = true;
|
||||||
|
const bool partial = this->at_update_ != 0;
|
||||||
|
this->at_update_ = (this->at_update_ + 1) % this->full_update_every_;
|
||||||
|
if (partial) {
|
||||||
|
this->partial_update_();
|
||||||
|
} else {
|
||||||
|
this->full_update_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int WaveshareEPaper2P13InV3::get_width_controller() { return 128; }
|
||||||
|
int WaveshareEPaper2P13InV3::get_width_internal() { return 122; }
|
||||||
|
|
||||||
|
int WaveshareEPaper2P13InV3::get_height_internal() { return 250; }
|
||||||
|
|
||||||
|
uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; }
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::dump_config() {
|
||||||
|
LOG_DISPLAY("", "Waveshare E-Paper", this)
|
||||||
|
ESP_LOGCONFIG(TAG, " Model: 2.13inV3");
|
||||||
|
LOG_PIN(" CS Pin: ", this->cs_);
|
||||||
|
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||||
|
LOG_PIN(" DC Pin: ", this->dc_pin_);
|
||||||
|
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||||
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) {
|
||||||
|
this->full_update_every_ = full_update_every;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace waveshare_epaper
|
||||||
|
} // namespace esphome
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
@@ -0,0 +1,200 @@
|
|||||||
|
# Andrew Villeneuve 2026/05
|
||||||
|
# Waveshare 2.9inch Touch ePaper HAT integration demo
|
||||||
|
#
|
||||||
|
# Platform: https://www.waveshare.com/2.9inch-Touch-e-Paper-HAT.htm
|
||||||
|
#
|
||||||
|
# Pin mappings
|
||||||
|
# ---
|
||||||
|
# |Function |ESP Pin |HAT Pin |Wire |
|
||||||
|
# |--- |--- |--- |--- |
|
||||||
|
# |5V |VIN |Pin2/5V |Red |
|
||||||
|
# |GND |GND |Pin6/GND |Blk |
|
||||||
|
# |Display SCLK |GPIO23 |Pin23/GPIO11/SPI0_SCLK |Blue |
|
||||||
|
# |Display MOSI |GPIO22 |Pin19/GPIO10/SPI0_MOSI |Green |
|
||||||
|
# |Display CS |GPIO19 |Pin24/GPIO8/SPI0_CE0 |Mgnta |
|
||||||
|
# |Display DC |GPIO18 |Pin22/GPIO25 |White |
|
||||||
|
# |Display Busy |GPIO4 |Pin24/GPIO24 |Brown |
|
||||||
|
# |Display RST |GPIO5 |Pin11/GPIO17/SPI1_CE1 |Orange |
|
||||||
|
#
|
||||||
|
# Exposed Entities
|
||||||
|
# ---
|
||||||
|
#
|
||||||
|
|
||||||
|
### Boilerplate ###
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: waveshare-epaper-test
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: nodemcu-32s
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret enckey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
password: !secret apipw
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: !secret wifi_ssid
|
||||||
|
password: !secret wappw
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: ${fallbackssid}
|
||||||
|
password: !secret hotspotpw
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
### Setup Interfaces ###
|
||||||
|
|
||||||
|
# For the display
|
||||||
|
spi:
|
||||||
|
clk_pin: GPIO23
|
||||||
|
mosi_pin: GPIO22
|
||||||
|
|
||||||
|
# For the touchscreen
|
||||||
|
# ...
|
||||||
|
|
||||||
|
### Okay, now the Good Stuff ###
|
||||||
|
|
||||||
|
# Load some fonts for the display renderer (we don't neeed these with LVGL)
|
||||||
|
font:
|
||||||
|
- file: "FreeSans.ttf"
|
||||||
|
id: font1
|
||||||
|
size: 26
|
||||||
|
bpp: 2
|
||||||
|
- file: "FreeSans.ttf"
|
||||||
|
id: headerfont
|
||||||
|
size: 80
|
||||||
|
- file: "FreeSans.ttf"
|
||||||
|
id: console
|
||||||
|
size: 30
|
||||||
|
|
||||||
|
# Pull in some icons from the material design library
|
||||||
|
image:
|
||||||
|
- file: "mdi:home-lock-open"
|
||||||
|
id: home_lock_open
|
||||||
|
type: binary
|
||||||
|
transparency: chroma_key
|
||||||
|
invert_alpha: true
|
||||||
|
resize: 40x40
|
||||||
|
|
||||||
|
# Force a full display refresh when we press the "boot" button on the devboard
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: GPIO0
|
||||||
|
mode:
|
||||||
|
input: true
|
||||||
|
pullup: true
|
||||||
|
inverted: true
|
||||||
|
id: boot_button
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- component.update: lvgl0
|
||||||
|
- delay: 1s
|
||||||
|
- component.update: display0
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: waveshare_epaper_2bit
|
||||||
|
id: display0
|
||||||
|
cs_pin: GPIO19
|
||||||
|
dc_pin: GPIO18
|
||||||
|
busy_pin: GPIO4
|
||||||
|
reset_pin: GPIO5
|
||||||
|
model: 2.90inv2-r2-2bpp
|
||||||
|
full_update_every: 30
|
||||||
|
rotation: 270°
|
||||||
|
auto_clear_enabled: false
|
||||||
|
update_interval: never
|
||||||
|
# show_test_card: true
|
||||||
|
# lambda: |-
|
||||||
|
# it.print(0, 0, id(headerfont), "Bleat!");
|
||||||
|
|
||||||
|
lvgl:
|
||||||
|
- id: lvgl0
|
||||||
|
displays:
|
||||||
|
- display0
|
||||||
|
# on_draw_end:
|
||||||
|
# - component.update: display0
|
||||||
|
bg_color: 0xFFFFFF
|
||||||
|
# Black: 0x000000
|
||||||
|
# Dark Grey: 0x555555
|
||||||
|
# Light Grey: 0xAAAAAA
|
||||||
|
# White: 0xFFFFFF
|
||||||
|
theme:
|
||||||
|
label:
|
||||||
|
text_color: 0x000000
|
||||||
|
# button:
|
||||||
|
# bg_color: 0x000000
|
||||||
|
# text_color: 0xFFFFFF
|
||||||
|
obj:
|
||||||
|
bg_color: 0xFFFFFF
|
||||||
|
widgets:
|
||||||
|
- label:
|
||||||
|
text: 'Test 2bpp 1326'
|
||||||
|
align: TOP_LEFT
|
||||||
|
text_font: font1
|
||||||
|
# - label:
|
||||||
|
# text: "0xFFFFFF"
|
||||||
|
# text_color: 0xFFFFFF
|
||||||
|
# x: 0
|
||||||
|
# y: 15
|
||||||
|
# - label:
|
||||||
|
# text: "0xAAAAAA"
|
||||||
|
# text_color: 0xAAAAAA
|
||||||
|
# x: 0
|
||||||
|
# y: 30
|
||||||
|
# - label:
|
||||||
|
# text: "0x555555"
|
||||||
|
# text_color: 0x555555
|
||||||
|
# x: 0
|
||||||
|
# y: 45
|
||||||
|
# - label:
|
||||||
|
# text: "0x000000"
|
||||||
|
# text_color: 0x000000
|
||||||
|
# x: 0
|
||||||
|
# y: 60
|
||||||
|
- obj:
|
||||||
|
align: BOTTOM_LEFT
|
||||||
|
width: 33%
|
||||||
|
height: 30%
|
||||||
|
bg_color: 0x000000 #Black
|
||||||
|
- obj:
|
||||||
|
align: BOTTOM_MID
|
||||||
|
width: 33%
|
||||||
|
height: 30%
|
||||||
|
bg_color: 0x555555 #Dark Grey
|
||||||
|
- obj:
|
||||||
|
align: BOTTOM_RIGHT
|
||||||
|
width: 33%
|
||||||
|
height: 30%
|
||||||
|
bg_color: 0xAAAAAA #Light Grey
|
||||||
|
- obj:
|
||||||
|
align: TOP_RIGHT
|
||||||
|
width: 33%
|
||||||
|
height: 30%
|
||||||
|
bg_color: 0xFFFFFF #White
|
||||||
|
- obj:
|
||||||
|
align: LEFT_MID
|
||||||
|
width: 66%
|
||||||
|
height: 30%
|
||||||
|
bg_color: 0x000000
|
||||||
|
bg_grad_color: 0xFFFFFF
|
||||||
|
bg_grad_dir: HOR
|
||||||
|
- button:
|
||||||
|
id: button0
|
||||||
|
align: TOP_RIGHT
|
||||||
|
widgets:
|
||||||
|
- image:
|
||||||
|
src: home_lock_open
|
||||||
|
|
||||||
Reference in New Issue
Block a user