#include "epaper_spi.h" #include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128; static constexpr const char *const EPAPER_STATE_STRINGS[] = { "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", }; const char *EPaperBase::epaper_state_to_string_() { if (auto idx = static_cast(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) return EPAPER_STATE_STRINGS[idx]; return "Unknown"; } void EPaperBase::setup() { if (!this->init_buffer_(this->buffer_length_)) { this->mark_failed(LOG_STR("Failed to initialise buffer")); return; } this->setup_pins_(); this->spi_setup(); } bool EPaperBase::init_buffer_(size_t buffer_length) { if (!this->buffer_.init(buffer_length)) { return false; } this->clear(); return true; } void EPaperBase::setup_pins_() const { this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); // OUTPUT this->reset_pin_->digital_write(true); } if (this->busy_pin_ != nullptr) { this->busy_pin_->setup(); // INPUT } } float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; } void EPaperBase::command(uint8_t value) { ESP_LOGV(TAG, "Command: 0x%02X", value); this->dc_pin_->digital_write(false); this->enable(); this->write_byte(value); this->disable(); } // write a command followed by zero or more bytes of data. void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(EPAPER_MAX_CMD_LOG_BYTES)]; ESP_LOGV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, format_hex_pretty_to(hex_buf, ptr, length, '.')); #endif this->dc_pin_->digital_write(false); this->enable(); this->write_byte(command); if (length > 0) { this->dc_pin_->digital_write(true); this->write_array(ptr, length); } this->disable(); } bool EPaperBase::is_idle_() const { if (this->busy_pin_ == nullptr) { return true; } return !this->busy_pin_->digital_read(); } bool EPaperBase::reset() { if (this->reset_pin_ != nullptr) { if (this->state_ == EPaperState::RESET) { this->reset_pin_->digital_write(false); return false; } this->reset_pin_->digital_write(true); } return true; } void EPaperBase::update_effective_transform_() { switch (this->rotation_) { case DISPLAY_ROTATION_90_DEGREES: this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X); break; case DISPLAY_ROTATION_180_DEGREES: this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X); break; case DISPLAY_ROTATION_270_DEGREES: this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y); break; default: this->effective_transform_ = this->transform_; break; } } void EPaperBase::update() { if (this->state_ != EPaperState::IDLE) { ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); return; } this->set_state_(EPaperState::UPDATE); this->enable_loop(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG this->update_start_time_ = millis(); #endif } void EPaperBase::wait_for_idle_(bool should_wait) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE this->waiting_for_idle_start_ = millis(); #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() { auto now = millis(); // using modulus arithmetic to handle wrap-around int diff = now - this->delay_until_; if (diff < 0) return; if (this->waiting_for_idle_) { if (this->is_idle_()) { this->waiting_for_idle_ = false; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); #endif } else { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE if (now - this->waiting_for_idle_last_print_ >= 1000) { ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); this->waiting_for_idle_last_print_ = millis(); } #endif return; } } this->process_state_(); } /** * Process the state machine. * 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_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); this->set_state_(EPaperState::IDLE); break; case EPaperState::IDLE: this->disable_loop(); break; case EPaperState::RESET: case EPaperState::RESET_END: if (this->reset()) { this->set_state_(EPaperState::INITIALISE); } else { this->set_state_(EPaperState::RESET_END, this->reset_duration_); } break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) { this->set_state_(EPaperState::IDLE); return; } this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: if (!this->initialise(this->update_count_ != 0)) { return; // Not done yet, come back next loop } this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: if (!this->transfer_data()) { return; // Not done yet, come back next loop } this->x_low_ = this->width_; this->x_high_ = 0; this->y_low_ = this->height_; this->y_high_ = 0; this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: this->power_on(); this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: this->refresh_screen(this->update_count_ != 0); this->update_count_ = (this->update_count_ + 1) % this->full_update_every_; this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: this->power_off(); this->set_state_(EPaperState::DEEP_SLEEP); break; case EPaperState::DEEP_SLEEP: this->deep_sleep(); this->set_state_(EPaperState::IDLE); ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_); break; } } 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); // allow subclasses to nominate delays if (delay == 0) delay = this->next_delay_; this->next_delay_ = 0; this->delay_until_ = millis() + delay; ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, TRUEFALSE(this->waiting_for_idle_)); if (state == EPaperState::IDLE) { this->disable_loop(); } } void EPaperBase::start_data_() { this->dc_pin_->digital_write(true); this->enable(); } void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) { size_t index = 0; while (index != length) { if (length - index < 2) { this->mark_failed(LOG_STR("Malformed init sequence")); return; } const uint8_t cmd = sequence[index++]; if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { ESP_LOGV(TAG, "Delay %dms", cmd); delay(cmd); } else { const uint8_t num_args = x & 0x7F; if (length - index < num_args) { ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args); this->mark_failed(); return; } this->cmd_data(cmd, sequence + index, num_args); index += num_args; } } } bool EPaperBase::initialise(bool partial) { this->send_init_sequence_(this->init_sequence_, this->init_sequence_length_); return true; } /** * Check and rotate coordinates based on the transform flags. * @param x * @param y * @return false if the coordinates are out of bounds */ bool EPaperBase::rotate_coordinates_(int &x, int &y) { if (!this->get_clipping().inside(x, y)) return false; if (this->effective_transform_ & SWAP_XY) std::swap(x, y); if (this->effective_transform_ & MIRROR_X) x = this->width_ - x - 1; if (this->effective_transform_ & MIRROR_Y) y = this->height_ - y - 1; if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) return false; this->x_low_ = clamp_at_most(this->x_low_, x); this->x_high_ = clamp_at_least(this->x_high_, x + 1); this->y_low_ = clamp_at_most(this->y_low_, y); this->y_high_ = clamp_at_least(this->y_high_, y + 1); return true; } /** * Default implementation for monochrome displays where 8 pixels are packed to a byte. * @param x * @param y * @param color */ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { if (!rotate_coordinates_(x, y)) return; const size_t byte_position = y * this->row_width_ + x / 8; const uint8_t bit_position = x % 8; const uint8_t pixel_bit = 0x80 >> bit_position; const auto original = this->buffer_[byte_position]; if ((color_to_bit(color) == 0)) { this->buffer_[byte_position] = original & ~pixel_bit; } else { this->buffer_[byte_position] = original | pixel_bit; } } void EPaperBase::dump_config() { LOG_DISPLAY("", "E-Paper SPI", this); ESP_LOGCONFIG(TAG, " Model: %s\n" " SPI Data Rate: %uMHz\n" " Full update every: %d\n" " Swap X/Y: %s\n" " Mirror X: %s\n" " Mirror Y: %s", this->name_, (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, YESNO(this->transform_ & SWAP_XY), YESNO(this->transform_ & MIRROR_X), YESNO(this->transform_ & MIRROR_Y)); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); } } // namespace esphome::epaper_spi