mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470ed7cc06 | ||
|
|
195c8285d7 | ||
|
|
e5b5dd52ef | ||
|
|
014e6938c3 | ||
|
|
67a56da25c | ||
|
|
bf36f7532d | ||
|
|
10c32a5cc7 | ||
|
|
69f6429728 | ||
|
|
4ae6ec545c | ||
|
|
263de44633 | ||
|
|
bab99748f0 |
No files matched your search
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_() {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user