Skip to content

[epaper_spi] spectra e6 13in#16946

Open
raccettura wants to merge 12 commits into
esphome:devfrom
raccettura:epaper_spectra_e6_13in
Open

[epaper_spi] spectra e6 13in#16946
raccettura wants to merge 12 commits into
esphome:devfrom
raccettura:epaper_spectra_e6_13in

Conversation

@raccettura

@raccettura raccettura commented Jun 14, 2026

Copy link
Copy Markdown

What does this implement/fix?

Add support for Spectra E6 13in GDEP133C02

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected) — policy
  • Developer breaking change (an API change that could break external components) — policy
  • Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — policy
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

  • fixes

Pull request in esphome.io with documentation (if applicable):

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040/RP2350
  • BK72xx
  • RTL87xx
  • LN882x
  • nRF52840

Example entry for config.yaml:

esphome:
  name: epaper-13in
  friendly_name: "13.3 inch e-Paper"

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: esp-idf

# 960KB framebuffer requires PSRAM — board is ESP32-S3-WROOM-1-N16R8 (octal PSRAM)
psram:
  mode: octal
  speed: 80MHz

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:
ota:
  - platform: esphome

logger:
  hardware_uart: UART0

# SPI bus — fixed routing on the integrated GDEP133C02 driver board (from manufacturer pindefine.h)
spi:
  clk_pin: GPIO9      # SPI_CLK
  mosi_pin: GPIO41    # SPI_Data0

display:
  - platform: epaper_spi
    model: 13.3in-Spectra-E6
    id: epaper_display
    cs_pin: GPIO18         # SPI_CS0 — master IC
    cs_slave_pin: GPIO17   # SPI_CS1 — slave IC
    reset_pin: GPIO6       # EPD_RST
    busy_pin: GPIO7        # EPD_BUSY (inverted:true is applied automatically)
    enable_pin: GPIO45     # LOAD_SW — panel power enable (driven HIGH at setup)
    reset_duration: 20ms
    update_interval: 60s
    # NOTE: this panel has no D/C line — command/data is framed by CS, so dc_pin is omitted.
    lambda: |-
      const int w = it.get_width();    // 1200
      const int h = it.get_height();   // 1600
      const int mid = w / 2;           // 600 — the master/slave IC boundary

      // White background so the panel starts clean.
      it.fill(Color::WHITE);

      // --- Six color bars spanning the FULL width (crosses both ICs) ---------
      // If the IC split or pixel ordering is wrong, these bars will tear or
      // mismatch at the x=600 seam.
      const int bands = 6;
      const int band_h = h / bands;
      Color bar_colors[bands] = {
        Color(0, 0, 0),       // BLACK
        Color(255, 0, 0),     // RED
        Color(255, 255, 0),   // YELLOW
        Color(0, 0, 255),     // BLUE
        Color(0, 255, 0),     // GREEN
        Color(255, 255, 255), // WHITE (bordered below so it's visible)
      };
      for (int i = 0; i < bands; i++) {
        it.filled_rectangle(0, i * band_h, w, band_h, bar_colors[i]);
      }

      // --- Outer border to confirm edges/orientation are not cropped ---------
      it.rectangle(0, 0, w, h, Color(0, 0, 0));
      it.rectangle(1, 1, w - 2, h - 2, Color(0, 0, 0));

      // --- Vertical seam marker at the IC boundary (x=600) -------------------
      // Should run perfectly straight down the physical center if both halves
      // are aligned.
      it.filled_rectangle(mid - 2, 0, 4, h, Color(0, 255, 0));

      // --- Corner labels to verify orientation (which corner is which) -------
      it.print(20, 20, id(font_med), Color(255, 255, 255),
               TextAlign::TOP_LEFT, "TOP-LEFT (0,0)");
      it.print(w - 20, 20, id(font_med), Color(255, 255, 255),
               TextAlign::TOP_RIGHT, "TOP-RIGHT");
      it.print(20, h - 20, id(font_med), Color(0, 0, 0),
               TextAlign::BOTTOM_LEFT, "BOTTOM-LEFT");
      it.print(w - 20, h - 20, id(font_med), Color(0, 0, 0),
               TextAlign::BOTTOM_RIGHT, "BOTTOM-RIGHT");

      // --- Half-identifying labels straddling the seam ----------------------
      it.print(mid / 2, h / 2, id(font_large), Color(255, 255, 255),
               TextAlign::CENTER, "MASTER / LEFT");
      it.print(mid + mid / 2, h / 2, id(font_large), Color(0, 0, 0),
               TextAlign::CENTER, "SLAVE / RIGHT");

      // --- Center title -----------------------------------------------------
      it.print(mid, 60, id(font_large), Color(0, 0, 0),
               TextAlign::TOP_CENTER, "GDEP133C02 6-color test");

font:
  - file: "gfonts://Roboto"
    id: font_large
    size: 72
  - file: "gfonts://Roboto"
    id: font_med
    size: 40
esphome:
  name: epaper-13in-url
  friendly_name: "13.3 inch e-Paper (URL image)"

# Change this to the PNG you want to display.
substitutions:
  image_url: "https://yourdomain.com/path/to/image.png"

esp32:
  board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: esp-idf
    sdkconfig_options:
      # Drawing 1200×1600 pixels into the 4bpp framebuffer takes ~10s; the default
      # 5s WDT would trigger. The SPI transfer itself takes another ~14s but that
      # yields between chunks so the WDT resets there automatically.
      CONFIG_ESP_TASK_WDT_TIMEOUT_S: "30"

# 960KB framebuffer + decoded image both live in PSRAM — board is ESP32-S3-WROOM-1-N16R8
psram:
  mode: octal
  speed: 80MHz

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:
ota:
  - platform: esphome

logger:
  hardware_uart: UART0

# Tracks whether the image should be drawn. "Clear" sets this false (blank white);
# "Refresh"/URL update set it true.
globals:
  - id: show_image
    type: bool
    restore_value: false
    initial_value: "true"

# Needed by online_image to fetch the BMP over HTTP(S).
http_request:
  timeout: 30s
  verify_ssl: false   # set true (and provide certs) for HTTPS with verification

# SPI bus — fixed routing on the integrated GDEP133C02 driver board (manufacturer pindefine.h)
spi:
  clk_pin: GPIO9      # SPI_CLK
  mosi_pin: GPIO41    # SPI_Data0

# Downloads the PNG and decodes it into a 6-color-friendly RGB565 buffer.
online_image:
  - url: ${image_url}
    id: web_image
    format: PNG
    type: RGB565
    resize: 1200x1600       # scale source to the panel resolution
    update_interval: 5min   # re-download every 5 minutes (downloads once on boot too)
    on_download_finished:
      - logger.log: "Image downloaded — refreshing display"
      - component.update: epaper_display
    on_error:
      - logger.log: "Image download failed"

display:
  - platform: epaper_spi
    model: 13.3in-Spectra-E6
    id: epaper_display
    cs_pin: GPIO18         # SPI_CS0 — master IC
    cs_slave_pin: GPIO17   # SPI_CS1 — slave IC
    reset_pin: GPIO6       # EPD_RST
    busy_pin: GPIO7        # EPD_BUSY (inverted:true is applied automatically)
    enable_pin: GPIO45     # LOAD_SW — panel power enable (driven HIGH at setup)
    reset_duration: 20ms
    # Only refresh when a new image arrives (triggered by on_download_finished above).
    update_interval: never
    # NOTE: this panel has no D/C line — command/data is framed by CS, so dc_pin is omitted.
    lambda: |-
      it.fill(Color::WHITE);
      if (id(show_image)) {
        it.image(0, 0, id(web_image));
      }

# Panel rotation, exposed to Home Assistant. Applied at runtime, then the display redraws.
select:
  - platform: template
    name: "Display Rotation"
    id: display_rotation
    optimistic: true
    restore_value: true
    options:
      - ""
      - "90°"
      - "180°"
      - "270°"
    initial_option: ""
    on_value:
      - lambda: |-
          int deg = 0;
          if (x == "90°") {
            deg = 90;
          } else if (x == "180°") {
            deg = 180;
          } else if (x == "270°") {
            deg = 270;
          }
          id(epaper_display).set_rotation(static_cast<esphome::display::DisplayRotation>(deg));
      - component.update: epaper_display

# Editable image URL, exposed to Home Assistant as a text entity. The "Update Image URL"
# button below applies whatever is typed here.
text:
  - platform: template
    name: "Image URL"
    id: image_url_input
    mode: text
    optimistic: true
    restore_value: true
    initial_value: ${image_url}

# Buttons exposed to Home Assistant.
button:
  # Blank the panel to white without touching the downloaded image.
  - platform: template
    name: "Clear Display"
    on_press:
      - globals.set:
          id: show_image
          value: "false"
      - component.update: epaper_display

  # Re-download the current URL and redraw (also un-clears the display).
  - platform: template
    name: "Refresh Image"
    on_press:
      - globals.set:
          id: show_image
          value: "true"
      - component.update: web_image

  # Apply the URL from the "Image URL" text entity, then download + display it.
  - platform: template
    name: "Update Image URL"
    on_press:
      - globals.set:
          id: show_image
          value: "true"
      - online_image.set_url:
          id: web_image
          url: !lambda "return id(image_url_input).state;"

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

Copilot AI review requested due to automatic review settings June 14, 2026 05:02
@raccettura raccettura requested a review from a team as a code owner June 14, 2026 05:02
@esphome

esphome Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://pr#16946
    components: [epaper_spi]
    refresh: 1h

(Added by the PR bot)

@esphome

esphome Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

👋 Hi there! This PR modifies 6 file(s) with codeowners.

@esphome/core - As codeowner(s) of the affected files, your review would be appreciated! 🙏

Note: Automatic review request may have failed, but you're still welcome to review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for a 13.3" Spectra E6 dual-chip-select (master+slave CS) ePaper panel in epaper_spi, including a new model definition and a new C++ driver that can operate without a D/C line.

Changes:

  • Introduces a new 13.3in-Spectra-E6 model with dual-CS support and inverted BUSY default.
  • Adds a new dual-CS C++ implementation (EPaperSpectraE6DualCS) and updates the base epaper SPI code to support optional D/C and a power-enable pin.
  • Extends YAML test configuration to compile the new panel model.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/components/epaper_spi/test.esp32-s3-idf.yaml Adds a compile-time YAML config for the new dual-CS 13.3" Spectra E6 panel.
esphome/components/epaper_spi/models/spectra_e6_13in.py Defines the new 13.3" Spectra E6 dual-CS model and its config behaviors (optional D/C, inverted BUSY).
esphome/components/epaper_spi/models/init.py Adds a model hook to customize BUSY pin schema per model.
esphome/components/epaper_spi/epaper_spi_spectra_e6_dual_cs.h Declares the new dual-CS driver class.
esphome/components/epaper_spi/epaper_spi_spectra_e6_dual_cs.cpp Implements init, dual-CS command/data helpers, and split-buffer transfer logic.
esphome/components/epaper_spi/epaper_spi_spectra_e6.h Removes final so the new dual-CS driver can inherit from EPaperSpectraE6.
esphome/components/epaper_spi/epaper_spi.h Adds enable-pin support and makes D/C operations safe no-ops when no D/C pin exists.
esphome/components/epaper_spi/epaper_spi.cpp Powers the panel via enable-pin early in setup and routes D/C toggles through null-safe helpers.
esphome/components/epaper_spi/display.py Adds cs_slave_pin, makes D/C optional in codegen, and adds codegen for enable pin + slave CS.

Comment on lines +203 to +208
if enable_pins := config.get(CONF_ENABLE_PIN):
# Power-enable pin(s) (manufacturer "LOAD_SW"/"PWR"): driven high at setup so the
# panel's source-driver power comes on before reset.
for enable_pin in enable_pins:
enable = await cg.gpio_pin_expression(enable_pin)
cg.add(var.set_enable_pin(enable))
Comment on lines +130 to +149
if (this->current_data_index_ == 0) {
ESP_LOGI(TAG, "DTM transfer to master IC (left half, %u bytes)", HALF_FRAME_BYTES);
this->dc_command_();
this->enable(); // assert master CS — held for entire transfer
this->write_byte(CMD_DTM);
this->dc_data_();
}
size_t buf_idx = 0;
while (this->current_data_index_ != HALF_FRAME_BYTES) {
uint32_t idx = this->current_data_index_++;
bytes_to_send[buf_idx++] = this->buffer_[idx / COLS_PER_IC * (COLS_PER_IC * 2) + idx % COLS_PER_IC];

if (buf_idx == sizeof bytes_to_send) {
this->write_array(bytes_to_send, buf_idx); // master CS remains low
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) {
return false; // yield; CS stays asserted across loop iterations
}
}
}
Comment on lines +41 to +46
// Switch on panel power first (matches the manufacturer driving LOAD_SW/PWR high before
// anything else). Must happen before reset so the controller can power up.
if (this->enable_pin_ != nullptr) {
this->enable_pin_->setup(); // OUTPUT
this->enable_pin_->digital_write(true);
}
Comment on lines +36 to +47
void EPaperSpectraE6DualCS::setup() {
EPaperBase::setup();
if (this->cs_slave_ != nullptr) {
this->cs_slave_->setup();
this->cs_slave_->digital_write(true);
}
}

void EPaperSpectraE6DualCS::dump_config() {
EPaperBase::dump_config();
LOG_PIN(" Slave CS Pin: ", this->cs_slave_);
}
Comment on lines +175 to +177
dc_pin:
allow_other_uses: true
number: GPIO17
@raccettura raccettura changed the title epaper spectra e6 13in [epaper_spi] spectra e6 13in Jun 16, 2026
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.29%. Comparing base (b09a5f9) to head (cab969b).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##              dev   #16946   +/-   ##
=======================================
  Coverage   79.29%   79.29%           
=======================================
  Files          72       72           
  Lines       15079    15079           
  Branches     2209     2209           
=======================================
  Hits        11957    11957           
  Misses       2671     2671           
  Partials      451      451           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

Memory Impact Analysis

Components: epaper_spi
Platform: esp32-s3-idf

Metric Target Branch This PR Change
RAM 27,020 bytes 27,284 bytes 📈 🔸 +264 bytes (+0.98%)
Flash 268,515 bytes 270,587 bytes 📈 🔸 +2,072 bytes (+0.77%)
📊 Component Memory Breakdown
Component Target Flash PR Flash Change
[esphome]epaper_spi 7,738 bytes 8,938 bytes 📈 🚨 +1,200 bytes (+15.51%)
[esphome]core 9,344 bytes 9,555 bytes 📈 🔸 +211 bytes (+2.26%)
[esphome]display 2,137 bytes 2,231 bytes 📈 🚨 +94 bytes (+4.40%)
[esphome]spi 3,760 bytes 3,828 bytes 📈 🔸 +68 bytes (+1.81%)
🔍 Symbol-Level Changes (click to expand)

Changed Symbols

Symbol Target Size PR Size Change
setup() 2,155 bytes 2,366 bytes 📈 +211 bytes (+9.79%)
esphome::epaper_spi::EPaperBase::dump_config() 178 bytes 202 bytes 📈 +24 bytes (+13.48%)
esphome::epaper_spi::EPaperBase::cmd_data(unsigned char, unsigned char const*, unsigned int) 124 bytes 112 bytes 📉 -12 bytes (-9.68%)
esphome::epaper_spi::EPaperSpectraE6::draw_pixel_at(int, int, esphome::Color) 101 bytes 109 bytes 📈 +8 bytes (+7.92%)
esphome::epaper_spi::EPaperBase::start_data_() 23 bytes 16 bytes 📉 -7 bytes (-30.43%)
esphome::App 180 bytes 184 bytes 📈 +4 bytes (+2.22%)
esphome::epaper_spi::EPaperBase::process_state_() 383 bytes 379 bytes 📉 -4 bytes (-1.04%)
esphome::epaper_spi::EPaperBase::command(unsigned char) 68 bytes 64 bytes 📉 -4 bytes (-5.88%)
esphome::epaper_spi::EPaperSpectraE6::clear() 31 bytes 35 bytes 📈 +4 bytes (+12.90%)
esphome::epaper_spi::EPaperBase::send_init_sequence_(unsigned char const*, unsigned int) 130 bytes 131 bytes 📈 +1 bytes (+0.77%)

New Symbols (top 15)

Symbol Size
esphome::epaper_spi::EPaperSpectraE6DualCS::transfer_data() 431 bytes
epaper_spi__epaper_spi_epaperspectrae6dualcs_id__pstorage 224 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::initialise(bool) 220 bytes
vtable for esphome::epaper_spi::EPaperSpectraE6DualCS 140 bytes
setup()::{lambda(esphome::display::Display&)#5}::_FUN(esphome::display::Display&) 94 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::both_cmd_data_(unsigned char, unsigned char const*, u...esphome::epaper_spi::EPaperSpectraE6DualCS::both_cmd_data_(unsigned char, unsigned char const*, unsigned int)
80 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::deep_sleep() 60 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::setup() 34 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::power_off() 34 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::refresh_screen(bool) 34 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::power_on() 33 bytes
esphome::spi::SPIDevice<(esphome::spi::SPIBitOrder)1, (esphome::spi::SPIClockPolarity)0, (esphome...esphome::spi::SPIDevice<(esphome::spi::SPIBitOrder)1, (esphome::spi::SPIClockPolarity)0, (esphome::spi::SPIClockPhase)0, (esphome::spi::SPIDataRate)2000000>::write_byte(unsigned char) [$isra$0]
22 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::dump_config() 22 bytes
esphome::epaper_spi::EPaperSpectraE6DualCS::both_command_(unsigned char) 20 bytes
esphome::epaper_spi::EPaperBase::dc_command_() const 19 bytes
25 more new symbols... Total: 1,580 bytes

Note: This analysis measures static RAM and Flash usage only (compile-time allocation).
Dynamic memory (heap) cannot be measured automatically.
⚠️ You must test this PR on a real device to measure free heap and ensure no runtime memory issues.

This analysis runs automatically when components change. Memory usage is measured from a representative test configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants