New boards Wishlist

Support for e-ink displays (reterminal/ m5Paper)

rtucker666Suggested this wish Β·

Hi Tessera,

This sounds like an epic way to control smart dashboards. I have recently purchased a reTerminal Sticky which is an ESP-32 S3 based touch e-ink display, which I have flashed custom firmware to in order to allow Home Assistant tiles and entities to be controlled. However the process is pretty tiresome every time I want to make a change and have to carefully re code my build in order for it not to get stuck in a boot loop after compiliation. Hipefully you guys might support this screen in the future, I see you already have some Seeedstudio and M5Paper screens, so hopefully not much of a stretch to include some e-Ink!

Thanks

Hardware testing

We often do not have this board ourselves. Community testing helps establish what actually works. A successful report is evidence for the maintainer to review, not an automatic compatibility claim.

Board details & configuration
Model
reTerminal Sticky
Chip, RAM and flash
ESP32-S3, 8MB PSRAM
Display and touch
Black-and-white touch E-Ink display, 3.97 inches, 800 x 480, SPI / 12C
Already tried
ESP32-S3 generic
Configuration status
working
View shared configuration
# =============================================================================
#  reTerminal Sticky - Home Assistant wall controller
#  Automation Addict  |  v22
#
#      +------------+------------+
#      |   TILE 1   |   TILE 2   |
#      +------------+------------+
#      |   TILE 3   |   TILE 4   |
#      +------------+------------+
#      |   Voice    |   Note     |
#      +-------------------------+
#
#  EVERYTHING YOU NEED TO CHANGE IS IN THE BLOCK DIRECTLY BELOW.
#  Nothing under `# ---- machinery ----` needs touching to re-skin this for
#  your own house.
#
#  Tiles 1-3 toggle anything toggleable - light, switch, fan, input_boolean,
#  media_player, script. Tile 4 is the cover slot (blinds, curtains, garage).
#  Want a fourth toggle instead of a cover? See SWAP-GUIDE.md.
#
#  Icons are Material Design Icon NAMES. Browse them at
#  https://pictogrammers.com/library/mdi/ and paste the name in - no
#  codepoints, no downloads, ESPHome fetches the SVG when it builds.
# =============================================================================
#
#  ---------------------------------------------------------------------------
#  BEFORE YOUR FIRST BUILD - secrets
#  ---------------------------------------------------------------------------
#  This file ships with NO credentials in it. Everything sensitive lives in
#  `secrets.yaml`, which sits next to this file in your ESPHome config folder
#  and never gets shared.
#
#  1. Generate an API encryption key. Easiest route is the ESPHome dashboard
#     (it offers to make one when you add a device), or run:
#         python3 -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"
#     Paste the result into the `api_encryption_key` line below.
#
#  2. Create `secrets.yaml` with these four lines:
#
#         wifi_ssid: "YourNetworkName"
#         wifi_password: "YourWiFiPassword"
#         ota_password: "something-long-and-random"
#         api_encryption_key: "paste-the-44-character-key-here"
#
#  3. Add `secrets.yaml` to your `.gitignore` before you commit anything.
#
#  Note: OTA has its own password here rather than reusing the Wi-Fi one.
#  They are separate credentials and should stay that way.
#  ---------------------------------------------------------------------------
substitutions:
  # ---------------------------------------------------------------------------
  #  >>>>>>>>>>>>>>>>>>>>>>  EDIT FROM HERE  <<<<<<<<<<<<<<<<<<<<<<
  # ---------------------------------------------------------------------------
  device_name: sticky_eink_v1
  friendly_name: "Sticky_Eink_v1"
  footer_text: "captains quarters"
  # ---- TILE 1 (top left) ----------------------------------------------------
  tile1_entity: input_boolean.music_transfer
  tile1_label:  "Music"
  tile1_icon:   music
  # ---- TILE 2 (top right) ---------------------------------------------------
  tile2_entity: input_boolean.lighting_control
  tile2_label:  "Lighting"
  tile2_icon:   ceiling-light
  # ---- TILE 3 (bottom left) -------------------------------------------------
  tile3_entity: input_boolean.cooking_mode_bypass
  tile3_label:  "Cooking"
  tile3_icon:   chef-hat
  # ---- TILE 4 (bottom right) - THE COVER SLOT -------------------------------
  #  Must be a `cover.` entity. Tapping it opens or closes, whichever is the
  #  opposite of where it is now.
  tile4_entity: cover.living_room_blinds
  tile4_label:  "Heating"
  tile4_icon:   radiator
  # ---- BOTTOM ROW: TALK and NOTE --------------------------------------------
  voice_label: "TALK"
  voice_icon:  home-assistant
  #  The note is stored in Home Assistant, not on the device. Create a helper:
  #    Settings > Devices & Services > Helpers > Create helper > Text
  #    Name it "Sticky Note", set Maximum length to 255.
  #  Storing it in HA rather than on the device means it survives deep sleep
  #  AND you can set or clear the note from your phone while you are out.
  note_entity:  input_text.sticky_note
  note_label:   "NOTE"
  note_icon:    note-edit-outline
  #  Cosmetics for the note card.
  #  The little icon above the greeting, top-left of the card.
  note_accent_icon:  pin
  note_greeting:     "Message waiting"
  #  The big doodle in the bottom-right corner of the card.
  note_doodle_icon:  home-assistant
  # ---- LOW POWER MODE -------------------------------------------------------
  #  "true"  - sleeps after the idle timeout below, wakes on any button press.
  #            Days of battery, but while asleep it cannot hear Home
  #            Assistant, so the tiles show the last known states until
  #            something wakes it.
  #  "false" - never sleeps. Instantly responsive and always live, but you
  #            will be charging it roughly daily.
  #
  #  Compile-time on purpose. This used to be a switch in Home Assistant, and
  #  a stored "off" silently overrode the config - which is a miserable thing
  #  to debug, because the file says one thing and the device does another.
  #  Now the file is the only answer. Changing it means a reflash; knowing
  #  what your device will do means reading one line.
  low_power: "true"
  awake_after_button_ms: "300000"    # 5 min awake after you touch it
  awake_after_timer_ms:  "25000"     # 25 s awake on a self-triggered refresh
  sleep_duration: 360min              # how often it wakes itself to catch up
  tile1_state: input_text.sticky_room
  tile2_state: input_text.sticky_mode
  # ---------------------------------------------------------------------------
  #  >>>>>>>>>>>>>>>>>>>>>>  EDIT TO HERE  <<<<<<<<<<<<<<<<<<<<<<
  #  Everything past this point is machinery.
  # ---------------------------------------------------------------------------
esphome:
  name: ${device_name}
  friendly_name: ${friendly_name}
  on_boot:
    # -------------------------------------------------------------------------
    # PRIORITY 1000 - runs before ANY component sets up, display included
    # (the display initialises at HARDWARE priority, 800).
    #
    # Why this exists: ESPHome puts the SSD1677 controller into its OWN deep
    # sleep mode after each refresh, and the datasheet is clear that the only
    # way out of that state is a HARDWARE reset. On an MCU deep-sleep wake the
    # panel was never power-cycled - it is still sitting in controller deep
    # sleep - so ESPHome's init sequence talks to a chip that is not listening.
    # The result is a frame with inverted polarity and horizontal streaks,
    # because the waveform LUT never loaded.
    #
    # So we cold-start the panel by hand here: drop its enable rail, hold
    # reset low, let the rails collapse, then bring it back. By the time the
    # display component initialises it is talking to a genuinely fresh
    # controller.
    #
    # Pin numbers come from ESPHome's own board preset for this model:
    #   enable 47   reset 17   cs 15   dc 16   busy 18
    # -------------------------------------------------------------------------
    - priority: 1000
      then:
        - lambda: |-
            // 1. Release anything the deep-sleep pad hold was pinning.
            gpio_deep_sleep_hold_dis();
            gpio_hold_dis((gpio_num_t) 45);
            gpio_hold_dis((gpio_num_t) 46);
            // 2. Hard power-cycle the ePaper panel.
            const gpio_num_t EPD_EN  = (gpio_num_t) 47;
            const gpio_num_t EPD_RST = (gpio_num_t) 17;
            gpio_hold_dis(EPD_EN);
            gpio_hold_dis(EPD_RST);
            gpio_reset_pin(EPD_EN);
            gpio_reset_pin(EPD_RST);
            gpio_set_direction(EPD_EN,  GPIO_MODE_OUTPUT);
            gpio_set_direction(EPD_RST, GPIO_MODE_OUTPUT);
            gpio_set_level(EPD_RST, 0);      // hold controller in reset
            gpio_set_level(EPD_EN,  0);      // and kill its supply
            delay(150);                      // let the rails actually collapse
            gpio_set_level(EPD_EN,  1);      // power back up
            delay(30);
            gpio_set_level(EPD_RST, 1);      // release reset
            delay(30);
            ESP_LOGI("epd", "panel cold-started");
    - priority: 600
      then:
        - lambda: |-
            // A button wake means a person is here - stay up for the full
            // idle window. A timed wake is just a refresh, so get in and out.
            auto cause = esp_sleep_get_wakeup_cause();
            bool by_button = (cause == ESP_SLEEP_WAKEUP_EXT1);
            id(awake_ms) = by_button ? ${awake_after_button_ms} : ${awake_after_timer_ms};
            id(note_sleep_card) = false;
            ESP_LOGI("sleep", "wake cause=%d  awake_for=%d ms", (int) cause, id(awake_ms));
        - deep_sleep.prevent: sleeper
        - output.turn_on: pwr_hold
        - output.turn_on: pwr_lock
        - switch.turn_on: touch_power_enable
        - output.turn_on: mic_power_enable
        - output.turn_on: charge_enable
        - pcf8563.read_time:
    - priority: -100
      then:
        - delay: 4s
        # Two full refreshes on wake. ePaper needs one pass to flush whatever
        # the controller was left holding and a second to lay down a clean
        # frame - cheap insurance against a half-initialised panel, and it
        # costs a couple of seconds only on wake, not during normal use.
        # Two refreshes, spaced well past a full cycle. The old version had
        # them 1.5 s apart, so the second and third were simply rejected -
        # "Display already in state POWER_OFF" in your log was exactly that.
        - component.update: eink
        - delay: 10s
        - component.update: eink
        - script.execute: idle_timer
esp32:
  board: esp32-s3-devkitc-1
  variant: ESP32S3
  flash_size: 32MB
  framework:
    type: arduino
    advanced:
      enable_idf_experimental_features: true
logger:
  hardware_uart: UART0
  level: INFO
api:
  encryption:
    # Generate your own - see the "BEFORE YOUR FIRST BUILD" note at the top.
    key: !secret api_encryption_key
ota:
  - platform: esphome
    password: !secret wifi_password
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  power_save_mode: LIGHT
  ap:
    ssid: "${device_name} Setup"
captive_portal:
spi:
  clk_pin: GPIO13
  mosi_pin: GPIO14
  miso_pin: GPIO12
i2c:
  - id: i2c_sensor
    scl: GPIO0
    sda: GPIO1
    frequency: 400kHz
    # Boot log prints "Found i2c device at address 0x.." for each chip.
    # Expect 0x44 (SHT40), 0x51 (RTC), 0x55 (BQ27220), 0x6A (IMU).
    # If 0x55 is absent, the fuel gauge is not answering and no amount of
    # register fiddling will help.
    scan: true
  - id: i2c_touch
    scl: GPIO2
    sda: GPIO3
    frequency: 100kHz
i2s_audio:
  i2s_lrclk_pin: GPIO19
# =============================================================================
#  DEEP SLEEP
#
#  Wakes on ANY of the three physical buttons (OK/UP/DOWN), and once an hour on
#  its own to refresh the clock and entity states. Sleeps again after the idle
#  timer expires.
#
#  READ THIS BEFORE FLASHING - the one way this bites:
#  GPIO45 and GPIO46 are the power latch. In deep sleep, ordinary GPIOs stop
#  driving, the latch opens, and the device powers all the way off instead of
#  sleeping. `gpio_deep_sleep_hold_en()` in the go_to_sleep script pins them
#  where they are. If that ever fails you have not bricked anything - OK is the
#  physical power button, so a long press brings it back.
# =============================================================================
deep_sleep:
  id: sleeper
  esp32_ext1_wakeup:
    pins:
      - number: GPIO4
        mode: INPUT_PULLUP
        allow_other_uses: true
      - number: GPIO5
        mode: INPUT_PULLUP
        allow_other_uses: true
      - number: GPIO6
        mode: INPUT_PULLUP
        allow_other_uses: true
    mode: ANY_LOW
  sleep_duration: ${sleep_duration}
output:
  - platform: gpio
    pin: GPIO45
    id: pwr_hold
  - platform: gpio
    pin: GPIO46
    id: pwr_lock
  - platform: gpio
    pin: GPIO38
    id: mic_power_enable
  # BQ25616 charger enable, active LOW. This was missing - without driving it
  # the charge path is left to whatever the pin defaults to. Turning this
  # output ON drives the pin LOW, which enables charging.
  - platform: gpio
    pin:
      number: GPIO39
      inverted: true
    id: charge_enable
  - platform: ledc
    pin: GPIO48
    id: buzzer_pwm
    frequency: 2700Hz
switch:
  # ---------------------------------------------------------------------------
  # Touch axis correction, adjustable at runtime from Home Assistant.
  # These stay switches on purpose: calibration is something you fiddle with
  # once, and toggling beats a reflash-and-guess cycle. Unlike a power mode,
  # getting one wrong is instantly obvious - the wrong tile fires.
  # ---------------------------------------------------------------------------
  - platform: template
    id: flip_x
    name: "Touch Flip X"
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    entity_category: config
  - platform: template
    id: flip_y
    name: "Touch Flip Y"
    optimistic: true
    # SETTLED: this board needs Y flipped and X left alone.
    restore_mode: RESTORE_DEFAULT_ON
    entity_category: config
  - platform: gpio
    pin: GPIO42
    id: touch_power_enable
    internal: true
    restore_mode: ALWAYS_ON
    setup_priority: 1100
light:
  - platform: monochromatic
    output: buzzer_pwm
    id: buzzer
    internal: true
    default_transition_length: 0s
time:
  - platform: pcf8563
    i2c_id: i2c_sensor
    id: rtc_time
    address: 0x51
    update_interval: never
  - platform: homeassistant
    id: ha_time
    on_time_sync:
      then:
        - pcf8563.write_time:
# ui_mode: 0 menu, 1 listening, 2 thinking, 3 reply
globals:
  - id: ui_mode
    type: int
    restore_value: false
    initial_value: '0'
  - id: awake_ms
    type: int
    restore_value: false
    initial_value: '${awake_after_button_ms}'
  - id: hit_tile
    type: int
    restore_value: false
    initial_value: '-1'
  # True while a voice session is capturing a NOTE rather than a command.
  - id: note_capture
    type: bool
    restore_value: false
    initial_value: 'false'
  # True when the note card is being drawn as the sleep screen, so we leave
  # the buttons off it - nothing can be pressed while it is asleep.
  - id: sleep_redraw
    type: bool
    restore_value: false
    initial_value: 'false'
  - id: note_sleep_card
    type: bool
    restore_value: false
    initial_value: 'false'
  - id: last_heard
    type: std::string
    restore_value: false
    initial_value: '""'
  - id: last_reply
    type: std::string
    restore_value: false
    initial_value: '""'
font:
  - file: "gfonts://Inter@700"
    id: f_tile
    size: 27
  - file: "gfonts://Inter@700"
    id: f_state
    size: 21
  - file: "gfonts://Inter@700"
    id: f_head
    size: 36
  - file: "gfonts://Inter@400"
    id: f_body
    size: 24
  # Three sizes for the note. Short notes get the big one; the lambda steps
  # down only if the text will not fit.
  - file: "gfonts://Inter@600"
    id: f_note_xl
    size: 50
  - file: "gfonts://Inter@600"
    id: f_note_lg
    size: 40
  - file: "gfonts://Inter@500"
    id: f_reply
    size: 31
  - file: "gfonts://Inter@400"
    id: f_small
    size: 17
# BINARY + chroma_key transparency = only the glyph pixels are drawn, no
# background box. (TRANSPARENT_BINARY was the old spelling; ESPHome removed it.)
image:
  - id: ic_tile1
    file: mdi:${tile1_icon}
    resize: 96x96
    type: BINARY
    transparency: chroma_key
  - id: ic_tile2
    file: mdi:${tile2_icon}
    resize: 96x96
    type: BINARY
    transparency: chroma_key
  - id: ic_tile3
    file: mdi:${tile3_icon}
    resize: 96x96
    type: BINARY
    transparency: chroma_key
  - id: ic_tile4
    file: mdi:${tile4_icon}
    resize: 96x96
    type: BINARY
    transparency: chroma_key
  - id: ic_mic
    file: mdi:${voice_icon}
    resize: 64x64
    type: BINARY
    transparency: chroma_key
  - id: ic_listening
    file: mdi:microphone-message
    resize: 160x160
    type: BINARY
    transparency: chroma_key
  - id: ic_thinking
    file: mdi:dots-horizontal-circle-outline
    resize: 160x160
    type: BINARY
    transparency: chroma_key
  - id: ic_back
    file: mdi:arrow-left
    resize: 40x40
    type: BINARY
    transparency: chroma_key
  - id: ic_note
    file: mdi:${note_icon}
    resize: 64x64
    type: BINARY
    transparency: chroma_key
  - id: ic_accent
    file: mdi:${note_accent_icon}
    resize: 34x34
    type: BINARY
    transparency: chroma_key
  - id: ic_doodle
    file: mdi:${note_doodle_icon}
    resize: 120x120
    type: BINARY
    transparency: chroma_key
  - id: ic_trash
    file: mdi:trash-can-outline
    resize: 40x40
    type: BINARY
    transparency: chroma_key
  - id: ic_reply
    file: mdi:message-text-outline
    resize: 88x88
    type: BINARY
    transparency: chroma_key
sensor:
  - platform: sht4x
    i2c_id: i2c_sensor
    address: 0x44
    temperature:
      name: "Temperature"
      id: temp_sensor
    humidity:
      name: "Humidity"
      id: hum_sensor
    update_interval: 120s
  - platform: template
    id: battery_level
    name: "Battery Level"
    unit_of_measurement: "%"
    device_class: battery
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 120s
    lambda: |-
      // BQ27220 StateOfCharge, standard command 0x2C. No unseal needed.
      uint8_t reg = 0x2C;
      uint8_t raw[2] = {0, 0};
      auto err = id(i2c_sensor).write_readv(0x55, &reg, 1, raw, 2);
      if (err != esphome::i2c::ERROR_OK) {
        ESP_LOGW("battery", "BQ27220 SoC read failed, i2c error=%d", (int) err);
        return NAN;
      }
      return static_cast<float>(static_cast<uint16_t>(raw[0]) |
                               (static_cast<uint16_t>(raw[1]) << 8));
  # Voltage and current are the diagnostic pair. If these read fine but SoC
  # does not, it is a register problem. If all three fail, the gauge is not on
  # the bus at all - check the scan lines in the boot log.
  - platform: template
    id: battery_voltage
    name: "Battery Voltage"
    unit_of_measurement: "mV"
    device_class: voltage
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 120s
    lambda: |-
      uint8_t reg = 0x08;                      // Voltage()
      uint8_t raw[2] = {0, 0};
      auto err = id(i2c_sensor).write_readv(0x55, &reg, 1, raw, 2);
      if (err != esphome::i2c::ERROR_OK) return NAN;
      return static_cast<float>(static_cast<uint16_t>(raw[0]) |
                               (static_cast<uint16_t>(raw[1]) << 8));
  - platform: template
    id: battery_current
    name: "Battery Current"
    unit_of_measurement: "mA"
    device_class: current
    state_class: measurement
    accuracy_decimals: 0
    update_interval: 120s
    lambda: |-
      uint8_t reg = 0x0C;                      // AverageCurrent(), signed
      uint8_t raw[2] = {0, 0};
      auto err = id(i2c_sensor).write_readv(0x55, &reg, 1, raw, 2);
      if (err != esphome::i2c::ERROR_OK) return NAN;
      uint16_t v = static_cast<uint16_t>(raw[0]) | (static_cast<uint16_t>(raw[1]) << 8);
      return static_cast<float>(static_cast<int16_t>(v));   // positive = charging
  - platform: wifi_signal
    name: "Wi-Fi Signal"
    update_interval: 120s
    entity_category: diagnostic
# -----------------------------------------------------------------------------
# PDM microphone
#
# ESPHome's defaults here are wrong for a single PDM mic and produce exactly
# what you saw: 15 seconds of listening, pure silence, and Whisper filling the
# void with "Thank you."
#
#   channel          default is `right`. A single PDM mic with its SELECT pin
#                    tied low sits on the LEFT channel - which is the usual
#                    wiring. Reading the right channel gives you digital
#                    silence, not quiet audio. THIS IS THE PRIME SUSPECT.
#   bits_per_sample  default is `32bit`. PDM mics want 16.
#   correct_dc_offset  PDM output

1 owner has volunteered to test.

Sign in to volunteer

Discussion 0

Add a comment

Start the conversation

Share an example, ask a question or explain how you would use this.

Add your thoughts, a screenshot or a useful link.

Markdown & basic HTML
Paste or drop images Β· URLs on their own line become previews0 / 10,000
Create your account to join in

We need a verified email address to reach you about your contribution. Your address stays private.

Continue with GitHub

Your draft stays in this tab. Return here after verification.