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:
@@ -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(); }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
65
esphome/components/epaper_spi/models/__init__.py
Normal file
65
esphome/components/epaper_spi/models/__init__.py
Normal 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)
|
||||||
51
esphome/components/epaper_spi/models/spectra_e6.py
Normal file
51
esphome/components/epaper_spi/models/spectra_e6.py
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user