Crowpanel 1-28inch ESP32 rotary display with ESPHome

Has anyone managed to get one of these working with ESPHome? I bought one but I am struggling to get it to work. I can load the code but unable to get the display to work.

Your display GC9A01 and touch CST816D are both supported, so should be doable.

Also, have a look here:

If someone has gotten this to work please post your code. I can get it to compile and upload but it would never fully boot if I remember correctly.

Hey, I got it to work finally. See the code below. still needs a bit of work but right now this is a basic clock that is displaying the correct time pulled from HA.

esphome:
  name: elecrowrotary128-inch
  platformio_options:
    build_flags: "-DBOARD_HAS_PSRAM"
    board_build.esp-idf.memory_type: qio_opi
    board_build.flash_mode: dio
  friendly_name: Rotary1.28-inch
  on_boot:
    priority: 800
    then:
      - logger.log: "Backlight ON"
      - output.turn_on: gpio_3_backlight_pwm
      - delay: 200ms
      - output.turn_off: power_light
      - output.turn_on: out1
      - output.turn_on: out2

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf
    version: recommended

# Enable logging
logger:
  level: VERY_VERBOSE

# Enable Home Assistant API
api:
  encryption:
    key: !secret encryption_key

ota:
  - platform: esphome
    password: !secret ota_key

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${friendly_name} Hotspot"
    password: "6T3RFWEF71"

psram:
  mode: octal
  speed: 80MHz 

time:
  - platform: homeassistant
    timezone: "Australia/Melbourne"
    id: esptime
    on_time_sync:
      - script.execute: time_update
    on_time:
      - minutes: '*'
        seconds: 0
        then:
          - script.execute: time_update

output:
  - platform: ledc
    pin: GPIO46
    id: gpio_3_backlight_pwm
  - platform: gpio
    id: power_light
    pin: GPIO40
    # inverted: true
  - platform: gpio
    id: out4
    pin: GPIO4
  - platform: gpio
    id: out12
    pin: GPIO12
  - platform: gpio
    id: out1
    pin: GPIO1
  - platform: gpio
    id: out2
    pin: GPIO2

light:
  - platform: monochromatic
    name: "Backlight"
    output: gpio_3_backlight_pwm
    id: back_light
    restore_mode: ALWAYS_ON

sensor:
  - platform: rotary_encoder
    id: knob
    name: "Encoder"
    pin_a:
      number: 45
      mode:
        input: true
        pullup: true
    pin_b:
      number: 42
      mode:
        input: true
        pullup: true
    resolution: 1
    min_value: -100000
    max_value:  100000

binary_sensor:
  - platform: touchscreen
    id: change_page # Name is not needed, ID is sufficient
    # This zone now covers the entire 240x240 screen
    x_min: 0
    y_min: 0
    x_max: 240
    y_max: 240
    on_press:
      - script.execute: screentime
  - platform: gpio
    pin: 41
    id: rotary_button

script:
  - id: screentime
    mode: restart
    then:
      - light.turn_on: back_light
      - delay: 1 min
      - light.turn_off: back_light
  - id: dim_screen
    then:
      - light.turn_on:
          id: back_light
          brightness: 30%
      - logger.log: "script: dim to 30%"
  - id: bright_screen
    then:
      - light.turn_on:
          id: back_light
          brightness: 100%
      - logger.log: "script: brightness to full"
  - id: time_update
    then:
      - lvgl.indicator.update:
          id: minute_hand
          value: !lambda |-
            return id(esptime).now().minute;
      - lvgl.indicator.update:
          id: hour_hand
          value: !lambda |-
            auto now = id(esptime).now();
            return std::fmod(now.hour, 12) * 60 + now.minute;
      - lvgl.label.update:
          id: date_label
          text: !lambda |-
            static const char * const mon_names[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
            static char date_buf[8];
            auto now = id(esptime).now();
            snprintf(date_buf, sizeof(date_buf), "%s %2d", mon_names[now.month-1], now.day_of_month);
            return date_buf;
      - lvgl.label.update:
          id: day_label
          text: !lambda |-
            static const char * const day_names[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
            return day_names[id(esptime).now().day_of_week - 1];

i2c:
  - id: i2c_touch
    setup_priority: 400
    sda: 6
    scl: 7
    scan: true
    frequency: 400kHz
  

touchscreen:
  platform: cst816
  id: touch
  address: 0x15
  interrupt_pin: 5
  reset_pin: 13
  i2c_id: i2c_touch
  transform:
    mirror_x: false
    mirror_y: true
    swap_xy: true


spi:
  id: spi_bus
  mosi_pin: 11
  clk_pin: 10

font:
  - file: "gfonts://Roboto"
    id: roboto16
    size: 16

display:
  - platform: ili9xxx
    id: round_display
    model: GC9A01A
    cs_pin: GPIO9
    dc_pin: GPIO3
    reset_pin: GPIO14
    invert_colors: true
    show_test_card: true 
    rotation: 0

      
#external_components:
#  - source: 
#      type: local
#      path: components
#    components: [gc9a01a_display]
lvgl:
  displays:
    - round_display
  default_font: roboto16
  disp_bg_color: black

  touchscreens:
    touchscreen_id: touch

  style_definitions:
    - id: date_style    
      text_font: unscii_8
      align: center
      text_color: 0x000000
      bg_opa: cover
      radius: 4
      pad_all: 2

  pages:
    - id: clock_page
      widgets:
        - obj: # clock container
            height: 240
            width: 240
            align: CENTER
            pad_all: 0
            border_width: 0
            bg_color: 0x000000 # black
            widgets:
              - meter: # clock face
                  height: 240
                  width: 240
                  align: CENTER
                  bg_opa: TRANSP
                  border_width: 0
                  text_color: 0xFFFFFF # white
                  scales:
                    - range_from: 0 # minutes scale
                      range_to: 60
                      angle_range: 360
                      rotation: 270
                      ticks:
                        width: 1
                        count: 61
                        length: 10
                        color: 0xFFFFFF # white
                      indicators:
                        - line:
                            id: minute_hand
                            width: 3
                            color: 0xFF0000 # Red
                            r_mod: -4
                            value: 0
                    - range_from: 1 # hours scale for labels
                      range_to: 12
                      angle_range: 330
                      rotation: 300
                      ticks:
                        width: 1
                        count: 12
                        length: 1
                        major:
                          stride: 1
                          width: 4
                          length: 10
                          color: 0xC0C0C0 # silver
                          label_gap: 12
                    - range_from: 0 # hi-res hours scale for hand
                      range_to: 720
                      angle_range: 360
                      rotation: 270
                      ticks:
                        count: 0
                      indicators:
                        - line:
                            id: hour_hand
                            width: 5
                            color: 0xFF0000 # Red
                            r_mod: -30
                            value: 0
              - label:
                  styles: date_style
                  id: day_label
                  y: -30
              - label:
                  id: date_label
                  styles: date_style
                  y: 30


captive_portal:

I’ll post back here as I get more of the functions to work.

Note for me the thing that got it working was holding down the boot button on the back while powering on. Before that I was having issues with boot loops.

Where you able to upload using web.esphome.io interface? There instruction show using Arduino and I’ve not been able to connect using the ESP home web tool.

Hi, curious if you got any further. Thinking about getting one of these as wel…

Hi,

there are some esphome example configs in the official elecrow GitHub repo:

I played a while with the device. I think I got everything working.

  • Display (SPI / ili9xxx / GC9A01A)
  • Touchscreen (I2C / cst816)
  • Touchscreen Backlight
  • Rotary Encoder
  • Button Press
  • Ambient Light LEDs / Status Indicator LEDs
  • Power Indicator LED
  • I2C Port
  • UART Port

Works fine with ESPHome + Home Assistant

Wow thats a good list of functions, do you mind sharing your config for all of us?

looks like you managed to get a lot further along than I did.

Sure. Here are the relevant parts:

esphome:
  name: knob
  friendly_name: Knob
  platformio_options:
    build_flags: '-DBOARD_HAS_PSRAM'
    board_build.esp-idf.memory_type: qio_opi
    board_build.flash_mode: dio
  on_boot:
    then:
      - output.turn_off: gpio40_power_indicator_led
      - output.turn_on: gpio3_backlight_pwm
      - output.turn_on: gpio1_screen
      - light.turn_on:
          id: back_light
          brightness: 1

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

psram:
  mode: octal
  speed: 80MHz

psram:
  mode: octal
  speed: 80MHz

logger:
  level: DEBUG
  #baud_rate: 0

api:

ota:
  - platform: esphome

wifi:
  ssid:
  password:

spi:
  mosi_pin: GPIO11
  clk_pin: GPIO10
  interface: hardware

i2c:
  - id: i2c_touchscreen
    sda: GPIO6
    scl: GPIO7
    scan: false

  - id: i2c_sensor
    sda: GPIO38
    scl: GPIO39
    scan: false

output:
  - platform: ledc
    pin: GPIO46
    id: gpio3_backlight_pwm

  - platform: gpio
    inverted: true
    id: gpio40_power_indicator_led
    pin: GPIO40

  - platform: gpio
    id: gpio1_screen
    pin: GPIO1

light:
  - platform: monochromatic
    name: Backlight
    id: back_light
    output: 
    restore_mode: ALWAYS_ON

  - platform: esp32_rmt_led_strip
    name: Status Indicator LED
    id: status_indicator_led
    rgb_order: GRB
    pin: GPIO48
    num_leds: 5
    chipset: ws2811

binary_sensor:
  - platform: gpio
    name: Button Press
    id: btn_01
    pin:
      number: GPIO41
      inverted: true
      mode:
        input: true
        pullup: true
    filters:
      - delayed_on: 15ms
      - delayed_off: 15ms

sensor:
  - platform: rotary_encoder
    id: knob
    name: 'Rotary Encoder'
    pin_a:
      number: GPIO45
      mode:
        input: true
        pullup: true
    pin_b:
      number: GPIO42
      mode:
        input: true
        pullup: true
    resolution: 1
    filters:
      - debounce: 20ms

touchscreen:
  id: touchscreen_id
  platform: cst816
  i2c_id: i2c_touchscreen
  address: 21
  interrupt_pin: GPIO5
  reset_pin: GPIO13
  skip_probe: true

display:
  id: display_id
  platform: mipi_spi
  model: GC9A01A
  cs_pin: GPIO9
  dc_pin: GPIO3
  reset_pin: GPIO14
  data_rate: 80MHz
  invert_colors: true
  show_test_card: false
  rotation: 0
  update_interval: never
  auto_clear_enabled: false

lvgl:
  displays:
    - display_id

  touchscreens:
    - touchscreen_id

  encoders:
    sensor: knob
    enter_button: btn_01

Any suggestions on how to mount this thing to a wall? There isn’t any mounting hardware. I don’t have a 3-D printer to create something either. I kind of wanted to use this as a display for when our front door is open notifying people to close it (as well as a few other things.)

In my model, you can remove the backplate (or use longer screws?) to mount it onto something

Ive got mine working as well now with my nest.

substitutions:
# ── Update these to match your setup ──────────────────────────────────────
climate_entity: climate.nest # Your HA climate entity
device_name: knob
friendly_name: Knob

esphome:
name: ${device_name}
friendly_name: ${friendly_name}
platformio_options:
build_flags: β€˜-DBOARD_HAS_PSRAM’
board_build.esp-idf.memory_type: qio_opi
board_build.flash_mode: dio
on_boot:
then:
- output.turn_off: gpio40_power_indicator_led
- output.turn_on: gpio3_backlight_pwm
- output.turn_on: gpio1_screen
- light.turn_on:
id: back_light
brightness: 1

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

psram:
mode: octal
speed: 80MHz

logger:
level: DEBUG

api:

ota:

  • platform: esphome

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: β€œ${friendly_name} Fallback”
password: !secret ap_password

captive_portal:

spi:
mosi_pin: GPIO11
clk_pin: GPIO10
interface: hardware

i2c:

  • id: i2c_touchscreen
    sda: GPIO6
    scl: GPIO7
    scan: false

  • id: i2c_sensor
    sda: GPIO38
    scl: GPIO39
    scan: false

── Fonts ──────────────────────────────────────────────────────────────────

font:

  • file: β€œgfonts://Roboto”
    id: font_huge
    size: 80
    bpp: 4
    glyphs: "0123456789.-Β° "

  • file: β€œgfonts://Roboto”
    id: font_large
    size: 48
    bpp: 4
    glyphs: "0123456789.-Β°% "

  • file: β€œgfonts://Roboto”
    id: font_medium
    size: 20
    bpp: 4
    glyphs: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-Β°%● "

  • file: β€œgfonts://Roboto”
    id: font_small
    size: 20
    bpp: 4
    glyphs: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-Β°% "
    globals:

  • id: encoder_ready
    type: bool
    restore_value: no
    initial_value: β€˜true’

  • id: heating_on
    type: bool
    restore_value: no
    initial_value: β€˜false’

  • id: target_temp
    type: float
    restore_value: no
    initial_value: β€˜20.0’

  • id: pending_temp_send
    type: bool
    restore_value: no
    initial_value: β€˜false’

  • id: display_dimmed
    type: bool
    restore_value: no
    initial_value: β€˜false’

── Dim Screen Script ──────────────────────────────────────────────────────────────────

script:

  • id: dim_timer
    mode: restart
    then:
    • delay: 15s
    • light.turn_on:
      id: back_light
      brightness: 5%
    • lambda: β€˜id(display_dimmed) = true;’

output:

  • platform: ledc
    pin: GPIO46
    id: gpio3_backlight_pwm

  • platform: gpio
    inverted: true
    id: gpio40_power_indicator_led
    pin: GPIO40

  • platform: gpio
    id: gpio1_screen
    pin: GPIO1

light:

  • platform: monochromatic
    name: Backlight
    id: back_light
    output: gpio3_backlight_pwm
    restore_mode: ALWAYS_ON

  • platform: esp32_rmt_led_strip
    name: Status Indicator LED
    id: status_indicator_led
    rgb_order: GRB
    pin: GPIO48
    num_leds: 5
    chipset: ws2811

binary_sensor:

  • platform: gpio
    name: Button Press
    id: btn_01
    pin:
    number: GPIO41
    inverted: true
    mode:
    input: true
    pullup: true
    filters:
    • delayed_on: 15ms
    • delayed_off: 15ms

── dimming of screen restore on touch (not click!!)──────────────────────────────────────────────────────────────────

on_press:
  then:
    - if:
        condition:
          lambda: 'return id(display_dimmed);'
        then:
          - light.turn_on:
              id: back_light
              brightness: 100%
          - lambda: 'id(display_dimmed) = false;'
          - script.execute: dim_timer

────────────────────────────────────────────────────────────────────

        else:
          - lambda: |-
              id(heating_on) = !id(heating_on);
          - if:
              condition:
                lambda: 'return id(heating_on);'
              then:
                - homeassistant.service:
                    service: climate.set_hvac_mode
                    data:
                      entity_id: ${climate_entity}
                      hvac_mode: "heat"
                - lvgl.label.update:
                    id: lv_heat_label
                    text: "ON"
                    text_align: CENTER
                - lvgl.widget.update:
                    id: lv_button_2
                    bg_color: 0xEF4444
                - lvgl.arc.update:
                    id: lv_arc_1
                    value: !lambda 'return (int) id(target_temp);'
                - lvgl.widget.update:
                    id: lv_arc_1
                    state:
                      disabled: false
              else:
                - homeassistant.service:
                    service: climate.set_hvac_mode
                    data:
                      entity_id: ${climate_entity}
                      hvac_mode: "off"
                - lvgl.label.update:
                    id: lv_heat_label
                    text: "OFF"
                    text_align: CENTER
                - lvgl.widget.update:
                    id: lv_button_2
                    bg_color: 0x4F46E5
                - lvgl.arc.update:
                    id: lv_arc_1
                    value: 0
                - lvgl.widget.update:
                    id: lv_arc_1
                    state:
                      disabled: true

sensor:

  • platform: rotary_encoder
    id: knob
    name: β€˜Rotary Encoder’
    pin_a:
    number: GPIO45
    mode:
    input: true
    pullup: true
    pin_b:
    number: GPIO42
    mode:
    input: true
    pullup: true
    resolution: 1
    filters:

    • debounce: 20ms
      on_clockwise:
      then:
      • lambda: |-
        float new_temp = id(target_temp) + 0.5f;
        if (new_temp > 30.0f) new_temp = 30.0f;
        id(target_temp) = new_temp;
        id(pending_temp_send) = true;
      • lvgl.arc.update:
        id: lv_arc_1
        value: !lambda β€˜return (int) id(target_temp);’
        indicator:
        arc_color: !lambda |-
        float t = (id(target_temp) - 15.0f) / 15.0f;
        if (t < 0.0f) t = 0.0f;
        if (t > 1.0f) t = 1.0f;
        int r = (int)(0x3B + t * (0xEF - 0x3B));
        int g = (int)(0x82 + t * (0x44 - 0x82));
        int b = (int)(0xF6 + t * (0x44 - 0xF6));
        return lv_color_make(r, g, b);
        on_anticlockwise:
        then:
      • lambda: |-
        float new_temp = id(target_temp) - 0.5f;
        if (new_temp < 15.0f) new_temp = 15.0f;
        id(target_temp) = new_temp;
        id(pending_temp_send) = true;
      • lvgl.arc.update:
        id: lv_arc_1
        value: !lambda β€˜return (int) id(target_temp);’
        indicator:
        arc_color: !lambda |-
        float t = (id(target_temp) - 15.0f) / 15.0f;
        if (t < 0.0f) t = 0.0f;
        if (t > 1.0f) t = 1.0f;
        int r = (int)(0x3B + t * (0xEF - 0x3B));
        int g = (int)(0x82 + t * (0x44 - 0x82));
        int b = (int)(0xF6 + t * (0x44 - 0xF6));
        return lv_color_make(r, g, b);

    Current room temperature

  • platform: homeassistant
    id: current_temperature
    entity_id: ${climate_entity}
    attribute: current_temperature
    internal: true
    on_value:
    then:
    - lvgl.label.update:
    id: lbl_current_temp
    text: !lambda |-
    char buf[8];
    snprintf(buf, sizeof(buf), β€œ%.1fΒ°C”, x);
    return std::string(buf);

  • platform: homeassistant
    id: nest_temperature
    entity_id: ${climate_entity}
    attribute: temperature
    on_value:
    then:
    - lambda: |-
    id(target_temp) = x;
    - lvgl.arc.update:
    id: lv_arc_1
    value: !lambda β€˜return (int) id(target_temp);’
    indicator:
    arc_color: !lambda |-
    float t = (id(target_temp) - 15.0f) / 15.0f;
    if (t < 0.0f) t = 0.0f;
    if (t > 1.0f) t = 1.0f;
    int r = (int)(0x3B + t * (0xEF - 0x3B));
    int g = (int)(0x82 + t * (0x44 - 0x82));
    int b = (int)(0xF6 + t * (0x44 - 0xF6));
    return lv_color_make(r, g, b);

text_sensor:

  • platform: homeassistant
    id: nest_hvac_mode
    entity_id: ${climate_entity}
    on_value:
    then:
    - lambda: |-
    id(heating_on) = (x == β€œheat”);
    - if:
    condition:
    lambda: β€˜return id(heating_on);’
    then:
    - lvgl.label.update:
    id: lv_heat_label
    text: β€œON”
    - lvgl.widget.update:
    id: lv_button_2
    bg_color: 0xEF4444
    - lvgl.widget.update:
    id: lv_arc_1
    state:
    disabled: false
    - lvgl.arc.update:
    id: lv_arc_1
    value: !lambda β€˜return (int) id(target_temp);’
    else:
    - lvgl.label.update:
    id: lv_heat_label
    text: β€œOFF”
    - lvgl.widget.update:
    id: lv_button_2
    bg_color: 0x4F46E5
    - lvgl.widget.update:
    id: lv_arc_1
    state:
    disabled: true
    - lvgl.arc.update:
    id: lv_arc_1
    value: 0

interval:

  • interval: 1500ms
    then:
    • if:
      condition:
      lambda: β€˜return id(pending_temp_send);’
      then:
      - lambda: |-
      id(pending_temp_send) = false;
      ESP_LOGI(β€œknob”, β€œSending debounced temp: %.1f”, id(target_temp));
      - homeassistant.service:
      service: climate.set_temperature
      data:
      entity_id: ${climate_entity}
      temperature: !lambda β€˜return id(target_temp);’

touchscreen:
id: touchscreen_id
platform: cst816
i2c_id: i2c_touchscreen
address: 21
interrupt_pin: GPIO5
reset_pin: GPIO13
skip_probe: true
on_touch:
then:
- if:
condition:
lambda: β€˜return id(display_dimmed);’
then:
- light.turn_on:
id: back_light
brightness: 100%
- lambda: β€˜id(display_dimmed) = false;’
- script.execute: dim_timer
else:
- script.execute: dim_timer

display:
id: display_id
platform: mipi_spi
model: GC9A01A
cs_pin: GPIO9
dc_pin: GPIO3
reset_pin: GPIO14
data_rate: 80MHz
invert_colors: true
show_test_card: false
rotation: 0
update_interval: never
auto_clear_enabled: false

lvgl:
pages:
- id: canvas_1
bg_color: 0x111827
pad_all: 0
widgets:
- arc:
id: lv_arc_1
x: 0
y: 0
width: 230
height: 230
value: 20
min_value: 15
max_value: 30
adjustable: true
start_angle: 135
end_angle: 45
arc_color: 0x404040
arc_width: 15
arc_opa: 100%
arc_rounded: false
indicator:
arc_color: 0x818CF8
arc_width: 15
arc_opa: 100%
arc_rounded: false
knob:
bg_color: 0xFFFFFF
radius: 9999
border_width: 2
border_color: 0x818CF8
bg_opa: 0%

    - button:
        id: lv_button_2
        x: 25
        y: 25
        width: 180
        height: 180
        text_align: CENTER
        bg_color: 0x4F46E5
        checkable: false
        radius: 65535
                   
        
        widgets:
          - label:
              id: lv_heat_label
              text: "OFF"
              text_color: 0xFFFFFF 
              text_font: font_medium
              align: CENTER
              x: 0
              y: 0


              
    
    - label:
        id: lv_label_3
        x: 70
        y: 40
        width: 100
        height: 30
        text: "HEATING"
        text_font: font_medium
        text_color: 0xffffff
        text_align: CENTER
        pad_top: 6

    - label:
        id: lbl_current_temp
        x: 0
        y: 65
        text_font: font_medium
        text_color: 0xFFFFFF
        text: "20Β°C"
        align: CENTER

Thank you!!! I also got everything to work EXCEPT the display. Tried so many different variants and tested sooo many Gemini suggestions. Then I found this and copy/pasted your display related sections and BOOM! You rock - thanks for posting!