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. 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..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 @@ -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_ = 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"); } @@ -433,12 +440,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 +450,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 +474,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 +494,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 +518,122 @@ 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::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; + } + 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; + } + + this->wake_panel_(); + 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_(); - ESP_LOGV(TAG, "Display update triggered"); + 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() { @@ -528,6 +655,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; @@ -556,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++) { @@ -617,5 +776,134 @@ 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; } + 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; + 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; +} + +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 84cb011..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 @@ -1,6 +1,7 @@ #pragma once #include "SPI.h" +#include "SdFat.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 @@ -61,6 +64,36 @@ 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. + void full_refresh(); + // 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); + // 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; } @@ -77,6 +110,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_(); @@ -89,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); @@ -119,6 +158,14 @@ 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; + // 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