From 2850633f5575995301515920c8f7f2e255004bcc Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Sat, 13 Jun 2026 13:00:50 +0200 Subject: [PATCH 1/8] Ajout du refresh partiel par zone (mode live secteur) Pourquoi: en mode live alimente sur secteur, rafraichir tout l'ecran en GC16 (flash ~0.5-1s) a chaque changement de capteur est lent et visuellement penible. Le materiel IT8951 sait deja rafraichir un sous-rectangle avec une waveform rapide (DU mode 1, sans flash) mais le composant cablait update() en plein ecran. Approche: exposer deux methodes publiques sans casser l'existant. - full_refresh(): comportement historique (INIT mode 0 + GC16 mode 2), utilise au boot et pour le deghost nocturne; reset le compteur de partiels. - refresh_zone(x,y,w,h,mode=1): re-rend le framebuffer complet (pour refleter les valeurs courantes) puis ne pousse que le rectangle demande en DU. Conversion des coordonnees logiques vers panneau (framebuffer miroir en X) et alignement x/w sur 4px (contrainte 4bpp). Garde-fou: full_refresh force tous les 60 partiels pour purger le ghosting. update() delegue desormais a full_refresh(). Extraction de wake_panel_/sleep_panel_ pour mutualiser la sequence reveil/sommeil de l'IT8951. Alternatives ecartees: chemin 1bpp partiel (alignement 16px plus contraignant et re-packing complexe) -> on reutilise le slice direct du framebuffer 4bpp, byte-order identique au chemin plein ecran. Pas de patch du flux de sommeil (le panneau dort toujours entre rafraichissements: ~10ms de reveil, image bistable conservee). Impact: aucune regression sur le mode plein ecran; nouvelles methodes opt-in appelables depuis un lambda ESPHome. Securite: bornage/clamp des rectangles, buffer de ligne borne a 936 octets (largeur max 1872px en 4bpp). --- .../it8951_reterminal_e1003.cpp | 137 ++++++++++++++++-- .../it8951_reterminal_e1003.h | 12 ++ 2 files changed, 135 insertions(+), 14 deletions(-) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index 3e44258..3c00600 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -433,12 +433,9 @@ void IT8951ReTerminalE1003Display::draw_driver_test_pattern_() { } } -void IT8951ReTerminalE1003Display::update() { - if (this->framebuffer_ == nullptr) { - ESP_LOGW(TAG, "Skipping update because the framebuffer is not available"); - return; - } +void IT8951ReTerminalE1003Display::update() { this->full_refresh(); } +void IT8951ReTerminalE1003Display::wake_panel_() { if (this->it8951_sleeping_) { ESP_LOGD(TAG, "Waking IT8951 from inter-refresh sleep"); digitalWrite(IT8951_PIN_EN, HIGH); @@ -446,6 +443,22 @@ void IT8951ReTerminalE1003Display::update() { this->lcd_sys_run_(); this->it8951_sleeping_ = false; } +} + +void IT8951ReTerminalE1003Display::sleep_panel_() { + this->lcd_write_cmd_code_(IT8951_TCON_SLEEP); + digitalWrite(IT8951_PIN_EN, LOW); + this->it8951_sleeping_ = true; + ESP_LOGD(TAG, "IT8951 sleeping, EPD_Drive power cut"); +} + +void IT8951ReTerminalE1003Display::full_refresh() { + if (this->framebuffer_ == nullptr) { + ESP_LOGW(TAG, "Skipping update because the framebuffer is not available"); + return; + } + + this->wake_panel_(); this->do_update_(); this->log_framebuffer_stats_(); @@ -454,7 +467,7 @@ void IT8951ReTerminalE1003Display::update() { this->log_framebuffer_stats_(); } - ESP_LOGD(TAG, "Transferring image to IT8951..."); + ESP_LOGD(TAG, "Transferring full image to IT8951..."); const uint16_t w = this->get_width_internal(); const uint16_t h = this->get_height_internal(); @@ -474,16 +487,13 @@ void IT8951ReTerminalE1003Display::update() { const uint16_t one_bpp_width_bytes = w / 8; ESP_LOGD(TAG, "Using sharp 1bpp upload path"); this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_8BPP, 0, 0, 0, one_bpp_width_bytes, h); - ESP_LOGD(TAG, "Uploading %u rows x %u packed bytes", h, one_bpp_width_bytes); this->lcd_write_framebuffer_1bpp_(w, h); } else { ESP_LOGD(TAG, "Using 4bpp grayscale upload path"); this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_4BPP, 0, 0, 0, w, h); - ESP_LOGD(TAG, "Uploading %u rows x %u words", h, width_in_words); this->lcd_write_framebuffer_4bpp_(reinterpret_cast(this->framebuffer_), width_in_words, h); } - ESP_LOGD(TAG, "Framebuffer upload complete"); this->lcd_write_cmd_code_(IT8951_TCON_LD_IMG_END); // INIT pass (mode 0) fully discharges residual pixel state from the previous image, @@ -501,12 +511,111 @@ void IT8951ReTerminalE1003Display::update() { } this->first_update_ = false; - this->lcd_write_cmd_code_(IT8951_TCON_SLEEP); - digitalWrite(IT8951_PIN_EN, LOW); - this->it8951_sleeping_ = true; - ESP_LOGD(TAG, "IT8951 sleeping, EPD_Drive power cut"); + this->sleep_panel_(); + this->partials_since_full_ = 0; +} + +// Upload only a sub-rectangle of the framebuffer in 4bpp. x and w MUST be +// multiples of 4 (4 pixels per 16-bit word). Byte order matches +// lcd_write_framebuffer_4bpp_ (MSB-first per word). +void IT8951ReTerminalE1003Display::lcd_write_framebuffer_4bpp_area_(uint16_t x, uint16_t y, uint16_t w, + uint16_t h) { + const uint16_t words = w / 4; + const uint32_t row_size_bytes = uint32_t(words) * 2; + uint8_t row_buffer[936]; + if (row_size_bytes > sizeof(row_buffer)) { + ESP_LOGE(TAG, "Area row buffer too small for %u-byte transfer", static_cast(row_size_bytes)); + return; + } + const uint32_t fb_row_bytes = uint32_t(this->get_width_internal()) / 2; + const uint32_t x_byte = uint32_t(x) / 2; + + digitalWrite(IT8951_PIN_CS, LOW); + SPI.beginTransaction(SPISettings(this->spi_frequency_, MSBFIRST, SPI_MODE0)); + this->lcd_wait_for_ready_(); + this->spi_send_word_(0x0000); + this->lcd_wait_for_ready_(); + + for (uint16_t yy = 0; yy < h; yy++) { + const uint8_t *src = this->framebuffer_ + (uint32_t(y) + yy) * fb_row_bytes + x_byte; + for (uint16_t i = 0; i < words; i++) { + const uint8_t b0 = src[uint32_t(i) * 2]; + const uint8_t b1 = src[uint32_t(i) * 2 + 1]; + row_buffer[uint32_t(i) * 2] = b1; + row_buffer[uint32_t(i) * 2 + 1] = b0; + } + SPI.writeBytes(row_buffer, row_size_bytes); + if ((yy & 0x07) == 0) { + App.feed_wdt(); + } + } + + SPI.endTransaction(); + digitalWrite(IT8951_PIN_CS, HIGH); +} + +void IT8951ReTerminalE1003Display::refresh_zone(int lx, int ly, int lw, int lh, int mode) { + if (this->framebuffer_ == nullptr || lw <= 0 || lh <= 0) { + return; + } + const int panel_w = this->get_width_internal(); + const int panel_h = this->get_height_internal(); + + // The framebuffer is mirrored in X (see draw_absolute_pixel_internal): + // logical column lx maps to panel column panel_w - 1 - lx. Convert the + // logical rectangle to panel coordinates, then align x/w to 4 px (4bpp). + int px = panel_w - lx - lw; + int pend = px + lw; + if (px < 0) px = 0; + if (pend > panel_w) pend = panel_w; + px &= ~0x03; + pend = (pend + 3) & ~0x03; + const int pw = pend - px; + + int y = ly; + int h = lh; + if (y < 0) { + h += y; + y = 0; + } + if (y + h > panel_h) { + h = panel_h - y; + } + if (pw <= 0 || h <= 0) { + return; + } - ESP_LOGV(TAG, "Display update triggered"); + this->wake_panel_(); + // Re-render the full framebuffer so the zone reflects current sensor values; + // only the rectangle below is physically pushed to the panel. + this->do_update_(); + this->wait_for_display_ready_(); + + this->lcd_write_cmd_code_(USDEF_I80_CMD_TEMP); + this->lcd_write_data_(0x0001); // Force Set from host + this->lcd_write_data_(static_cast(this->temperature_)); + + this->it8951_write_reg_(UP1SR + 2, this->it8951_read_reg_(UP1SR + 2) & ~(1 << 2)); + this->set_img_buf_base_addr_(this->img_buf_addr_); + this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_4BPP, 0, static_cast(px), + static_cast(y), static_cast(pw), + static_cast(h)); + this->lcd_write_framebuffer_4bpp_area_(static_cast(px), static_cast(y), + static_cast(pw), static_cast(h)); + this->lcd_write_cmd_code_(IT8951_TCON_LD_IMG_END); + this->wait_for_display_ready_(); + + ESP_LOGD(TAG, "Zone refresh panel x=%d y=%d w=%d h=%d mode=%d", px, y, pw, h, mode); + this->it8951_display_area_(static_cast(px), static_cast(y), static_cast(pw), + static_cast(h), static_cast(mode)); + this->wait_for_display_ready_(); + + this->sleep_panel_(); + + if (++this->partials_since_full_ >= MAX_PARTIALS_BEFORE_FULL) { + ESP_LOGD(TAG, "Partial threshold reached, forcing full refresh to purge ghosting"); + this->full_refresh(); + } } void IT8951ReTerminalE1003Display::dump_config() { diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index 84cb011..5f44977 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -61,6 +61,13 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { void setup() override; void update() override; void dump_config() override; + + // Full-screen INIT + GC16 refresh (flash). Used at boot and for the nightly + // deghost. Resets the partial-refresh counter. + void full_refresh(); + // Refresh ONLY the given logical rectangle (lambda coordinates) with a fast + // waveform (default mode 1 = DU, no flash). Re-renders the framebuffer first. + void refresh_zone(int x, int y, int w, int h, int mode = 1); float get_setup_priority() const override { return setup_priority::HARDWARE; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; } @@ -77,6 +84,9 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { void lcd_write_n_data_(uint16_t *buf, uint32_t word_count); void lcd_write_framebuffer_4bpp_(uint16_t *buf, uint16_t width_in_words, uint16_t height); void lcd_write_framebuffer_1bpp_(uint16_t width, uint16_t height); + void lcd_write_framebuffer_4bpp_area_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void wake_panel_(); + void sleep_panel_(); uint16_t lcd_read_data_(); void lcd_read_n_data_(uint16_t *buf, uint32_t word_count); void lcd_wait_for_ready_(); @@ -119,6 +129,8 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { bool it8951_sleeping_{false}; int8_t temperature_{23}; uint32_t spi_read_frequency_{1000000}; + uint32_t partials_since_full_{0}; + static const uint32_t MAX_PARTIALS_BEFORE_FULL = 60; }; } // namespace it8951_reterminal_e1003 From afdc67a860b0717da970b2e36856dc8905ed95e9 Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Sat, 13 Jun 2026 18:18:01 +0200 Subject: [PATCH 2/8] Separer rendu et push: render_framebuffer() + flush_zone() Pourquoi: en mode live, refresh_zone() re-rendait le framebuffer complet (do_update_, lambda ~3.3s a cause des ecritures PSRAM) a CHAQUE zone. Un tick multi-zones bloquait la boucle principale ~12s (3 zones), gelant WiFi/API. Approche: separer le rendu (couteux, 1x) du push (rapide, par zone). - render_framebuffer(): execute la lambda une fois (remplit le framebuffer RAM). - flush_zone(x,y,w,h,mode=1): pousse seulement le rectangle, SANS re-rendre (suppose le framebuffer a jour). Coeur deplace depuis l'ancien refresh_zone. - refresh_zone(): devient render_framebuffer() + flush_zone() (zone isolee/event). Le YAML appelle render_framebuffer() une fois puis flush_zone() par zone -> un seul rendu par tick au lieu de N. Impact: tick multi-zones passe de ~N*3.3s a ~3.3s + N*0.4s. Pas de changement de comportement par zone; aucune regression plein ecran. --- .../it8951_reterminal_e1003.cpp | 17 ++++++++++++++--- .../it8951_reterminal_e1003.h | 10 ++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index 3c00600..9bac5df 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -554,7 +554,21 @@ void IT8951ReTerminalE1003Display::lcd_write_framebuffer_4bpp_area_(uint16_t x, digitalWrite(IT8951_PIN_CS, HIGH); } +void IT8951ReTerminalE1003Display::render_framebuffer() { + if (this->framebuffer_ == nullptr) { + return; + } + // Runs the display lambda once. Heavy on PSRAM (~seconds); do this ONCE then + // call flush_zone() for each zone rather than re-rendering per zone. + this->do_update_(); +} + void IT8951ReTerminalE1003Display::refresh_zone(int lx, int ly, int lw, int lh, int mode) { + this->render_framebuffer(); + this->flush_zone(lx, ly, lw, lh, mode); +} + +void IT8951ReTerminalE1003Display::flush_zone(int lx, int ly, int lw, int lh, int mode) { if (this->framebuffer_ == nullptr || lw <= 0 || lh <= 0) { return; } @@ -586,9 +600,6 @@ void IT8951ReTerminalE1003Display::refresh_zone(int lx, int ly, int lw, int lh, } this->wake_panel_(); - // Re-render the full framebuffer so the zone reflects current sensor values; - // only the rectangle below is physically pushed to the panel. - this->do_update_(); this->wait_for_display_ready_(); this->lcd_write_cmd_code_(USDEF_I80_CMD_TEMP); diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index 5f44977..89cbdf7 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -65,8 +65,14 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { // Full-screen INIT + GC16 refresh (flash). Used at boot and for the nightly // deghost. Resets the partial-refresh counter. void full_refresh(); - // Refresh ONLY the given logical rectangle (lambda coordinates) with a fast - // waveform (default mode 1 = DU, no flash). Re-renders the framebuffer first. + // Re-render the whole framebuffer in RAM (runs the display lambda once). Cheap + // on CPU but NOT on PSRAM (~seconds); call ONCE then flush_zone() several times. + void render_framebuffer(); + // Push ONLY the given logical rectangle (lambda coordinates) to the panel with + // a fast waveform (default mode 1 = DU, no flash). Does NOT re-render; assumes + // the framebuffer is current (call render_framebuffer() first). + void flush_zone(int x, int y, int w, int h, int mode = 1); + // Convenience: render_framebuffer() + flush_zone() for a single isolated zone. void refresh_zone(int x, int y, int w, int h, int mode = 1); float get_setup_priority() const override { return setup_priority::HARDWARE; } From f184464493acfb770047c556f41a313a93b9bb34 Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Sat, 13 Jun 2026 18:57:14 +0200 Subject: [PATCH 3/8] fill() en memset + DU plein ecran comme strategie live Mesure live: chaque flush_zone (DU) coute ~2.4s (sequencage d'alim EPD par appel DPY_AREA, inherent au panneau 10"), independamment de la taille -> N zones = N*2.4s. Le rendu (do_update_) coutait ~3.3s a cause de fill() qui bouclait pixel par pixel (~2.6M appels draw_absolute_pixel) pour l'auto-clear. Corrections: - fill(Color) override en memset sur le framebuffer 4bpp (nibble*0x11). L'auto-clear par frame devient quasi instantane -> rendu ~0.3s. - Consequence strategie: un DU PLEIN ECRAN (flush_zone(0,0,1872,1404,1)) coute ~le meme qu'une zone (~2.4s) et reste sans flash. Donc 1 DU plein ecran >> N zones. Le YAML rafraichit tout en un seul DU au lieu de decouper en zones. - MAX_PARTIALS_BEFORE_FULL 60 -> 180 (avec un DU/minute, GC16 toutes les ~3h au lieu de chaque heure). Impact: tick live ~0.3s (rendu) + ~2.4s (DU) au lieu de ~17s. Pas de regression plein ecran GC16 (deghost). fill() override est transparent pour le reste. --- .../it8951_reterminal_e1003.cpp | 10 ++++++++++ .../it8951_reterminal_e1003/it8951_reterminal_e1003.h | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index 9bac5df..41902d7 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -648,6 +648,16 @@ void IT8951ReTerminalE1003Display::dump_config() { LOG_UPDATE_INTERVAL(this); } +void IT8951ReTerminalE1003Display::fill(Color color) { + if (this->framebuffer_ == nullptr) { + return; + } + const uint8_t gray8 = static_cast((77u * color.r + 150u * color.g + 29u * color.b) >> 8); + const uint8_t nib = gray8 >> 4; + const uint8_t byte = static_cast((nib << 4) | nib); + memset(this->framebuffer_, byte, static_cast(this->get_width_internal()) * this->get_height_internal() / 2); +} + void IT8951ReTerminalE1003Display::draw_absolute_pixel_internal(int x, int y, Color color) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) { return; diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index 89cbdf7..5aaf6dd 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -61,6 +61,9 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { void setup() override; void update() override; void dump_config() override; + // Fast uniform clear (memset) instead of the base per-pixel loop (~2.6M calls + // on this panel). Makes the per-frame auto-clear ~instant. + void fill(Color color) override; // Full-screen INIT + GC16 refresh (flash). Used at boot and for the nightly // deghost. Resets the partial-refresh counter. @@ -136,7 +139,7 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { int8_t temperature_{23}; uint32_t spi_read_frequency_{1000000}; uint32_t partials_since_full_{0}; - static const uint32_t MAX_PARTIALS_BEFORE_FULL = 60; + static const uint32_t MAX_PARTIALS_BEFORE_FULL = 180; }; } // namespace it8951_reterminal_e1003 From 5ccf729c4c9b141608899e2f612bf39db850e7cc Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Tue, 16 Jun 2026 21:41:39 +0200 Subject: [PATCH 4/8] Lecture SD + affichage image (.raw 4bpp) en GC16 Phase A photos: lire un fichier .raw (1872x1404 4bpp, format exact du framebuffer) depuis la carte SD et l'afficher en 16 gris (GC16). - SD sur le bus SPI PARTAGE avec l'IT8951 (un seul maitre Arduino SPI, CS distincts: ecran=10, SD=14). Alim SD = GPIO39 (SD_EN) activee au setup. - setup(): SD.begin(14, SPI, 10MHz) apres l'init IT8951; flag sd_ok_. - show_sd_image(path): SD.open + lecture par blocs dans framebuffer_ (1.3 Mo), puis upload 4bpp + INIT + GC16. Reutilise wake/sleep_panel_. Risque connu (a valider sur hard): coexistence SD.h / Arduino SPI direct de l'IT8951 sur le meme bus. CS coordonnes, transactions sequentielles. --- .../it8951_reterminal_e1003.cpp | 47 +++++++++++++++++++ .../it8951_reterminal_e1003.h | 7 +++ 2 files changed, 54 insertions(+) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index 41902d7..bcb1760 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -349,6 +349,13 @@ void IT8951ReTerminalE1003Display::setup() { } memset(this->framebuffer_, 0xFF, buffer_size); + // Carte SD sur le bus SPI partage (CS=14). Alimentation SD = GPIO39 (SD_EN). + pinMode(IT8951_PIN_SD_EN, OUTPUT); + digitalWrite(IT8951_PIN_SD_EN, HIGH); + delay(10); + this->sd_ok_ = SD.begin(IT8951_PIN_SD_CS, SPI, 10000000); + ESP_LOGCONFIG(TAG, "SD card: %s", this->sd_ok_ ? "OK" : "absente/echec init"); + ESP_LOGCONFIG(TAG, "IT8951 reTerminal E1003 initialization complete"); } @@ -747,5 +754,45 @@ void IT8951ReTerminalE1003Display::it8951_display_area_1bpp_(uint16_t x, uint16_ this->wait_for_display_ready_(); } +void IT8951ReTerminalE1003Display::show_sd_image(const char *path) { + if (this->framebuffer_ == nullptr) { ESP_LOGW(TAG, "show_sd_image: framebuffer absent"); return; } + if (!this->sd_ok_) { ESP_LOGW(TAG, "show_sd_image: SD non initialisee"); return; } + File f = SD.open(path, FILE_READ); + if (!f) { ESP_LOGW(TAG, "show_sd_image: ouverture echouee %s", path); return; } + const size_t need = (size_t) this->get_width_internal() * this->get_height_internal() / 2; + size_t got = 0; + while (got < need) { + size_t chunk = (need - got > 4096) ? 4096 : (need - got); + int rd = f.read(this->framebuffer_ + got, chunk); + if (rd <= 0) break; + got += rd; + if ((got & 0x3FFF) == 0) App.feed_wdt(); + } + f.close(); + ESP_LOGI(TAG, "show_sd_image %s: %u/%u octets lus", path, (unsigned) got, (unsigned) need); + if (got < need) memset(this->framebuffer_ + got, 0xFF, need - got); + + // Pousse le framebuffer en GC16 (photo = 16 gris, chemin 4bpp). + this->wake_panel_(); + const uint16_t w = this->get_width_internal(); + const uint16_t h = this->get_height_internal(); + const uint16_t width_in_words = (w + 3) / 4; + this->wait_for_display_ready_(); + this->lcd_write_cmd_code_(USDEF_I80_CMD_TEMP); + this->lcd_write_data_(0x0001); + this->lcd_write_data_(static_cast(this->temperature_)); + this->it8951_write_reg_(UP1SR + 2, this->it8951_read_reg_(UP1SR + 2) & ~(1 << 2)); + this->set_img_buf_base_addr_(this->img_buf_addr_); + this->it8951_load_img_area_start_(IT8951_LDIMG_L_ENDIAN, IT8951_4BPP, 0, 0, 0, w, h); + this->lcd_write_framebuffer_4bpp_(reinterpret_cast(this->framebuffer_), width_in_words, h); + this->lcd_write_cmd_code_(IT8951_TCON_LD_IMG_END); + this->it8951_display_area_(0, 0, w, h, 0); // INIT (purge ghost) + this->wait_for_display_ready_(); + this->it8951_display_area_(0, 0, w, h, 2); // GC16 (16 gris) + this->wait_for_display_ready_(); + this->sleep_panel_(); + this->partials_since_full_ = 0; +} + } // namespace it8951_reterminal_e1003 } // namespace esphome diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index 5aaf6dd..62f3841 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -1,6 +1,7 @@ #pragma once #include "SPI.h" +#include "SD.h" #include "driver/gpio.h" #include "esphome/components/display/display_buffer.h" #include "esphome/core/component.h" @@ -18,6 +19,8 @@ static const gpio_num_t IT8951_PIN_BUSY = GPIO_NUM_13; static const gpio_num_t IT8951_PIN_EN = GPIO_NUM_11; static const gpio_num_t IT8951_PIN_RST = GPIO_NUM_12; static const gpio_num_t IT8951_PIN_ITE_EN = GPIO_NUM_21; +static const gpio_num_t IT8951_PIN_SD_CS = GPIO_NUM_14; // SD CS (bus SPI partage) +static const gpio_num_t IT8951_PIN_SD_EN = GPIO_NUM_39; // SD power enable (U13) #define IT8951_TCON_SYS_RUN 0x0001 #define IT8951_TCON_STANDBY 0x0002 @@ -77,6 +80,9 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { void flush_zone(int x, int y, int w, int h, int mode = 1); // Convenience: render_framebuffer() + flush_zone() for a single isolated zone. void refresh_zone(int x, int y, int w, int h, int mode = 1); + // Lit un .raw (1872x1404 4bpp, format framebuffer) depuis la SD et l'affiche + // en GC16 (16 gris). Chemin racine SD ex: "/1.raw". + void show_sd_image(const char *path); float get_setup_priority() const override { return setup_priority::HARDWARE; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; } @@ -138,6 +144,7 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { bool it8951_sleeping_{false}; int8_t temperature_{23}; uint32_t spi_read_frequency_{1000000}; + bool sd_ok_{false}; uint32_t partials_since_full_{0}; static const uint32_t MAX_PARTIALS_BEFORE_FULL = 180; }; From 6d4bf24d195f2694635665c0fdf83d9f69ff464f Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Tue, 16 Jun 2026 21:46:45 +0200 Subject: [PATCH 5/8] SD: passer de SD.h (VFS, rmdir non lie) a SdFat SHARED_SPI (autonome, bus partage) --- .../it8951_reterminal_e1003/it8951_reterminal_e1003.cpp | 4 ++-- .../it8951_reterminal_e1003/it8951_reterminal_e1003.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index bcb1760..c2ea4df 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -353,7 +353,7 @@ void IT8951ReTerminalE1003Display::setup() { pinMode(IT8951_PIN_SD_EN, OUTPUT); digitalWrite(IT8951_PIN_SD_EN, HIGH); delay(10); - this->sd_ok_ = SD.begin(IT8951_PIN_SD_CS, SPI, 10000000); + this->sd_ok_ = this->sd_.begin(SdSpiConfig(IT8951_PIN_SD_CS, SHARED_SPI, SD_SCK_MHZ(10), &SPI)); ESP_LOGCONFIG(TAG, "SD card: %s", this->sd_ok_ ? "OK" : "absente/echec init"); ESP_LOGCONFIG(TAG, "IT8951 reTerminal E1003 initialization complete"); @@ -757,7 +757,7 @@ void IT8951ReTerminalE1003Display::it8951_display_area_1bpp_(uint16_t x, uint16_ void IT8951ReTerminalE1003Display::show_sd_image(const char *path) { if (this->framebuffer_ == nullptr) { ESP_LOGW(TAG, "show_sd_image: framebuffer absent"); return; } if (!this->sd_ok_) { ESP_LOGW(TAG, "show_sd_image: SD non initialisee"); return; } - File f = SD.open(path, FILE_READ); + File32 f = this->sd_.open(path, O_RDONLY); if (!f) { ESP_LOGW(TAG, "show_sd_image: ouverture echouee %s", path); return; } const size_t need = (size_t) this->get_width_internal() * this->get_height_internal() / 2; size_t got = 0; diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index 62f3841..fd9edfb 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -1,7 +1,7 @@ #pragma once #include "SPI.h" -#include "SD.h" +#include "SdFat.h" #include "driver/gpio.h" #include "esphome/components/display/display_buffer.h" #include "esphome/core/component.h" @@ -144,6 +144,7 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { bool it8951_sleeping_{false}; int8_t temperature_{23}; uint32_t spi_read_frequency_{1000000}; + SdFat sd_; bool sd_ok_{false}; uint32_t partials_since_full_{0}; static const uint32_t MAX_PARTIALS_BEFORE_FULL = 180; From b82d8f60f7d34feda2f681dedf337c7384ea31df Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Tue, 16 Jun 2026 21:48:33 +0200 Subject: [PATCH 6/8] SD: FsFile (SdFat 2.x) au lieu de File32 --- .../it8951_reterminal_e1003/it8951_reterminal_e1003.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index c2ea4df..bc417e2 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -757,7 +757,7 @@ void IT8951ReTerminalE1003Display::it8951_display_area_1bpp_(uint16_t x, uint16_ void IT8951ReTerminalE1003Display::show_sd_image(const char *path) { if (this->framebuffer_ == nullptr) { ESP_LOGW(TAG, "show_sd_image: framebuffer absent"); return; } if (!this->sd_ok_) { ESP_LOGW(TAG, "show_sd_image: SD non initialisee"); return; } - File32 f = this->sd_.open(path, O_RDONLY); + FsFile f = this->sd_.open(path, O_RDONLY); if (!f) { ESP_LOGW(TAG, "show_sd_image: ouverture echouee %s", path); return; } const size_t need = (size_t) this->get_width_internal() * this->get_height_internal() / 2; size_t got = 0; From 0d94b3c4a56ad14d264fee8830023c50e13986a4 Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Sat, 20 Jun 2026 18:15:19 +0200 Subject: [PATCH 7/8] SD: miniatures (thumbnails) generees et mises en cache pour menu galerie Pourquoi: le mode photo n'offrait qu'une navigation lineaire (suivante/precedente). Pour selectionner directement une image il faut une grille de miniatures cliquables. Generer une miniature a chaque affichage serait lent (lecture de tout le .raw plein ecran ~1,3 Mo par vignette); d'ou un cache SD. Approche: trois methodes publiques sur le driver IT8951. - make_thumbnail(src,dst): charge le .raw plein ecran dans le framebuffer (reutilise la boucle de lecture de show_sd_image), sous-echantillonne en nearest d'un facteur THUMB_FACTOR=4 (468x351) puis ecrit un .raw miniature 4bpp row-major LOGIQUE. - draw_sd_thumbnail(path,dx,dy): blit de la miniature dans le framebuffer au coin logique (dx,dy), sans declencher de refresh (a appeler depuis le lambda d'affichage). - sd_file_exists(path): test du cache (genere une seule fois par image). Helpers get_pixel_logical_/set_pixel_nibble_ pour rester en coordonnees logiques (le framebuffer stocke l'image inversee en X comme draw_absolute_pixel_internal), ce qui rend generation et blit symetriques: le mirror s'annule. Alternatives ecartees: generation hors-appareil via outil + recopie SD (impose de regenerer/recopier la carte a chaque ajout de photo); sous-echantillon a la volee sans cache (trop lent, relit toute la source a chaque rendu de menu). Compromis: nearest (pas de moyenne de bloc) -> vignettes un peu crues mais generation rapide; ~82 Ko par miniature sur SD. La generation ecrase le framebuffer (chargement de la source) -> make_thumbnail doit etre appele AVANT un rendu, jamais pendant. Impact: aucune regression sur show_sd_image (boucle de lecture inchangee, reutilisee). Bus SPI SD partage deja gere. Teste sur le modele E1003. --- .../it8951_reterminal_e1003.cpp | 111 ++++++++++++++++++ .../it8951_reterminal_e1003.h | 18 +++ 2 files changed, 129 insertions(+) diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp index bc417e2..ddb8133 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.cpp @@ -693,6 +693,28 @@ uint8_t IT8951ReTerminalE1003Display::get_pixel_nibble_(uint16_t x, uint16_t y) return this->framebuffer_[pos] >> 4; } +// Lecture en coordonnees LOGIQUES: applique le meme mirror X que +// draw_absolute_pixel_internal (qui stocke l'image inversee en X). +uint8_t IT8951ReTerminalE1003Display::get_pixel_logical_(int x, int y) { + return this->get_pixel_nibble_(static_cast(this->get_width() - 1 - x), static_cast(y)); +} + +// Ecriture d'un nibble (0=noir .. 0x0F=blanc) en coordonnees LOGIQUES, meme +// mapping que draw_absolute_pixel_internal mais sans conversion de couleur. +void IT8951ReTerminalE1003Display::set_pixel_nibble_(int x, int y, uint8_t nibble) { + if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height() || this->framebuffer_ == nullptr) { + return; + } + x = this->get_width() - 1 - x; + const uint32_t pos = (x + y * this->get_width()) / 2; + nibble &= 0x0F; + if ((x % 2) == 0) { + this->framebuffer_[pos] = (this->framebuffer_[pos] & 0xF0) | nibble; + } else { + this->framebuffer_[pos] = (this->framebuffer_[pos] & 0x0F) | (nibble << 4); + } +} + void IT8951ReTerminalE1003Display::lcd_send_cmd_arg_(uint16_t cmd, uint16_t *args, uint16_t num_args) { this->lcd_write_cmd_code_(cmd); for (uint16_t i = 0; i < num_args; i++) { @@ -794,5 +816,94 @@ void IT8951ReTerminalE1003Display::show_sd_image(const char *path) { this->partials_since_full_ = 0; } +bool IT8951ReTerminalE1003Display::sd_file_exists(const char *path) { + if (!this->sd_ok_) { + return false; + } + return this->sd_.exists(path); +} + +void IT8951ReTerminalE1003Display::make_thumbnail(const char *src_path, const char *dst_path) { + if (this->framebuffer_ == nullptr) { + ESP_LOGW(TAG, "make_thumbnail: framebuffer absent"); + return; + } + if (!this->sd_ok_) { + ESP_LOGW(TAG, "make_thumbnail: SD non initialisee"); + return; + } + // 1) Charger la source plein ecran dans le framebuffer (meme boucle que show_sd_image). + FsFile f = this->sd_.open(src_path, O_RDONLY); + if (!f) { + ESP_LOGW(TAG, "make_thumbnail: ouverture source echouee %s", src_path); + return; + } + const size_t need = (size_t) this->get_width_internal() * this->get_height_internal() / 2; + size_t got = 0; + while (got < need) { + size_t chunk = (need - got > 4096) ? 4096 : (need - got); + int rd = f.read(this->framebuffer_ + got, chunk); + if (rd <= 0) + break; + got += rd; + if ((got & 0x3FFF) == 0) + App.feed_wdt(); + } + f.close(); + if (got < need) { + memset(this->framebuffer_ + got, 0xFF, need - got); + } + + // 2) Sous-echantillonner (nearest) en lisant des pixels LOGIQUES, empaqueter + // 2 pixels/octet en row-major LOGIQUE -> meme convention que draw_sd_thumbnail. + FsFile o = this->sd_.open(dst_path, O_WRONLY | O_CREAT | O_TRUNC); + if (!o) { + ESP_LOGW(TAG, "make_thumbnail: creation dst echouee %s", dst_path); + return; + } + const int row_bytes = THUMB_W / 2; // 234 + uint8_t row[THUMB_W / 2]; + for (int ty = 0; ty < THUMB_H; ty++) { + const int sy = ty * THUMB_FACTOR; + for (int i = 0; i < row_bytes; i++) { + const int tx = i * 2; + const uint8_t lo = this->get_pixel_logical_(tx * THUMB_FACTOR, sy); + const uint8_t hi = this->get_pixel_logical_((tx + 1) * THUMB_FACTOR, sy); + row[i] = (lo & 0x0F) | (hi << 4); + } + o.write(row, row_bytes); + if ((ty & 0x1F) == 0) + App.feed_wdt(); + } + o.close(); + ESP_LOGI(TAG, "make_thumbnail %s -> %s (%dx%d)", src_path, dst_path, THUMB_W, THUMB_H); +} + +void IT8951ReTerminalE1003Display::draw_sd_thumbnail(const char *path, int dx, int dy) { + if (this->framebuffer_ == nullptr || !this->sd_ok_) { + return; + } + FsFile f = this->sd_.open(path, O_RDONLY); + if (!f) { + ESP_LOGW(TAG, "draw_sd_thumbnail: ouverture echouee %s", path); + return; + } + const int row_bytes = THUMB_W / 2; // 234 + uint8_t row[THUMB_W / 2]; + for (int ty = 0; ty < THUMB_H; ty++) { + int rd = f.read(row, row_bytes); + if (rd < row_bytes) + break; + for (int i = 0; i < row_bytes; i++) { + const int tx = i * 2; + this->set_pixel_nibble_(dx + tx, dy + ty, row[i] & 0x0F); + this->set_pixel_nibble_(dx + tx + 1, dy + ty, row[i] >> 4); + } + if ((ty & 0x1F) == 0) + App.feed_wdt(); + } + f.close(); +} + } // namespace it8951_reterminal_e1003 } // namespace esphome diff --git a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h index fd9edfb..8a76ffe 100644 --- a/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h +++ b/Seeed-reTerminal-E1003/components/it8951_reterminal_e1003/it8951_reterminal_e1003.h @@ -83,6 +83,17 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { // Lit un .raw (1872x1404 4bpp, format framebuffer) depuis la SD et l'affiche // en GC16 (16 gris). Chemin racine SD ex: "/1.raw". void show_sd_image(const char *path); + // Vrai si le fichier existe sur la SD (test de presence du cache miniature). + bool sd_file_exists(const char *path); + // Genere une miniature (sous-echantillon nearest x THUMB_FACTOR) a partir d'un + // .raw plein ecran et l'ecrit sur la SD (format 4bpp row-major LOGIQUE, + // THUMB_W x THUMB_H). ECRASE le framebuffer (chargement de la source) -> a + // appeler AVANT de dessiner un ecran, jamais pendant un rendu. + void make_thumbnail(const char *src_path, const char *dst_path); + // Dessine une miniature SD dans le framebuffer au coin logique (dx,dy). Ne + // declenche AUCUN refresh: ecrit seulement les pixels (a appeler depuis le + // lambda d'affichage, avant le push GC16). + void draw_sd_thumbnail(const char *path, int dx, int dy); float get_setup_priority() const override { return setup_priority::HARDWARE; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_GRAYSCALE; } @@ -114,6 +125,9 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { bool framebuffer_is_binary_(); void draw_driver_test_pattern_(); uint8_t get_pixel_nibble_(uint16_t x, uint16_t y); + // Lecture/ecriture en coordonnees LOGIQUES (mirror X comme draw_absolute_pixel_internal). + uint8_t get_pixel_logical_(int x, int y); + void set_pixel_nibble_(int x, int y, uint8_t nibble); void lcd_send_cmd_arg_(uint16_t cmd, uint16_t *args, uint16_t num_args); uint16_t it8951_read_reg_(uint16_t addr); @@ -148,6 +162,10 @@ class IT8951ReTerminalE1003Display : public display::DisplayBuffer { bool sd_ok_{false}; uint32_t partials_since_full_{0}; static const uint32_t MAX_PARTIALS_BEFORE_FULL = 180; + // Miniatures: sous-echantillon entier de l'image plein ecran (1872x1404). + static const int THUMB_FACTOR = 4; + static const int THUMB_W = 1872 / THUMB_FACTOR; // 468 + static const int THUMB_H = 1404 / THUMB_FACTOR; // 351 }; } // namespace it8951_reterminal_e1003 From 31ddd2f1eacdc1ca5f9076b71f1f5565e7169e26 Mon Sep 17 00:00:00 2001 From: pierrehenrymuller Date: Thu, 25 Jun 2026 21:11:52 +0200 Subject: [PATCH 8/8] doc(E1003): document live/partial refresh, SD images and thumbnail API The driver added several public methods on top of the upstream art-frame flow (render_framebuffer/flush_zone/refresh_zone for flash-free partial updates, show_sd_image for network-free rendering from the SD card, and make_thumbnail/draw_sd_thumbnail/sd_file_exists for a cached gallery). None of these were documented, so adopters had to read the header to discover them. Add a reference section per feature plus an 'Interactive examples' section with copy-pasteable YAML: clock zone refresh, a touch button calling a HA action, a paginated thumbnail grid, and touch photo navigation. Documentation only, no code change; keeps the PR series self-explanatory for upstream review. --- Seeed-reTerminal-E1003/README.md | 207 +++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/Seeed-reTerminal-E1003/README.md b/Seeed-reTerminal-E1003/README.md index 9ccb151..f4ab0fb 100644 --- a/Seeed-reTerminal-E1003/README.md +++ b/Seeed-reTerminal-E1003/README.md @@ -28,6 +28,213 @@ ESPHome currently does not provide support for the IT8951 controller used in the Some great investigative work by [ar0v3r](https://github.com/ar0v3r/reTerminal-E1003-ESPHome/) revealed that the SD-card and touch controllers were still drawing power during deep sleep. I've implemented their fixes in v0.8.0 of this configuration and have cut power consumption during deep sleep by half! Many thanks to u/ar0v3r. +## Live and partial refresh (interactive mode) + +The original art-frame flow does one full-screen GC16 refresh per wake and then sleeps. For an always-on dashboard you usually want to update a small region (a clock, a value) without the full-screen flash and without re-pushing all 2.6 M pixels. The driver exposes a render/push split: + +- `render_framebuffer()` runs the ESPHome display lambda once into the PSRAM framebuffer. It does not touch the panel. +- `flush_zone(x, y, w, h, mode = 1)` pushes only the given logical rectangle to the panel. The default mode 1 (DU) is a fast binary waveform with no flash. It does not re-render, so call `render_framebuffer()` first when the content changed. +- `refresh_zone(x, y, w, h, mode = 1)` is the convenience combination of the two for a single isolated zone. +- `full_refresh()` does a full-screen INIT + GC16 pass (the flashy, ghost-free one). Use it at boot and for a periodic deghost. + +Example: refresh the whole screen every minute with a single DU pass (no flash), and do a clean full refresh once a day. + +```yaml +script: + - id: live_refresh + then: + - lambda: |- + id(epaper_display).render_framebuffer(); + id(epaper_display).flush_zone(0, 0, 1872, 1404, 1); + - id: deghost + then: + - lambda: 'id(epaper_display).full_refresh();' + +time: + - platform: homeassistant + on_time: + - seconds: 0 + then: { script.execute: live_refresh } + - hours: 5 + minutes: 0 + seconds: 0 + then: { script.execute: deghost } +``` + +Notes: +- DU (mode 1) accumulates ghosting over many partial updates. Schedule a periodic `full_refresh()` (nightly, for example) to clear it. +- `render_framebuffer()` is cheap on CPU but walks the whole PSRAM buffer, so it takes on the order of a second. Render once, then issue several `flush_zone()` calls when you update multiple small regions in the same pass. + +## Displaying images straight from the SD card + +The board has a microSD slot on the same SPI bus as the IT8951 (CS on GPIO14). The driver embeds SdFat in SHARED_SPI mode so it cooperates with the display CS line, and can render a full-screen image directly from the card with no Home Assistant download. + +Images on the card are raw framebuffer files: 1872x1404, 4 bits per pixel, the exact in-memory layout the panel uses. Name them `/1.raw`, `/2.raw`, and so on at the card root. + +```yaml +button: + - platform: template + name: "Show SD image 1" + on_press: + - lambda: 'id(epaper_display).show_sd_image("/1.raw");' +``` + +`show_sd_image()` loads the file into the framebuffer and pushes it in GC16 (16-level grayscale). This is how a local photo frame can keep working with no network once the card is written. To produce a `.raw`, convert a 1872x1404 16-level grayscale image and pack two pixels per byte (high nibble first), matching the panel framebuffer. + +## Thumbnail gallery + +For a picture-menu UI the driver can pre-compute and cache thumbnails on the SD card, then draw them into the framebuffer without triggering a refresh: + +- `make_thumbnail(src, dst)` downsamples a full-screen `.raw` by `THUMB_FACTOR` (4 gives 468x351) and writes the thumbnail to the card. It overwrites the framebuffer while loading the source, so call it before you draw a screen, never during a render. +- `sd_file_exists(path)` lets you build the cache lazily (generate only the missing thumbnails). +- `draw_sd_thumbnail(path, dx, dy)` blits a cached thumbnail into the framebuffer at a logical corner. It only writes pixels, so call it from the display lambda before the GC16 push. + +Example: generate the missing thumbnails for the current page once, then render a 3x3 grid from the lambda. + +```yaml +script: + - id: build_thumbs + then: + - lambda: |- + for (int n = 1; n <= 9; n++) { + std::string src = "/" + std::to_string(n) + ".raw"; + std::string thumb = "/" + std::to_string(n) + "_thumb.raw"; + if (!id(epaper_display).sd_file_exists(thumb.c_str())) + id(epaper_display).make_thumbnail(src.c_str(), thumb.c_str()); + } + - component.update: epaper_display + +# inside the display lambda, draw a cached thumbnail at a logical corner: +# id(epaper_display).draw_sd_thumbnail("/1_thumb.raw", 78, 111); +``` + +## Interactive examples (touch dashboard) + +These snippets show the display side of a live touch dashboard built on the calls above. They assume a landscape panel (`rotation: 0`, 1872x1404) and the GT911 touch controller enabled. + +### Refresh just the clock every minute + +`refresh_zone()` re-renders the framebuffer and pushes only the given rectangle with a fast DU waveform, so the rest of the screen is untouched and there is no flash. + +```yaml +script: + - id: clock_tick + then: + # x, y, w, h of the clock box, DU (mode 1) + - lambda: 'id(epaper_display).refresh_zone(1512, 0, 360, 130, 1);' + +time: + - platform: homeassistant + on_time: + - seconds: 0 + then: { script.execute: clock_tick } +``` + +### A clickable button + +Draw a labelled hit area in the display lambda, then test the touch coordinates in `on_touch` and call a Home Assistant action. + +```yaml +touchscreen: + - platform: gt911 + id: touch + display: epaper_display + reset_pin: GPIO48 + on_touch: + - lambda: |- + // bottom-right corner toggles the gate + if (touch.x >= 1512 && touch.y >= 800 && touch.y <= 1080) + id(toggle_gate).execute(); + +script: + - id: toggle_gate + then: + - homeassistant.action: + action: cover.toggle + data: + entity_id: cover.portail + +display: + - platform: it8951_reterminal_e1003 + id: epaper_display + pages: + - id: main + lambda: |- + // The driver is inverted: COLOR_OFF is black, COLOR_ON is white. + // Draw the button label under the touch zone. + it.printf(1692, 880, id(font_icon), COLOR_OFF, TextAlign::TOP_CENTER, "\U000F0299"); // gate + it.printf(1692, 1020, id(font_label), COLOR_OFF, TextAlign::TOP_CENTER, "Gate"); +``` + +### Thumbnail grid with pagination + +Lazily build the cache for the current page, then draw a 3x3 grid from the page lambda. Changing `menu_page` and re-running the script paginates. + +```yaml +globals: + - id: menu_page + type: int + initial_value: "0" + +script: + - id: show_menu + then: + - lambda: |- + int base = id(menu_page) * 9; + for (int i = 0; i < 9; i++) { + int n = base + i + 1; + std::string src = "/" + std::to_string(n) + ".raw"; + std::string thumb = "/" + std::to_string(n) + "_thumb.raw"; + if (!id(epaper_display).sd_file_exists(thumb.c_str())) + id(epaper_display).make_thumbnail(src.c_str(), thumb.c_str()); + } + - component.update: epaper_display + +display: + - platform: it8951_reterminal_e1003 + id: epaper_display + pages: + - id: gallery + lambda: |- + // 3x3 grid of cached thumbnails (cell 624x413, thumb 468x351) + for (int r = 0; r < 3; r++) + for (int c = 0; c < 3; c++) { + int n = id(menu_page) * 9 + r * 3 + c + 1; + std::string t = "/" + std::to_string(n) + "_thumb.raw"; + id(epaper_display).draw_sd_thumbnail(t.c_str(), c * 624 + 78, 80 + r * 413); + } +``` + +### Touch navigation between photos + +Tap the right third for the next photo, the left third for the previous one. `show_sd_image()` both loads the frame from the card and pushes it in GC16. + +```yaml +substitutions: + photo_count: "21" + +globals: + - id: photo + type: int + initial_value: "1" + +touchscreen: + - platform: gt911 + display: epaper_display + reset_pin: GPIO48 + on_touch: + - lambda: |- + const int W = 1872, COUNT = ${photo_count}; + if (touch.x > (W * 2) / 3) + id(photo) = (id(photo) >= COUNT) ? 1 : id(photo) + 1; + else if (touch.x < W / 3) + id(photo) = (id(photo) <= 1) ? COUNT : id(photo) - 1; + else + return; + std::string p = "/" + std::to_string(id(photo)) + ".raw"; + id(epaper_display).show_sd_image(p.c_str()); +``` + ## AI warning While this is based on an original configuration for the Seeed reTerminal E1002 that I wrote manually, this version for the E1003 was made with heavy assistance from Claude Code. If you are against using AI-generated code please do not install this.