1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-17 15:26:01 +00:00

Merge branch 'integration' into memory_api

This commit is contained in:
J. Nick Koston
2025-11-03 16:36:17 -06:00
14 changed files with 619 additions and 344 deletions

View File

@@ -210,7 +210,7 @@ class Display : public PollingComponent {
/// Fill the entire screen with the given color. /// Fill the entire screen with the given color.
virtual void fill(Color color); virtual void fill(Color color);
/// Clear the entire screen by filling it with OFF pixels. /// Clear the entire screen by filling it with OFF pixels.
void clear(); virtual void clear();
/// Get the calculated width of the display in pixels with rotation applied. /// Get the calculated width of the display in pixels with rotation applied.
virtual int get_width() { return this->get_width_internal(); } virtual int get_width() { return this->get_width_internal(); }

View File

@@ -1,21 +1,35 @@
import importlib
import pkgutil
from esphome import core, pins from esphome import core, 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.mipi import flatten_sequence, map_sequence
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUSY_PIN, CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_INIT_SEQUENCE,
CONF_LAMBDA, CONF_LAMBDA,
CONF_MODEL, CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION, CONF_RESET_DURATION,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_WIDTH,
) )
from . import models
AUTO_LOAD = ["split_buffer"] AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_( EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
@@ -24,30 +38,79 @@ EPaperBase = epaper_spi_ns.class_(
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
MODELS = {
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
}
# Import all models dynamically from the models package
for module_info in pkgutil.iter_modules(models.__path__):
importlib.import_module(f".models.{module_info.name}", package=__package__)
CONFIG_SCHEMA = cv.All( MODELS = models.EpaperModel.models
display.FULL_DISPLAY_SCHEMA.extend(
{ DIMENSION_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(EPaperBase), {
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), cv.Required(CONF_HEIGHT): cv.int_,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, }
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
) )
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
)
)
.extend(
{
model.option(pin): pins.gpio_output_pin_schema
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN)
}
)
.extend(
{
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema,
cv.GenerateID(): cv.declare_id(class_name),
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
model.option(CONF_ENABLE_PIN): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
map_sequence
),
model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
)
def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
config = cv.Schema(
{
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
},
extra=cv.ALLOW_EXTRA,
)(config)
return model_schema(config)(config)
CONFIG_SCHEMA = customise_schema
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True "epaper_spi", require_miso=False, require_mosi=True
) )
@@ -56,8 +119,23 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
rhs = model.new() init_sequence = config.get(CONF_INIT_SEQUENCE)
var = cg.Pvariable(config[CONF_ID], rhs, model) if init_sequence is None:
init_sequence = model.get_init_sequence(config)
init_sequence = flatten_sequence(init_sequence)
init_sequence_length = len(init_sequence)
init_sequence_id = cg.static_const_array(
config[CONF_INIT_SEQUENCE_ID], init_sequence
)
width, height = model.get_dimensions(config)
var = cg.new_Pvariable(
config[CONF_ID],
model.name,
width,
height,
init_sequence_id,
init_sequence_length,
)
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)

View File

@@ -8,33 +8,20 @@ namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi"; static const char *const TAG = "epaper_spi";
static const LogString *epaper_state_to_string(EPaperState state) { static constexpr const char *const EPAPER_STATE_STRINGS[] = {
switch (state) { "IDLE", "UPDATE", "RESET", "RESET_END",
case EPaperState::IDLE:
return LOG_STR("IDLE"); "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
case EPaperState::UPDATE: };
return LOG_STR("UPDATE");
case EPaperState::RESET: const char *EPaperBase::epaper_state_to_string_() {
return LOG_STR("RESET"); if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
case EPaperState::INITIALISE: return EPAPER_STATE_STRINGS[idx];
return LOG_STR("INITIALISE"); return "Unknown";
case EPaperState::TRANSFER_DATA:
return LOG_STR("TRANSFER_DATA");
case EPaperState::POWER_ON:
return LOG_STR("POWER_ON");
case EPaperState::REFRESH_SCREEN:
return LOG_STR("REFRESH_SCREEN");
case EPaperState::POWER_OFF:
return LOG_STR("POWER_OFF");
case EPaperState::DEEP_SLEEP:
return LOG_STR("DEEP_SLEEP");
default:
return LOG_STR("UNKNOWN");
}
} }
void EPaperBase::setup() { void EPaperBase::setup() {
if (!this->init_buffer_(this->get_buffer_length())) { if (!this->init_buffer_(this->buffer_length_)) {
this->mark_failed("Failed to initialise buffer"); this->mark_failed("Failed to initialise buffer");
return; return;
} }
@@ -50,7 +37,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
return true; return true;
} }
void EPaperBase::setup_pins_() { void EPaperBase::setup_pins_() const {
this->dc_pin_->setup(); // OUTPUT this->dc_pin_->setup(); // OUTPUT
this->dc_pin_->digital_write(false); this->dc_pin_->digital_write(false);
@@ -81,11 +68,7 @@ void EPaperBase::data(uint8_t value) {
// write a command followed by zero or more bytes of data. // write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data. // The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...] // [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(const uint8_t *data) { void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
const uint8_t command = data[0];
const uint8_t length = data[1];
const uint8_t *ptr = data + 2;
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str()); format_hex_pretty(ptr, length, '.', false).c_str());
@@ -99,91 +82,146 @@ void EPaperBase::cmd_data(const uint8_t *data) {
this->disable(); this->disable();
} }
bool EPaperBase::is_idle_() { bool EPaperBase::is_idle_() const {
if (this->busy_pin_ == nullptr) { if (this->busy_pin_ == nullptr) {
return true; return true;
} }
return this->busy_pin_->digital_read(); return !this->busy_pin_->digital_read();
} }
void EPaperBase::reset() { bool EPaperBase::reset_() const {
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false); if (this->state_ == EPaperState::RESET) {
this->disable_loop(); this->reset_pin_->digital_write(false);
this->set_timeout(this->reset_duration_, [this] { return false;
this->reset_pin_->digital_write(true); }
this->set_timeout(20, [this] { this->enable_loop(); }); this->reset_pin_->digital_write(true);
});
} }
return true;
} }
void EPaperBase::update() { void EPaperBase::update() {
if (!this->state_queue_.empty()) { if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display update already in progress - %s", ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
return; return;
} }
this->set_state_(EPaperState::RESET);
this->state_queue_.push(EPaperState::UPDATE);
this->state_queue_.push(EPaperState::RESET);
this->state_queue_.push(EPaperState::INITIALISE);
this->state_queue_.push(EPaperState::TRANSFER_DATA);
this->state_queue_.push(EPaperState::POWER_ON);
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
this->state_queue_.push(EPaperState::POWER_OFF);
this->state_queue_.push(EPaperState::DEEP_SLEEP);
this->state_queue_.push(EPaperState::IDLE);
this->enable_loop(); this->enable_loop();
} }
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (should_wait) {
this->waiting_for_idle_start_ = millis();
this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_;
}
#endif
this->waiting_for_idle_ = should_wait;
}
/**
* Called during the loop task.
* First defer for any pending delays, then check if we are waiting for the display to become idle.
* If not waiting for idle, process the state machine.
*/
void EPaperBase::loop() { void EPaperBase::loop() {
auto now = millis();
if (this->delay_until_ != 0) {
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0) {
return;
}
this->delay_until_ = 0;
}
if (this->waiting_for_idle_) { if (this->waiting_for_idle_) {
if (this->is_idle_()) { if (this->is_idle_()) {
this->waiting_for_idle_ = false; this->waiting_for_idle_ = false;
ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
} else { } else {
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Waiting for idle"); if (now - this->waiting_for_idle_last_print_ >= 1000) {
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time(); ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
this->waiting_for_idle_last_print_ = millis();
} }
#endif
return; return;
} }
} }
this->process_state_();
}
auto state = this->state_queue_.front(); /**
* Process the state machine.
switch (state) { * Typical state sequence:
* IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF ->
* DEEP_SLEEP -> IDLE
*
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->disable_loop();
break;
case EPaperState::IDLE: case EPaperState::IDLE:
this->disable_loop(); this->disable_loop();
break; break;
case EPaperState::RESET:
case EPaperState::RESET_END:
if (this->reset_()) {
this->set_state_(EPaperState::UPDATE);
} else {
this->set_state_(EPaperState::RESET_END);
}
break;
case EPaperState::UPDATE: case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda this->do_update_(); // Calls ESPHome (current page) lambda
break; this->set_state_(EPaperState::INITIALISE);
case EPaperState::RESET:
this->reset();
break; break;
case EPaperState::INITIALISE: case EPaperState::INITIALISE:
this->initialise_(); this->initialise_();
this->set_state_(EPaperState::TRANSFER_DATA);
break; break;
case EPaperState::TRANSFER_DATA: case EPaperState::TRANSFER_DATA:
if (!this->transfer_data()) { if (!this->transfer_data()) {
return; // Not done yet, come back next loop return; // Not done yet, come back next loop
} }
this->set_state_(EPaperState::POWER_ON);
break; break;
case EPaperState::POWER_ON: case EPaperState::POWER_ON:
this->power_on(); this->power_on();
this->set_state_(EPaperState::REFRESH_SCREEN);
break; break;
case EPaperState::REFRESH_SCREEN: case EPaperState::REFRESH_SCREEN:
this->refresh_screen(); this->refresh_screen();
this->set_state_(EPaperState::POWER_OFF);
break; break;
case EPaperState::POWER_OFF: case EPaperState::POWER_OFF:
this->power_off(); this->power_off();
this->set_state_(EPaperState::DEEP_SLEEP);
break; break;
case EPaperState::DEEP_SLEEP: case EPaperState::DEEP_SLEEP:
this->deep_sleep(); this->deep_sleep();
this->set_state_(EPaperState::IDLE);
break; break;
} }
this->state_queue_.pop(); }
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
if (delay != 0) {
this->delay_until_ = millis() + delay;
} else {
this->delay_until_ = 0;
}
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
} }
void EPaperBase::start_command_() { void EPaperBase::start_command_() {
@@ -203,25 +241,39 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() { void EPaperBase::initialise_() {
size_t index = 0; size_t index = 0;
const auto &sequence = this->init_sequence_;
const size_t sequence_size = this->init_sequence_length_;
while (index != sequence_size) {
if (sequence_size - index < 2) {
this->mark_failed("Malformed init sequence");
return;
}
const auto *ptr = sequence + index;
const uint8_t length = ptr[1];
if (sequence_size - index < length + 2) {
this->mark_failed("Malformed init sequence");
return;
}
this->cmd_data(ptr); auto *sequence = this->init_sequence_;
index += length + 2; auto length = this->init_sequence_length_;
while (index != length) {
if (length - index < 2) {
this->mark_failed("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);
} 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;
}
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
this->cmd_data(cmd, sequence + index, num_args);
index += num_args;
}
} }
}
this->power_on(); void EPaperBase::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: %s", this->name_);
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);
} }
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -8,36 +8,48 @@
#include <queue> #include <queue>
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
using namespace display;
enum class EPaperState : uint8_t { enum class EPaperState : uint8_t {
IDLE, IDLE, // not doing anything
UPDATE, UPDATE, // update the buffer
RESET, RESET, // drive reset low (active)
INITIALISE, RESET_END, // drive reset high (inactive)
TRANSFER_DATA,
POWER_ON, SHOULD_WAIT, // states higher than this should wait for the display to be not busy
REFRESH_SCREEN, INITIALISE, // send the init sequence
POWER_OFF, TRANSFER_DATA, // transfer data to the display
DEEP_SLEEP, POWER_ON, // power on the display
REFRESH_SCREEN, // send refresh command
POWER_OFF, // power off the display
DEEP_SLEEP, // deep sleep the display
}; };
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public display::DisplayBuffer, class EPaperBase : public DisplayBuffer,
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,
spi::DATA_RATE_2MHZ> { spi::DATA_RATE_2MHZ> {
public: public:
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
: name_(name),
width_(width),
height_(height),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override; float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void dump_config() override;
void command(uint8_t value); void command(uint8_t value);
void data(uint8_t value); void data(uint8_t value);
void cmd_data(const uint8_t *data); void cmd_data(uint8_t command, const uint8_t *ptr, size_t length);
void update() override; void update() override;
void loop() override; void loop() override;
@@ -46,48 +58,84 @@ class EPaperBase : public display::DisplayBuffer,
void on_safe_shutdown() override; void on_safe_shutdown() override;
DisplayType get_display_type() override { return this->display_type_; };
protected: protected:
bool is_idle_(); int get_height_internal() override { return this->height_; };
void setup_pins_(); int get_width_internal() override { return this->width_; };
virtual void reset(); void process_state_();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
bool reset_() const;
void initialise_(); void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length); bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); }; virtual int get_width_controller() { return this->get_width_internal(); };
virtual void deep_sleep() = 0;
/**
* Methods that must be implemented by concrete classes to control the display
*/
/** /**
* Send data to the device via SPI * Send data to the device via SPI
* @return true if done, false if should be called next loop * @return true if done, false if it should be called next loop
*/ */
virtual bool transfer_data() = 0; virtual bool transfer_data() = 0;
/**
* Refresh the screen after data transfer
*/
virtual void refresh_screen() = 0; virtual void refresh_screen() = 0;
/**
* Power the display on
*/
virtual void power_on() = 0; virtual void power_on() = 0;
/**
* Power the display off
*/
virtual void power_off() = 0; virtual void power_off() = 0;
virtual uint32_t get_buffer_length() = 0;
/**
* Place the display into deep sleep
*/
virtual void deep_sleep() = 0;
void set_state_(EPaperState state, uint16_t delay = 0);
void start_command_(); void start_command_();
void end_command_(); void end_command_();
void start_data_(); void start_data_();
void end_data_(); void end_data_();
const size_t init_sequence_length_{0}; // properties initialised in the constructor
const char *name_;
uint16_t width_;
uint16_t height_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
DisplayType display_type_;
size_t current_data_index_{0}; size_t buffer_length_{};
size_t current_data_index_{0}; // used by data transfer to track progress
uint32_t reset_duration_{200}; uint32_t reset_duration_{200};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t transfer_start_time_{};
uint32_t waiting_for_idle_last_print_{0}; uint32_t waiting_for_idle_last_print_{0};
uint32_t waiting_for_idle_start_{0};
#endif
GPIOPin *dc_pin_; GPIOPin *dc_pin_{};
GPIOPin *busy_pin_{nullptr}; GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{};
const uint8_t *init_sequence_{nullptr};
bool waiting_for_idle_{false}; bool waiting_for_idle_{false};
uint32_t delay_until_{0};
split_buffer::SplitBuffer buffer_; split_buffer::SplitBuffer buffer_;
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}}; EPaperState state_{EPaperState::IDLE};
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -1,42 +0,0 @@
#include "epaper_spi_model_7p3in_spectra_e6.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
void EPaper7p3InSpectraE6::power_on() {
ESP_LOGI(TAG, "Power on");
this->command(0x04);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::power_off() {
ESP_LOGI(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::refresh_screen() {
ESP_LOGI(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::deep_sleep() {
ESP_LOGI(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
void EPaper7p3InSpectraE6::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
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);
}
} // namespace esphome::epaper_spi

View File

@@ -1,45 +0,0 @@
#pragma once
#include "epaper_spi_spectra_e6.h"
namespace esphome::epaper_spi {
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
static constexpr const uint16_t WIDTH = 800;
static constexpr const uint16_t HEIGHT = 480;
// clang-format off
// Command, data length, data
static constexpr uint8_t INIT_SEQUENCE[] = {
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
0x01, 1, 0x3F,
0x00, 2, 0x5F, 0x69,
0x03, 4, 0x00, 0x54, 0x00, 0x44,
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
0x30, 1, 0x03,
0x50, 1, 0x3F,
0x60, 2, 0x02, 0x00,
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
0x84, 1, 0x01,
0xE3, 1, 0x2F,
};
// clang-format on
public:
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
void dump_config() override;
protected:
int get_width_internal() override { return WIDTH; };
int get_height_internal() override { return HEIGHT; };
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,135 +1,166 @@
#include "epaper_spi_spectra_e6.h" #include "epaper_spi_spectra_e6.h"
#include <algorithm>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c"; static constexpr const char *const TAG = "epaper_spi.6c";
static constexpr size_t MAX_TRANSFER_SIZE = 128;
static constexpr unsigned char GRAY_THRESHOLD = 50;
static inline uint8_t color_to_hex(Color color) { enum E6Color {
if (color.red > 127) { BLACK,
if (color.green > 170) { WHITE,
if (color.blue > 127) { YELLOW,
return 0x1; // White RED,
} else { SKIP_1,
return 0x2; // Yellow BLUE,
} GREEN,
} else { CYAN,
return 0x3; // Red (or Magenta) SKIP_2,
} };
} else {
if (color.green > 127) { static uint8_t color_to_hex(Color color) {
if (color.blue > 127) { // --- Step 1: Check for Grayscale (Black or White) ---
return 0x5; // Cyan -> Blue // We define "grayscale" as a color where the min and max components
} else { // are close to each other.
return 0x6; // Green unsigned char max_rgb = std::max({color.r, color.g, color.b});
} unsigned char min_rgb = std::min({color.r, color.g, color.b});
} else {
if (color.blue > 127) { if ((max_rgb - min_rgb) < GRAY_THRESHOLD) {
return 0x5; // Blue // It's a shade of gray. Map to BLACK or WHITE.
} else { // We split the luminance at the halfway point (382 = (255*3)/2)
return 0x0; // Black if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
} return WHITE;
} }
return BLACK;
} }
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
bool r_on = (color.r > 128);
bool g_on = (color.g > 128);
bool b_on = (color.b > 128);
if (r_on && g_on && !b_on) {
return YELLOW;
}
if (r_on && !g_on && !b_on) {
return RED;
}
if (!r_on && g_on && !b_on) {
return GREEN;
}
if (!r_on && !g_on && b_on) {
return BLUE;
}
// Handle "impure" colors (Cyan, Magenta)
if (!r_on && g_on && b_on) {
// Cyan (G+B) -> Closest is Green or Blue. Pick Green.
return GREEN;
}
if (r_on && !g_on) {
// Magenta (R+B) -> Closest is Red or Blue. Pick Red.
return RED;
}
// Handle the remaining corners (White-ish, Black-ish)
if (r_on) {
// All high (but not gray) -> White
return WHITE;
}
// !r_on && !g_on && !b_on
// All low (but not gray) -> Black
return BLACK;
}
void EPaperSpectraE6::power_on() {
ESP_LOGD(TAG, "Power on");
this->command(0x04);
}
void EPaperSpectraE6::power_off() {
ESP_LOGD(TAG, "Power off");
this->command(0x02);
this->data(0x00);
}
void EPaperSpectraE6::refresh_screen() {
ESP_LOGD(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGD(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
} }
void EPaperSpectraE6::fill(Color color) { void EPaperSpectraE6::fill(Color color) {
uint8_t pixel_color; auto pixel_color = color_to_hex(color);
if (color.is_on()) {
pixel_color = color_to_hex(color);
} else {
pixel_color = 0x1;
}
// We store 8 bitset<3> in 3 bytes // We store 2 pixels per byte
// | byte 1 | byte 2 | byte 3 | this->buffer_.fill(pixel_color + (pixel_color << 4));
// |aaabbbaa|abbbaaab|bbaaabbb|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
const size_t buffer_length = this->get_buffer_length();
for (size_t i = 0; i < buffer_length; i += 3) {
this->buffer_[i + 0] = byte_1;
this->buffer_[i + 1] = byte_2;
this->buffer_[i + 2] = byte_3;
}
} }
uint32_t EPaperSpectraE6::get_buffer_length() { void EPaperSpectraE6::clear() {
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes // clear buffer to white, just like real paper.
return this->get_width_controller() * this->get_height_internal() / 8u * 3u; this->fill(COLOR_ON);
} }
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return; return;
uint8_t pixel_bits = color_to_hex(color); auto pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller(); uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t first_bit_position = pixel_position * 3; uint32_t byte_position = pixel_position / 2;
uint32_t byte_position = first_bit_position / 8u; auto original = this->buffer_[byte_position];
uint32_t byte_subposition = first_bit_position % 8u; if ((pixel_position & 1) != 0) {
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
if (byte_subposition <= 5) {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
(pixel_bits << (5 - byte_subposition));
} else { } else {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4);
(pixel_bits >> (byte_subposition - 5));
this->buffer_[byte_position + 1] =
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
(pixel_bits << (13 - byte_subposition));
} }
} }
bool HOT EPaperSpectraE6::transfer_data() { bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time(); const uint32_t start_time = App.get_loop_component_start_time();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) { if (this->current_data_index_ == 0) {
ESP_LOGV(TAG, "Sending data"); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->transfer_start_time_ = millis();
#endif
ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis());
this->command(0x10); this->command(0x10);
} }
uint8_t bytes_to_send[4]{0}; size_t buf_idx = 0;
const size_t buffer_length = this->get_buffer_length(); uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) { while (this->current_data_index_ != buffer_length) {
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
// 8 pixels are stored in 3 bytes
// |aaabbbaa|abbbaaab|bbaaabbb|
// | byte 1 | byte 2 | byte 3 |
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
this->start_data_(); if (buf_idx == sizeof bytes_to_send) {
this->write_array(bytes_to_send, sizeof(bytes_to_send)); this->start_data_();
this->end_data_(); this->write_array(bytes_to_send, buf_idx);
this->end_data_();
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) { if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop // Let the main loop run and come back next loop
this->current_data_index_ = i + 3; return false;
return false; }
} }
} }
// Finished the entire dataset // Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->end_data_();
}
this->current_data_index_ = 0; this->current_data_index_ = 0;
ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_);
return true; return true;
} }
void EPaperSpectraE6::reset() {
if (this->reset_pin_ != nullptr) {
this->disable_loop();
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] {
this->reset_pin_->digital_write(false);
delay(2);
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -6,18 +6,23 @@ namespace esphome::epaper_spi {
class EPaperSpectraE6 : public EPaperBase { class EPaperSpectraE6 : public EPaperBase {
public: public:
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length) EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
: EPaperBase(init_sequence, init_sequence_length) {} size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = width * height / 2; // 2 pixels per byte
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void fill(Color color) override; void fill(Color color) override;
void clear() override;
protected: protected:
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void draw_absolute_pixel_internal(int x, int y, Color color) override; void draw_absolute_pixel_internal(int x, int y, Color color) override;
uint32_t get_buffer_length() override;
bool transfer_data() override; bool transfer_data() override;
void reset() override;
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -0,0 +1,65 @@
from typing import Any, Self
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
class EpaperModel:
models: dict[str, Self] = {}
def __init__(
self,
name: str,
class_name: str,
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.class_name = class_name
self.initsequence = initsequence
self.defaults = defaults
EpaperModel.models[name] = self
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
def get_init_sequence(self, config: dict):
return self.initsequence
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
if fallback is None and self.get_default(name, None) is None:
return cv.Required(name)
return cv.Optional(name, default=self.get_default(name, fallback))
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
else:
(width, height) = dimensions
else:
# Default dimensions, use model defaults
width = self.get_default(CONF_WIDTH)
height = self.get_default(CONF_HEIGHT)
return width, height
def extend(self, name, **kwargs) -> "EpaperModel":
"""
Extend the current model with additional parameters or a modified init sequence.
Parameters supplied here will override the defaults of the current model.
if the initsequence is not provided, the current model's initsequence will be used.
If add_init_sequence is provided, it will be appended to the current initsequence.
:param name:
:param kwargs:
:return:
"""
initsequence = list(kwargs.pop("initsequence", self.initsequence) or ())
initsequence.extend(kwargs.pop("add_init_sequence", ()))
defaults = self.defaults.copy()
defaults.update(kwargs)
return self.__class__(name, initsequence=tuple(initsequence), **defaults)

View File

@@ -0,0 +1,51 @@
from typing import Any
from . import EpaperModel
class SpectraE6(EpaperModel):
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
super().__init__(name, class_name, **kwargs)
# fmt: off
def get_init_sequence(self, config: dict):
width, height = self.get_dimensions(config)
return (
(0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,),
(0x01, 0x3F,),
(0x00, 0x5F, 0x69,),
(0x03, 0x00, 0x54, 0x00, 0x44,),
(0x05, 0x40, 0x1F, 0x1F, 0x2C,),
(0x06, 0x6F, 0x1F, 0x17, 0x49,),
(0x08, 0x6F, 0x1F, 0x1F, 0x22,),
(0x30, 0x03,),
(0x50, 0x3F,),
(0x60, 0x02, 0x00,),
(0x61, width // 256, width % 256, height // 256, height % 256,),
(0x84, 0x01,),
(0xE3, 0x2F,),
)
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6.extend(
"Seeed-reTerminal-E1002",
width=800,
height=480,
data_rate="20MHz",
cs_pin=10,
dc_pin=11,
reset_pin=12,
busy_pin={
"number": 13,
"inverted": True,
"mode": {
"input": True,
"pullup": True,
},
},
)

View File

@@ -218,6 +218,21 @@ def map_sequence(value):
return tuple(value) return tuple(value)
def flatten_sequence(sequence: tuple | list):
"""
Flatten an init sequence into a single list of bytes.
:param sequence: The list of tuples
:return: a list of bytes
"""
return sum(
tuple(
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
for x in sequence
),
(),
)
def delay(ms): def delay(ms):
return DELAY_FLAG, ms return DELAY_FLAG, ms
@@ -456,13 +471,7 @@ class DriverChip:
# Flatten the sequence into a list of bytes, with the length of each command # Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed # or the delay flag inserted where needed
return sum( return flatten_sequence(sequence), madctl
tuple(
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
for x in sequence
),
(),
), madctl
def requires_buffer(config) -> bool: def requires_buffer(config) -> bool:

View File

@@ -4,7 +4,6 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::split_buffer { namespace esphome::split_buffer {
static constexpr const char *const TAG = "split_buffer"; static constexpr const char *const TAG = "split_buffer";
SplitBuffer::~SplitBuffer() { this->free(); } SplitBuffer::~SplitBuffer() { this->free(); }
@@ -102,32 +101,44 @@ void SplitBuffer::free() {
this->total_length_ = 0; this->total_length_ = 0;
} }
uint8_t &SplitBuffer::operator[](size_t index) { const uint8_t &SplitBuffer::operator[](size_t index) const {
if (index >= this->total_length_) { if (index >= this->total_length_) {
ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_);
// Return reference to a static dummy byte to avoid crash // Return reference to a static dummy byte since we can't throw exceptions.
// the byte is non-const since it will also be used by the non-const [] overload.
static uint8_t dummy = 0; static uint8_t dummy = 0;
return dummy; return dummy;
} }
size_t buffer_index = index / this->buffer_size_; const auto buffer_index = index / this->buffer_size_;
size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; const auto offset_in_buffer = index % this->buffer_size_;
return this->buffers_[buffer_index][offset_in_buffer]; return this->buffers_[buffer_index][offset_in_buffer];
} }
const uint8_t &SplitBuffer::operator[](size_t index) const { // non-const version of operator[] for write access
if (index >= this->total_length_) { uint8_t &SplitBuffer::operator[](size_t index) {
ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); // avoid code duplication. These casts are safe since we know the object is not const.
// Return reference to a static dummy byte to avoid crash return const_cast<uint8_t &>(static_cast<const SplitBuffer *>(this)->operator[](index));
static const uint8_t DUMMY = 0; }
return DUMMY;
/**
* Fill the entire buffer with a single byte value
* @param value Fill value
*/
void SplitBuffer::fill(uint8_t value) const {
if (this->buffer_count_ == 0)
return;
// clear all the full sized buffers
size_t i = 0;
for (; i != this->buffer_count_ - 1; i++) {
memset(this->buffers_[i], value, this->buffer_size_);
} }
// clear the last, potentially short, buffer.
size_t buffer_index = index / this->buffer_size_; // `i` is guaranteed to equal the last index since the loop terminates at that value.
size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; // where all buffers are the same size, the modulus must return the size, not 0.
auto size_last = ((this->total_length_ - 1) % this->buffer_size_) + 1;
return this->buffers_[buffer_index][offset_in_buffer]; memset(this->buffers_[i], value, size_last);
} }
} // namespace esphome::split_buffer } // namespace esphome::split_buffer

View File

@@ -4,7 +4,13 @@
#include <cstdlib> #include <cstdlib>
namespace esphome::split_buffer { namespace esphome::split_buffer {
/**
* A SplitBuffer allocates a large memory buffer potentially as multiple smaller buffers
* to facilitate allocation of large buffers on devices with fragmented memory spaces.
* Each sub-buffer is the same size, except for the last one which may be smaller.
* Standard array indexing using `[]` is possible on the buffer, but, since the buffer may not be contiguous in memory,
* there is no easy way to access the buffer as a single array, i.e. no `.data()` access like a vector.
*/
class SplitBuffer { class SplitBuffer {
public: public:
SplitBuffer() = default; SplitBuffer() = default;
@@ -19,13 +25,13 @@ class SplitBuffer {
// Access operators // Access operators
uint8_t &operator[](size_t index); uint8_t &operator[](size_t index);
const uint8_t &operator[](size_t index) const; const uint8_t &operator[](size_t index) const;
void fill(uint8_t value) const;
// Get the total length // Get the total length
size_t size() const { return this->total_length_; } size_t size() const { return this->total_length_; }
// Get buffer information // Get buffer information
size_t get_buffer_count() const { return this->buffer_count_; } size_t get_buffer_count() const { return this->buffer_count_; }
size_t get_buffer_size() const { return this->buffer_size_; }
// Check if successfully initialized // Check if successfully initialized
bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; } bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; }

View File

@@ -4,7 +4,10 @@ packages:
display: display:
- platform: epaper_spi - platform: epaper_spi
spi_id: spi_bus spi_id: spi_bus
model: 7.3in-spectra-e6 model: spectra-e6
dimensions:
width: 800
height: 480
cs_pin: GPIO5 cs_pin: GPIO5
dc_pin: GPIO17 dc_pin: GPIO17
reset_pin: GPIO16 reset_pin: GPIO16
@@ -13,3 +16,6 @@ display:
update_interval: 60s update_interval: 60s
lambda: |- lambda: |-
it.circle(64, 64, 50, Color::BLACK); it.circle(64, 64, 50, Color::BLACK);
- platform: epaper_spi
model: seeed-reterminal-e1002