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