Compare commits

...
Author SHA1 Message Date
J. Nick Koston 470ed7cc06 [display] Loop on the reader's step in display drivers and cap mipi_spi args in the reader 2026-10-22 15:45:42 -10:00
J. Nick Koston 195c8285d7 Merge branch 'display-init-sequence-reader' into display-init-sequence-reader-migrate 2026-10-22 15:45:21 -10:00
J. Nick Koston e5b5dd52ef [display] Return the step from the reader again, as a plain enum with END = 0
The bool API cost flash at every call site (about 10 B per mipi_spi
instantiation on ESP8266); the unscoped enum keeps the old code shape and
lets callers loop with while (auto step = reader.next(entry)).
2026-10-22 15:45:05 -10:00
J. Nick Koston 014e6938c3 Merge branch 'display-init-sequence-reader' into display-init-sequence-reader-migrate 2026-10-22 15:39:27 -10:00
J. Nick Koston 67a56da25c [display] Use the bool reader API and config_has_writer in display drivers
mipi_spi, mipi_dsi and mipi_rgb also switch to config_has_writer, and the
mipi_spi argument limit uses cv.Length instead of a wrapper function.
2026-10-22 15:38:47 -10:00
J. Nick Koston bf36f7532d [display] Let a caller cap the argument count in the init sequence reader 2026-10-22 15:38:46 -10:00
J. Nick Koston 10c32a5cc7 Merge branch 'display-init-sequence-reader' into display-init-sequence-reader-migrate 2026-10-22 15:36:42 -10:00
J. Nick Koston 69f6429728 [display] Rename has_writer to config_has_writer and make the reader return bool
The function shadowed the has_writer keyword in add_metadata (pylint W0621).
next() now returns false at the end or on a malformed entry, and malformed()
tells the two apart, which drops the step enum.
2026-10-22 15:36:39 -10:00
J. Nick Koston 4ae6ec545c [display] Loop on the reader until END and include it where it is used 2026-10-22 15:20:45 -10:00
J. Nick Koston 263de44633 [display] Use the shared init sequence reader and has_writer in display drivers 2026-10-22 15:15:21 -10:00
J. Nick Koston bab99748f0 [display] Add a shared init sequence reader and has_writer helper 2026-10-22 15:03:44 -10:00
19 changed files with 319 additions and 174 deletions

No files matched your search

+11 -4
View File
@@ -181,6 +181,16 @@ def get_all_display_metadata() -> dict[str, DisplayMetaData]:
return {id_.id: meta for id_, meta in entries}
def config_has_writer(config: ConfigType) -> bool:
"""Return True when the display config draws anything."""
return (
config.get(CONF_AUTO_CLEAR_ENABLED) is True
or config.get(CONF_PAGES) is not None
or config.get(CONF_LAMBDA) is not None
or config.get(CONF_SHOW_TEST_CARD) is True
)
def get_display_metadata(display_id: ID) -> DisplayMetaData:
"""Get display metadata by ID object
@@ -210,10 +220,7 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
height=dimensions[1],
has_hardware_rotation=False,
byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED),
has_writer=disp_config.get(CONF_AUTO_CLEAR_ENABLED) is True
or disp_config.get(CONF_PAGES) is not None
or disp_config.get(CONF_LAMBDA) is not None
or disp_config.get(CONF_SHOW_TEST_CARD) is True,
has_writer=config_has_writer(disp_config),
rotation=disp_config.get(CONF_ROTATION, 0),
draw_rounding=disp_config.get(CONF_DRAW_ROUNDING, 0),
)
@@ -0,0 +1,28 @@
#include "init_sequence.h"
#include "esphome/core/hal.h"
namespace esphome::display {
InitSequenceStep InitSequenceReader::next(InitSequenceEntry &entry, uint8_t max_count) {
const size_t remaining = this->length_ - this->index_;
if (remaining == 0)
return INIT_SEQUENCE_STEP_END;
const uint8_t *pos = this->sequence_ + this->index_;
if (remaining >= 2) {
entry.value = progmem_read_byte(pos);
entry.count = progmem_read_byte(pos + 1);
entry.args = pos + 2;
if (entry.count == INIT_SEQUENCE_DELAY_FLAG) {
this->index_ += 2;
return INIT_SEQUENCE_STEP_DELAY;
}
if (entry.count <= max_count && remaining - 2 >= entry.count) {
this->index_ += 2 + entry.count;
return INIT_SEQUENCE_STEP_COMMAND;
}
}
this->index_ = this->length_;
return INIT_SEQUENCE_STEP_MALFORMED;
}
} // namespace esphome::display
@@ -0,0 +1,40 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::display {
/// Count byte that marks a delay entry: the byte before it is the delay in ms.
static constexpr uint8_t INIT_SEQUENCE_DELAY_FLAG = 0xFF;
/// Unscoped so `while (auto step = reader.next(entry))` stops at END.
enum InitSequenceStep : uint8_t {
INIT_SEQUENCE_STEP_END = 0,
INIT_SEQUENCE_STEP_COMMAND,
INIT_SEQUENCE_STEP_DELAY,
INIT_SEQUENCE_STEP_MALFORMED,
};
/// `value` is the command or the delay in ms; `args` points into the (possibly PROGMEM) sequence.
struct InitSequenceEntry {
const uint8_t *args;
uint8_t value;
uint8_t count;
};
/// Walks a mipi.flatten_sequence table (`cmd, count, args...` or `ms, 0xFF`) with progmem reads; resumable.
class InitSequenceReader {
public:
InitSequenceReader(const uint8_t *sequence, size_t length) : sequence_(sequence), length_(length) {}
/// Fill `entry` with the next entry; a command with more than `max_count` args is MALFORMED.
InitSequenceStep next(InitSequenceEntry &entry, uint8_t max_count = 0xFE);
protected:
const uint8_t *sequence_;
size_t length_;
size_t index_{0};
};
} // namespace esphome::display
+1 -5
View File
@@ -15,7 +15,6 @@ from esphome.components.mipi import (
import esphome.config_validation as cv
from esphome.config_validation import update_interval
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DATA_RATE,
@@ -189,10 +188,7 @@ def customise_schema(config):
height,
has_hardware_rotation=True,
byte_order=cv.UNDEFINED,
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
or config.get(CONF_PAGES) is not None
or config.get(CONF_LAMBDA) is not None
or config.get(CONF_SHOW_TEST_CARD) is True,
has_writer=display.config_has_writer(config),
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=0,
)
+9 -16
View File
@@ -1,4 +1,5 @@
#include "epaper_spi.h"
#include "esphome/components/display/init_sequence.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
@@ -315,26 +316,18 @@ void EPaperBase::start_data_() {
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) {
size_t index = 0;
while (index != length) {
if (length - index < 2) {
display::InitSequenceReader reader(sequence, length);
display::InitSequenceEntry entry;
while (auto step = reader.next(entry)) {
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
const uint8_t cmd = sequence[index++];
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
ESP_LOGV(TAG, "Delay %dms", entry.value);
delay(entry.value);
} else {
const uint8_t num_args = x & 0x7F;
if (length - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
this->mark_failed();
return;
}
this->cmd_data(cmd, sequence + index, num_args);
index += num_args;
this->cmd_data(entry.value, entry.args, entry.count);
}
}
}
@@ -34,7 +34,6 @@ static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to
static constexpr size_t MAX_TRANSFER_SIZE = 128;
// Longer busy waits are the waveform or a stuck pin, and are polled at the normal loop pace
static constexpr uint32_t FAST_BUSY_POLL_MS = 50;
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
+1 -3
View File
@@ -38,7 +38,6 @@ from esphome.components.mipi import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
@@ -167,8 +166,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
height,
has_hardware_rotation=False,
byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config)
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
has_writer=display.config_has_writer(config),
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
)
+29 -37
View File
@@ -1,6 +1,7 @@
#ifdef USE_ESP32_VARIANT_ESP32P4
#include <utility>
#include "mipi_dsi.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/core/helpers.h"
#include <algorithm>
@@ -123,47 +124,38 @@ void MipiDsi::setup() {
this->smark_failed(LOG_STR("esp_lcd_init failed"), err);
return;
}
size_t index = 0;
const uint8_t *seq = this->init_sequence_;
const size_t len = this->init_sequence_len_;
while (index != len) {
if (len - index < 2) {
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
display::InitSequenceEntry entry;
while (auto step = reader.next(entry)) {
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
uint8_t cmd = seq[index++];
uint8_t x = seq[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (len - index < num_args) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
if (cmd == SLEEP_OUT) {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
delay(duration);
}
}
const auto *ptr = seq + index;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
#endif
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args,
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args);
if (err != ESP_OK) {
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
return;
}
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
ESP_LOGD(TAG, "Delay %dms", entry.value);
delay(entry.value);
continue;
}
const uint8_t cmd = entry.value;
if (cmd == SLEEP_OUT) {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
delay(duration);
}
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
#endif
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, entry.count,
format_hex_pretty_to(hex_buf, entry.args, entry.count, '.'));
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, entry.args, entry.count);
if (err != ESP_OK) {
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
return;
}
if (cmd == SLEEP_OUT)
delay(10);
}
this->io_lock_ = xSemaphoreCreateBinary();
esp_lcd_dpi_panel_event_callbacks_t cbs = {
-1
View File
@@ -31,7 +31,6 @@ const uint8_t INVERT_OFF = 0x20;
const uint8_t INVERT_ON = 0x21;
const uint8_t DISPLAY_ON = 0x29;
const uint8_t CMD2_BKSEL = 0xFF;
const uint8_t DELAY_FLAG = 0xFF;
const uint8_t MADCTL_BGR = 0x08;
const uint8_t MADCTL_MX = 0x40;
const uint8_t MADCTL_MY = 0x80;
+1 -3
View File
@@ -50,7 +50,6 @@ from esphome.components.spi import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BLUE,
CONF_COLOR_ORDER,
CONF_CS_PIN,
@@ -243,8 +242,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
height,
has_hardware_rotation=True,
byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config)
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
has_writer=display.config_has_writer(config),
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
)
+20 -35
View File
@@ -1,5 +1,6 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#include "mipi_rgb.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -13,8 +14,6 @@
namespace esphome::mipi_rgb {
static const uint8_t DELAY_FLAG = 0xFF;
// Maximum bytes to log for init commands (truncated if larger)
static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64;
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
@@ -75,44 +74,30 @@ void MipiRgbSpi::write_data_(uint8_t value) {
this->disable();
}
/**
* this relies upon the init sequence being well-formed, which is guaranteed by the Python init code.
*/
void MipiRgbSpi::write_init_sequence_() {
size_t index = 0;
const uint8_t *seq = this->init_sequence_;
const size_t len = this->init_sequence_len_;
while (index != len) {
if (len - index < 2) {
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
display::InitSequenceEntry entry;
while (auto step = reader.next(entry)) {
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
uint8_t cmd = seq[index++];
uint8_t x = seq[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (len - index < num_args) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
if (cmd == SLEEP_OUT) {
delay(120); // NOLINT
}
const auto *ptr = seq + index;
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args,
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
index += num_args;
this->write_command_(cmd);
while (num_args-- != 0)
this->write_data_(*ptr++);
if (cmd == SLEEP_OUT)
delay(10);
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
ESP_LOGD(TAG, "Delay %dms", entry.value);
delay(entry.value);
continue;
}
const uint8_t cmd = entry.value;
if (cmd == SLEEP_OUT)
delay(120); // NOLINT
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, entry.count,
format_hex_pretty_to(hex_buf, entry.args, entry.count, '.'));
this->write_command_(cmd);
for (uint8_t i = 0; i < entry.count; i++)
this->write_data_(entry.args[i]);
if (cmd == SLEEP_OUT)
delay(10);
}
// this->spi_teardown(); // SPI not needed after this
delay(10);
+14 -4
View File
@@ -33,7 +33,6 @@ from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SI
import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BRIGHTNESS,
CONF_BUFFER_SIZE,
CONF_COLOR_ORDER,
@@ -73,6 +72,10 @@ Model = mipi_spi_ns.enum("Model")
PixelMode = mipi_spi_ns.enum("PixelMode")
BusType = mipi_spi_ns.enum("BusType")
# Matches INIT_SEQUENCE_MAX_ARGS, the flash copy buffer in mipi_spi.h
INIT_SEQUENCE_MAX_ARGS = 128
COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
@@ -216,7 +219,15 @@ def model_schema(config: ConfigType) -> cv.All | cv.Schema:
bus_mode, lower=True
),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
iseqconf: cv.ensure_list(map_sequence),
iseqconf: cv.ensure_list(
cv.All(
map_sequence,
cv.Length(
max=INIT_SEQUENCE_MAX_ARGS + 1,
msg=f"An init sequence command can have at most {INIT_SEQUENCE_MAX_ARGS} data bytes",
),
)
),
cv.Optional(CONF_BUFFER_SIZE): cv.All(
cv.percentage, cv.Range(0.12, 1.0)
),
@@ -290,8 +301,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
height,
has_hardware_transform,
byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config)
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
has_writer=display.config_has_writer(config),
rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
)
+18 -28
View File
@@ -7,12 +7,16 @@
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mipi_spi {
/// Largest argument count in one init command; display.py rejects longer ones.
static constexpr size_t INIT_SEQUENCE_MAX_ARGS = 128;
ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
// Maximum bytes to log for commands (truncated if larger)
@@ -48,7 +52,6 @@ static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horiz
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
static constexpr uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian.
static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
@@ -163,36 +166,23 @@ class MipiSpi : public display::Display,
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
size_t index = 0;
const uint8_t *seq = this->init_sequence_;
const size_t len = this->init_sequence_len_;
while (index != len) {
if (len - index < 2) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
display::InitSequenceEntry entry;
// The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
uint8_t args[INIT_SEQUENCE_MAX_ARGS];
while (auto step = reader.next(entry, sizeof(args))) {
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
uint8_t cmd = progmem_read_byte(seq + index++);
uint8_t x = progmem_read_byte(seq + index++);
if (x == DELAY_FLAG) {
if (cmd == 0) {
cmd = clamp_at_least((int) (when - millis()), 0);
}
esph_log_d(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (len - index < num_args) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
// The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
uint8_t args[0x80];
progmem_memcpy(args, seq + index, num_args);
this->write_command_(cmd, args, num_args);
index += num_args;
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
const uint8_t ms = entry.value != 0 ? entry.value : clamp_at_least((int) (when - millis()), 0);
esph_log_d(TAG, "Delay %dms", ms);
delay(ms);
continue;
}
progmem_memcpy(args, entry.args, entry.count);
this->write_command_(entry.value, args, entry.count);
}
this->reset_params_();
// Marks init as done, so later commands log at verbose level instead of debug
+1 -8
View File
@@ -4,7 +4,6 @@ import pkgutil
from esphome import pins
import esphome.codegen as cg
from esphome.components import display, spi
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.mipi import (
DriverChip,
flatten_sequence,
@@ -13,16 +12,13 @@ from esphome.components.mipi import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_HEIGHT,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_LAMBDA,
CONF_MODEL,
CONF_PAGES,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_WIDTH,
@@ -182,10 +178,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
config[CONF_HEIGHT],
has_hardware_rotation=False,
byte_order=cv.UNDEFINED,
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
or config.get(CONF_PAGES) is not None
or config.get(CONF_LAMBDA) is not None
or config.get(CONF_SHOW_TEST_CARD) is True,
has_writer=display.config_has_writer(config),
rotation=config.get(CONF_ROTATION, 0),
)
return config
+15 -22
View File
@@ -56,33 +56,26 @@ void ST7305::reset_() {
}
void ST7305::send_init_sequence_() {
while (this->init_index_ < this->init_sequence_length_) {
if (this->init_sequence_length_ - this->init_index_ < 2) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
const uint8_t command = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
const uint8_t count = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
if (count == 0xFF) {
// Resume the sequence after the delay instead of blocking the main loop.
this->set_timeout(STARTUP_TIMEOUT_ID, command, [this]() { this->send_init_sequence_(); });
return;
}
if (this->init_sequence_length_ - this->init_index_ < count) {
this->mark_failed(LOG_STR("Malformed init sequence"));
return;
}
display::InitSequenceEntry entry;
display::InitSequenceStep step;
while ((step = this->init_reader_.next(entry)) == display::INIT_SEQUENCE_STEP_COMMAND) {
this->enable();
this->send_command_(command);
if (count != 0) {
this->send_command_(entry.value);
if (entry.count != 0) {
this->dc_pin_->digital_write(true);
for (uint8_t i = 0; i < count; i++)
this->write_byte(progmem_read_byte(&this->init_sequence_[this->init_index_ + i]));
for (uint8_t i = 0; i < entry.count; i++)
this->write_byte(progmem_read_byte(entry.args + i));
}
this->disable();
this->init_index_ += count;
}
this->finish_init_();
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
// Resume the sequence after the delay instead of blocking the main loop.
this->set_timeout(STARTUP_TIMEOUT_ID, entry.value, [this]() { this->send_init_sequence_(); });
} else if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(LOG_STR("Malformed init sequence"));
} else {
this->finish_init_();
}
}
void ST7305::finish_init_() {
+3 -5
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
@@ -35,8 +36,7 @@ class ST7305 : public display::DisplayBuffer,
row_start_(row_start),
row_end_(row_end),
buffer_length_(static_cast<size_t>(width) * static_cast<size_t>(height) / 8),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length) {}
init_reader_(init_sequence, init_sequence_length) {}
void setup() override;
void update() override;
@@ -113,9 +113,7 @@ class ST7305 : public display::DisplayBuffer,
uint8_t row_start_;
uint8_t row_end_;
size_t buffer_length_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
size_t init_index_{0};
display::InitSequenceReader init_reader_;
bool ready_{false};
int x_low_{0};
@@ -93,6 +93,16 @@ def run_schema_validation(config: ConfigType) -> None:
r"value must be a power of two for dictionary value @ data\['draw_rounding'\]",
id="invalid_draw_rounding",
),
pytest.param(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 320, "height": 240},
"init_sequence": [[0xE0] + [0x00] * 129],
},
r"at most 128 data bytes",
id="init_command_too_long",
),
],
)
def test_basic_configuration_errors(
@@ -606,3 +616,19 @@ def test_identical_init_sequences_share_one_table(
assert first is not None and second is not None
assert first.group(1) == second.group(1)
assert main_cpp.count("mipi_spi_init_sequence[] PROGMEM") == 1
def test_init_command_at_buffer_limit(set_core_config: SetCoreConfigCallable) -> None:
"""A command with exactly 128 data bytes fits the C++ copy buffer."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
run_schema_validation(
{
"model": "custom",
"dc_pin": 18,
"dimensions": {"width": 320, "height": 240},
"init_sequence": [[0xE0] + [0x00] * 128],
}
)
@@ -0,0 +1,82 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/display/init_sequence.h"
namespace esphome::display::testing {
TEST(InitSequenceReader, WalksCommandsAndDelays) {
static const uint8_t SEQ[] = {0x11, 0x00, 120, 0xFF, 0x3A, 0x01, 0x55, 0x2A, 0x02, 0x00, 0xEF};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x11);
EXPECT_EQ(entry.count, 0);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_DELAY);
EXPECT_EQ(entry.value, 120);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x3A);
ASSERT_EQ(entry.count, 1);
EXPECT_EQ(entry.args, &SEQ[6]);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x2A);
ASSERT_EQ(entry.count, 2);
EXPECT_EQ(entry.args[0], 0x00);
EXPECT_EQ(entry.args[1], 0xEF);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, EmptySequenceEnds) {
InitSequenceReader reader(nullptr, 0);
InitSequenceEntry entry{};
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
// A count above 127 is a real length, not a flag bit.
TEST(InitSequenceReader, HonoursFullCount) {
uint8_t seq[2 + 253 + 2] = {0x32, 253};
seq[2 + 253] = 0x29;
InitSequenceReader reader(seq, sizeof(seq));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x32);
EXPECT_EQ(entry.count, 253);
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(entry.value, 0x29);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, TruncatedHeaderIsMalformed) {
static const uint8_t SEQ[] = {0x11, 0x00, 0x29};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_MALFORMED);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, ShortArgsAreMalformed) {
static const uint8_t SEQ[] = {0x2A, 0x04, 0x00, 0x00, 0x00};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_MALFORMED);
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
}
TEST(InitSequenceReader, CountOverMaxIsMalformed) {
static const uint8_t SEQ[] = {10, 0xFF, 0x2A, 0x02, 0x00, 0xEF};
InitSequenceReader reader(SEQ, sizeof(SEQ));
InitSequenceEntry entry{};
ASSERT_EQ(reader.next(entry, 1), INIT_SEQUENCE_STEP_DELAY);
EXPECT_EQ(reader.next(entry, 1), INIT_SEQUENCE_STEP_MALFORMED);
}
} // namespace esphome::display::testing
+20 -2
View File
@@ -1,4 +1,4 @@
"""Tests for display.add_writer."""
"""Tests for display.add_writer and display.config_has_writer."""
from unittest.mock import AsyncMock, MagicMock, patch
@@ -6,7 +6,8 @@ import pytest
from esphome import codegen as cg
from esphome.components import display
from esphome.const import CONF_LAMBDA
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.const import CONF_AUTO_CLEAR_ENABLED, CONF_LAMBDA, CONF_PAGES
@pytest.mark.parametrize(
@@ -39,3 +40,20 @@ async def test_add_writer_without_lambda_does_nothing() -> None:
process.assert_not_awaited()
add.assert_not_called()
@pytest.mark.parametrize(
("config", "expected"),
[
({}, False),
({CONF_AUTO_CLEAR_ENABLED: True}, True),
({CONF_AUTO_CLEAR_ENABLED: False}, False),
({CONF_AUTO_CLEAR_ENABLED: "unspecified"}, False),
({CONF_PAGES: []}, True),
({CONF_LAMBDA: "x"}, True),
({CONF_SHOW_TEST_CARD: True}, True),
({CONF_SHOW_TEST_CARD: False}, False),
],
)
def test_config_has_writer(config: dict, expected: bool) -> None:
assert display.config_has_writer(config) is expected