Files
planar-EL512.256-H/components/el512/el512_display.cpp
T
mangavalkandClaude Opus 5.5 e9b91c5628 Add four animated KonoSuba ESPHome pages
Megumin's daily explosion, the Adventurers' Guild quest board (agenda,
weather, solar), Megumin's adventurer card and a party battle against a
giant toad with a live-data battle log. Original 1-bit pixel art lives in
tools/konosuba_sprites.py, which generates konosuba_sprites.h and can
render a preview PNG.

Home data and drawing helpers shared by the themed pages move to
el512_pages.h; the display gains dither_mask() for a dissolve-in intro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:46:24 +02:00

218 lines
7.1 KiB
C++

#include "el512_display.h"
#include <algorithm>
#include <cstring>
#include <esp_heap_caps.h>
#include <esp_private/esp_clk.h>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::el512 {
static const char *const TAG = "el512";
void EL512Display::setup() {
this->panel_ = new EL512Panel(this->pins_); // NOLINT(cppcoreguidelines-owning-memory)
if (!this->panel_->begin()) {
ESP_LOGE(TAG, "Panel init failed (out of DMA memory or no free interrupt)");
this->mark_failed();
return;
}
// Apply states that switches may have restored before setup ran.
this->panel_->setLowPower(this->low_power_);
this->panel_->setEnabled(this->enabled_);
}
void EL512Display::loop() {
if (this->panel_ == nullptr || !this->panel_->running())
return;
this->check_refresh_health_();
const uint32_t now = millis();
this->page_age_ms(); // notice page changes even when nothing draws
if (now - this->animated_at_ >= this->animation_interval_) {
const auto *page = this->get_active_page();
for (const auto *p : this->animated_pages_) {
if (p == page) {
this->animated_at_ = now;
this->update();
break;
}
}
}
if (this->still_alive_page_ == nullptr)
return;
const bool active = this->is_still_alive_playing();
if (active && !this->still_alive_was_active_) {
// Remember where we came from, then start the show from the top.
this->still_alive_return_ = this->previous_page_ != this->still_alive_page_ ? this->previous_page_ : nullptr;
this->still_alive_.start(now);
ESP_LOGI(TAG, "Still Alive started; start the song in %u ms", (unsigned) this->still_alive_song_start_ms());
}
this->still_alive_was_active_ = active;
if (!active)
return;
if (this->still_alive_.finished(now) && this->still_alive_return_ != nullptr) {
this->show_page(this->still_alive_return_);
this->update();
return;
}
if (this->still_alive_.update(now))
this->update();
}
// Refresh health, checked every 10 s: warn when the ESP32 side of the video
// signal had a problem (DMA FIFO underrun, a refill later than the ring's lead,
// or a frame swap that timed out); otherwise a debug summary once a minute.
// No warnings while the panel misbehaves means the signal left the ESP32
// intact and was disturbed on the wires (e.g. by Wi-Fi transmit bursts).
void EL512Display::check_refresh_health_() {
const uint32_t now = millis();
if (this->health_checked_ == 0) {
// Skip the start-up period (the FIFO is empty once while the DMA starts).
this->panel_->takeStats();
this->health_checked_ = now;
return;
}
if (now - this->health_checked_ < 10000)
return;
const uint32_t period_ms = now - this->health_checked_;
this->health_checked_ = now;
const auto s = this->panel_->takeStats();
const float max_gap_us = s.maxGapCycles / (float) (esp_clk_cpu_freq() / 1000000);
const float lead_us = 1e6f * EL512Panel::RING_LINES * EL512Panel::CLOCKS_PER_ROW / EL512Panel::PIXEL_CLOCK_HZ;
if (s.underruns > 0 || s.showTimeouts > 0 || max_gap_us > lead_us * 0.75f) {
ESP_LOGW(TAG, "Refresh problem in the last %us: %u FIFO underruns, %u swap timeouts, max refill gap %.0f us",
(unsigned) (period_ms / 1000), (unsigned) s.underruns, (unsigned) s.showTimeouts, max_gap_us);
}
if (now - this->health_logged_ >= 60000) {
this->health_logged_ = now;
ESP_LOGD(TAG, "Refresh OK: %.0f refills/s, max gap %.0f us, %u swaps", s.refills * 1000.0f / period_ms,
max_gap_us, (unsigned) s.swaps);
}
}
void EL512Display::play_still_alive() {
if (this->still_alive_page_ == nullptr)
return;
if (this->is_still_alive_playing()) {
this->still_alive_.start(millis()); // already showing: just restart
} else {
this->show_page(this->still_alive_page_); // loop() starts it
}
}
void EL512Display::draw_still_alive() {
if (this->panel_ != nullptr)
this->still_alive_.draw(*this->panel_);
}
uint32_t EL512Display::page_age_ms() {
const uint32_t now = millis();
if (this->get_active_page() != this->age_page_) {
this->age_page_ = this->get_active_page();
this->age_start_ = now;
}
return now - this->age_start_;
}
void EL512Display::shift_rows(int y, int height, int dx) {
if (this->panel_ == nullptr)
return;
constexpr int W = EL512Panel::WIDTH, ROW = W / 8;
dx %= W;
if (dx < 0)
dx += W;
if (dx == 0)
return;
uint8_t *fb = this->panel_->framebuffer();
uint8_t src[ROW];
for (int r = std::max(y, 0); r < std::min(y + height, (int) EL512Panel::HEIGHT); r++) {
uint8_t *row = fb + r * ROW;
memcpy(src, row, ROW);
memset(row, 0, ROW);
for (int x = 0; x < W; x++) {
const int s = (x - dx + W) % W;
if (src[s >> 3] & (0x80 >> (s & 7)))
row[x >> 3] |= 0x80 >> (x & 7);
}
}
}
void EL512Display::invert_rect(int x, int y, int width, int height) {
if (this->panel_ == nullptr)
return;
for (int py = y; py < y + height; py++)
for (int px = x; px < x + width; px++)
this->panel_->setPixel(px, py, !this->panel_->getPixel(px, py));
}
void EL512Display::dither_mask(int level) {
if (this->panel_ == nullptr || level >= 16)
return;
static const uint8_t BAYER[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
uint8_t masks[4];
for (int r = 0; r < 4; r++) {
masks[r] = 0;
for (int x = 0; x < 8; x++)
if (BAYER[r][x & 3] < level)
masks[r] |= 0x80 >> x;
}
uint8_t *fb = this->panel_->framebuffer();
constexpr int ROW = EL512Panel::WIDTH / 8;
for (int y = 0; y < EL512Panel::HEIGHT; y++)
for (int i = 0; i < ROW; i++)
fb[y * ROW + i] &= masks[y & 3];
}
void EL512Display::update() {
if (this->panel_ == nullptr || !this->panel_->running())
return;
this->do_update_();
this->panel_->show();
}
void EL512Display::fill(Color color) {
// The panel clear is a memset; with clipping active, fall back to per-pixel.
if (this->panel_ == nullptr || this->get_clipping().is_set()) {
Display::fill(color);
return;
}
this->panel_->clear(color.is_on());
}
void EL512Display::draw_absolute_pixel_internal(int x, int y, Color color) {
if (this->panel_ != nullptr)
this->panel_->setPixel(x, y, color.is_on());
}
void EL512Display::set_enabled(bool on) {
this->enabled_ = on;
if (this->panel_ != nullptr)
this->panel_->setEnabled(on);
}
void EL512Display::set_low_power(bool on) {
this->low_power_ = on;
if (this->panel_ != nullptr)
this->panel_->setLowPower(on);
}
void EL512Display::dump_config() {
LOG_DISPLAY("", "EL512.256-H", this);
ESP_LOGCONFIG(TAG,
" Pins: VCLK %d, VID %d, TVID %d, HS %d, VS %d, ENABLE %d, _LOWPWR %d\n"
" Refresh: %.1f Hz, DMA ring %u bytes\n"
" Free internal heap: %u bytes",
this->pins_.vclk, this->pins_.vid, this->pins_.tvid, this->pins_.hs, this->pins_.vs,
this->pins_.enable, this->pins_.lowpwr_n, EL512Panel::refreshHz(),
(unsigned) EL512Panel::RING_BYTES,
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
LOG_UPDATE_INTERVAL(this);
if (this->is_failed())
ESP_LOGE(TAG, " Init failed");
}
} // namespace esphome::el512