Proof of concept: 2bpp greyscale LVGL display

This commit is contained in:
2026-05-06 13:53:55 -07:00
commit d91fb65381
12 changed files with 6895 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"WebSearch"
]
}
}
+7
View File
@@ -0,0 +1,7 @@
# ESPHome build cache and secrets
.esphome/
secrets.yaml
# Python bytecode from custom components
__pycache__/
*.pyc
+59
View File
@@ -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
BIN
View File
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

+200
View File
@@ -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