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} 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: def get_display_metadata(display_id: ID) -> DisplayMetaData:
"""Get display metadata by ID object """Get display metadata by ID object
@@ -210,10 +220,7 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
height=dimensions[1], height=dimensions[1],
has_hardware_rotation=False, has_hardware_rotation=False,
byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED), byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED),
has_writer=disp_config.get(CONF_AUTO_CLEAR_ENABLED) is True has_writer=config_has_writer(disp_config),
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,
rotation=disp_config.get(CONF_ROTATION, 0), rotation=disp_config.get(CONF_ROTATION, 0),
draw_rounding=disp_config.get(CONF_DRAW_ROUNDING, 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 import esphome.config_validation as cv
from esphome.config_validation import update_interval from esphome.config_validation import update_interval
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BUSY_PIN, CONF_BUSY_PIN,
CONF_CS_PIN, CONF_CS_PIN,
CONF_DATA_RATE, CONF_DATA_RATE,
@@ -189,10 +188,7 @@ def customise_schema(config):
height, height,
has_hardware_rotation=True, has_hardware_rotation=True,
byte_order=cv.UNDEFINED, byte_order=cv.UNDEFINED,
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True has_writer=display.config_has_writer(config),
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,
rotation=config.get(CONF_ROTATION, 0), rotation=config.get(CONF_ROTATION, 0),
draw_rounding=0, draw_rounding=0,
) )
+9 -16
View File
@@ -1,4 +1,5 @@
#include "epaper_spi.h" #include "epaper_spi.h"
#include "esphome/components/display/init_sequence.h"
#include <cinttypes> #include <cinttypes>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -315,26 +316,18 @@ void EPaperBase::start_data_() {
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) { void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) {
size_t index = 0; display::InitSequenceReader reader(sequence, length);
display::InitSequenceEntry entry;
while (index != length) { while (auto step = reader.next(entry)) {
if (length - index < 2) { if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(LOG_STR("Malformed init sequence")); this->mark_failed(LOG_STR("Malformed init sequence"));
return; return;
} }
const uint8_t cmd = sequence[index++]; if (step == display::INIT_SEQUENCE_STEP_DELAY) {
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { ESP_LOGV(TAG, "Delay %dms", entry.value);
ESP_LOGV(TAG, "Delay %dms", cmd); delay(entry.value);
delay(cmd);
} else { } else {
const uint8_t num_args = x & 0x7F; this->cmd_data(entry.value, entry.args, entry.count);
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;
} }
} }
} }
@@ -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; 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 // 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 uint32_t FAST_BUSY_POLL_MS = 50;
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public Display, class EPaperBase : public Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, 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 import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
CONF_DIMENSIONS, CONF_DIMENSIONS,
CONF_ENABLE_PIN, CONF_ENABLE_PIN,
@@ -167,8 +166,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
height, height,
has_hardware_rotation=False, has_hardware_rotation=False,
byte_order=config[CONF_BYTE_ORDER], byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config) has_writer=display.config_has_writer(config),
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
rotation=config.get(CONF_ROTATION, 0), rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0), draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
) )
+29 -37
View File
@@ -1,6 +1,7 @@
#ifdef USE_ESP32_VARIANT_ESP32P4 #ifdef USE_ESP32_VARIANT_ESP32P4
#include <utility> #include <utility>
#include "mipi_dsi.h" #include "mipi_dsi.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <algorithm> #include <algorithm>
@@ -123,47 +124,38 @@ void MipiDsi::setup() {
this->smark_failed(LOG_STR("esp_lcd_init failed"), err); this->smark_failed(LOG_STR("esp_lcd_init failed"), err);
return; return;
} }
size_t index = 0; display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
const uint8_t *seq = this->init_sequence_; display::InitSequenceEntry entry;
const size_t len = this->init_sequence_len_; while (auto step = reader.next(entry)) {
while (index != len) { if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
if (len - index < 2) {
this->mark_failed(LOG_STR("Malformed init sequence")); this->mark_failed(LOG_STR("Malformed init sequence"));
return; return;
} }
uint8_t cmd = seq[index++]; if (step == display::INIT_SEQUENCE_STEP_DELAY) {
uint8_t x = seq[index++]; ESP_LOGD(TAG, "Delay %dms", entry.value);
if (x == DELAY_FLAG) { delay(entry.value);
ESP_LOGD(TAG, "Delay %dms", cmd); continue;
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);
} }
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(); this->io_lock_ = xSemaphoreCreateBinary();
esp_lcd_dpi_panel_event_callbacks_t cbs = { 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 INVERT_ON = 0x21;
const uint8_t DISPLAY_ON = 0x29; const uint8_t DISPLAY_ON = 0x29;
const uint8_t CMD2_BKSEL = 0xFF; const uint8_t CMD2_BKSEL = 0xFF;
const uint8_t DELAY_FLAG = 0xFF;
const uint8_t MADCTL_BGR = 0x08; const uint8_t MADCTL_BGR = 0x08;
const uint8_t MADCTL_MX = 0x40; const uint8_t MADCTL_MX = 0x40;
const uint8_t MADCTL_MY = 0x80; 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 import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BLUE, CONF_BLUE,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
CONF_CS_PIN, CONF_CS_PIN,
@@ -243,8 +242,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
height, height,
has_hardware_rotation=True, has_hardware_rotation=True,
byte_order=config[CONF_BYTE_ORDER], byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config) has_writer=display.config_has_writer(config),
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
rotation=config.get(CONF_ROTATION, 0), rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 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) #if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#include "mipi_rgb.h" #include "mipi_rgb.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/core/gpio.h" #include "esphome/core/gpio.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -13,8 +14,6 @@
namespace esphome::mipi_rgb { namespace esphome::mipi_rgb {
static const uint8_t DELAY_FLAG = 0xFF;
// Maximum bytes to log for init commands (truncated if larger) // Maximum bytes to log for init commands (truncated if larger)
static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64; static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64;
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top 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->disable();
} }
/**
* this relies upon the init sequence being well-formed, which is guaranteed by the Python init code.
*/
void MipiRgbSpi::write_init_sequence_() { void MipiRgbSpi::write_init_sequence_() {
size_t index = 0; display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
const uint8_t *seq = this->init_sequence_; display::InitSequenceEntry entry;
const size_t len = this->init_sequence_len_; while (auto step = reader.next(entry)) {
while (index != len) { if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
if (len - index < 2) {
this->mark_failed(LOG_STR("Malformed init sequence")); this->mark_failed(LOG_STR("Malformed init sequence"));
return; return;
} }
uint8_t cmd = seq[index++]; if (step == display::INIT_SEQUENCE_STEP_DELAY) {
uint8_t x = seq[index++]; ESP_LOGD(TAG, "Delay %dms", entry.value);
if (x == DELAY_FLAG) { delay(entry.value);
ESP_LOGD(TAG, "Delay %dms", cmd); continue;
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);
} }
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 // this->spi_teardown(); // SPI not needed after this
delay(10); 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 import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA from esphome.config_validation import ALLOW_EXTRA
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
@@ -73,6 +72,10 @@ Model = mipi_spi_ns.enum("Model")
PixelMode = mipi_spi_ns.enum("PixelMode") PixelMode = mipi_spi_ns.enum("PixelMode")
BusType = mipi_spi_ns.enum("BusType") 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 = { COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR, MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
@@ -216,7 +219,15 @@ def model_schema(config: ConfigType) -> cv.All | cv.Schema:
bus_mode, lower=True bus_mode, lower=True
), ),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=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.Optional(CONF_BUFFER_SIZE): cv.All(
cv.percentage, cv.Range(0.12, 1.0) cv.percentage, cv.Range(0.12, 1.0)
), ),
@@ -290,8 +301,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
height, height,
has_hardware_transform, has_hardware_transform,
byte_order=config[CONF_BYTE_ORDER], byte_order=config[CONF_BYTE_ORDER],
has_writer=requires_buffer(config) has_writer=display.config_has_writer(config),
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
rotation=config.get(CONF_ROTATION, 0), rotation=config.get(CONF_ROTATION, 0),
draw_rounding=config.get(CONF_DRAW_ROUNDING, 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/spi/spi.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h" #include "esphome/components/display/display_color_utils.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::mipi_spi { 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"); ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
// Maximum bytes to log for commands (truncated if larger) // 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 uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits 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. // store a 16 bit value in a buffer, big endian.
static inline void put16_be(uint8_t *buf, uint16_t value) { static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8; 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 // need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120; auto when = millis() + 120;
size_t index = 0; display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
const uint8_t *seq = this->init_sequence_; display::InitSequenceEntry entry;
const size_t len = this->init_sequence_len_; // The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
while (index != len) { uint8_t args[INIT_SEQUENCE_MAX_ARGS];
if (len - index < 2) { while (auto step = reader.next(entry, sizeof(args))) {
esph_log_e(TAG, "Malformed init sequence"); if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
this->mark_failed(); this->mark_failed(LOG_STR("Malformed init sequence"));
return; return;
} }
uint8_t cmd = progmem_read_byte(seq + index++); if (step == display::INIT_SEQUENCE_STEP_DELAY) {
uint8_t x = progmem_read_byte(seq + index++); const uint8_t ms = entry.value != 0 ? entry.value : clamp_at_least((int) (when - millis()), 0);
if (x == DELAY_FLAG) { esph_log_d(TAG, "Delay %dms", ms);
if (cmd == 0) { delay(ms);
cmd = clamp_at_least((int) (when - millis()), 0); continue;
}
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;
} }
progmem_memcpy(args, entry.args, entry.count);
this->write_command_(entry.value, args, entry.count);
} }
this->reset_params_(); this->reset_params_();
// Marks init as done, so later commands log at verbose level instead of debug // 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 from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.mipi import ( from esphome.components.mipi import (
DriverChip, DriverChip,
flatten_sequence, flatten_sequence,
@@ -13,16 +12,13 @@ from esphome.components.mipi import (
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_CS_PIN, CONF_CS_PIN,
CONF_DATA_RATE, CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_HEIGHT, CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_INIT_SEQUENCE, CONF_INIT_SEQUENCE,
CONF_LAMBDA,
CONF_MODEL, CONF_MODEL,
CONF_PAGES,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_ROTATION, CONF_ROTATION,
CONF_WIDTH, CONF_WIDTH,
@@ -182,10 +178,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
config[CONF_HEIGHT], config[CONF_HEIGHT],
has_hardware_rotation=False, has_hardware_rotation=False,
byte_order=cv.UNDEFINED, byte_order=cv.UNDEFINED,
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True has_writer=display.config_has_writer(config),
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,
rotation=config.get(CONF_ROTATION, 0), rotation=config.get(CONF_ROTATION, 0),
) )
return config return config
+15 -22
View File
@@ -56,33 +56,26 @@ void ST7305::reset_() {
} }
void ST7305::send_init_sequence_() { void ST7305::send_init_sequence_() {
while (this->init_index_ < this->init_sequence_length_) { display::InitSequenceEntry entry;
if (this->init_sequence_length_ - this->init_index_ < 2) { display::InitSequenceStep step;
this->mark_failed(LOG_STR("Malformed init sequence")); while ((step = this->init_reader_.next(entry)) == display::INIT_SEQUENCE_STEP_COMMAND) {
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;
}
this->enable(); this->enable();
this->send_command_(command); this->send_command_(entry.value);
if (count != 0) { if (entry.count != 0) {
this->dc_pin_->digital_write(true); this->dc_pin_->digital_write(true);
for (uint8_t i = 0; i < count; i++) for (uint8_t i = 0; i < entry.count; i++)
this->write_byte(progmem_read_byte(&this->init_sequence_[this->init_index_ + i])); this->write_byte(progmem_read_byte(entry.args + i));
} }
this->disable(); 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_() { void ST7305::finish_init_() {
+3 -5
View File
@@ -4,6 +4,7 @@
#include <cstdint> #include <cstdint>
#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/init_sequence.h"
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@@ -35,8 +36,7 @@ class ST7305 : public display::DisplayBuffer,
row_start_(row_start), row_start_(row_start),
row_end_(row_end), row_end_(row_end),
buffer_length_(static_cast<size_t>(width) * static_cast<size_t>(height) / 8), buffer_length_(static_cast<size_t>(width) * static_cast<size_t>(height) / 8),
init_sequence_(init_sequence), init_reader_(init_sequence, init_sequence_length) {}
init_sequence_length_(init_sequence_length) {}
void setup() override; void setup() override;
void update() override; void update() override;
@@ -113,9 +113,7 @@ class ST7305 : public display::DisplayBuffer,
uint8_t row_start_; uint8_t row_start_;
uint8_t row_end_; uint8_t row_end_;
size_t buffer_length_; size_t buffer_length_;
const uint8_t *init_sequence_; display::InitSequenceReader init_reader_;
size_t init_sequence_length_;
size_t init_index_{0};
bool ready_{false}; bool ready_{false};
int x_low_{0}; 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'\]", r"value must be a power of two for dictionary value @ data\['draw_rounding'\]",
id="invalid_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( 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 is not None and second is not None
assert first.group(1) == second.group(1) assert first.group(1) == second.group(1)
assert main_cpp.count("mipi_spi_init_sequence[] PROGMEM") == 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 from unittest.mock import AsyncMock, MagicMock, patch
@@ -6,7 +6,8 @@ import pytest
from esphome import codegen as cg from esphome import codegen as cg
from esphome.components import display 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( @pytest.mark.parametrize(
@@ -39,3 +40,20 @@ async def test_add_writer_without_lambda_does_nothing() -> None:
process.assert_not_awaited() process.assert_not_awaited()
add.assert_not_called() 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