From ea5b7c405fbf6d0551094754da67291137a88282 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 24 Oct 2026 20:19:59 -0700 Subject: [PATCH 1/4] Add M5Stack Dial Templates A board template for the M5Stack Dial and an LVGL example device that exercises it on a bench unit: the encoder pages, the front button beeps, and the touch, status, inputs and controls pages show each part working. Co-Authored-By: Claude Opus 5.5 --- HISTORY.md | 1 + OPERATIONS.md | 12 ++ README.md | 20 ++ cspell.json | 1 + templates/m5stack-dial.yaml | 259 ++++++++++++++++++++++++++ test/m5stack-dial.yaml | 354 ++++++++++++++++++++++++++++++++++++ 6 files changed, 647 insertions(+) create mode 100644 templates/m5stack-dial.yaml create mode 100644 test/m5stack-dial.yaml diff --git a/HISTORY.md b/HISTORY.md index 2479d5b..963f62b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,6 +7,7 @@ ESPHome configuration templates and projects. - Version 2.4: - Added M5Stack CoreS3 support, the first touchscreen template, with an LVGL example device. - Added an M5Stack CoreS3 camera overlay for the GC0308 camera. + - Added M5Stack Dial support, with an LVGL example device the encoder navigates. - Version 2.3: - Changed OTA to encryption only, using the API key, so only the upgrade bridge build still needs the `ota_password` secret. - Fixed the compile warnings the templates and the EasyStart component can address. diff --git a/OPERATIONS.md b/OPERATIONS.md index 676dd63..75a0bd1 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -329,6 +329,17 @@ Do not `!remove` blocks from Apollo's package without checking what depends on t - **The FT6336 drops quick taps once it falls into its monitor scan.** It slows its scan a few seconds after the last touch. Waking a dark screen then took several taps, and the touchscreen component logged one of three. ESPHome's `ft63x6` driver sets the active scan rate in register 0x88 and leaves the power mode in register 0x86 at its default. The board template writes 0 to register 0x86 through the touchscreen's own I2C device. It does so in an `on_boot` at priority -100, since the driver's setup resets the controller. A single tap then wakes the screen. - **The example device swipes between tabs only from the screen edge.** LVGL's tabview swipe is a plain scroll that also starts on the slider and the drawing pad, so the example turns it off. A drag starting within 24 pixels of the left or right edge and moving 60 pixels inward changes tab instead. +### M5Stack Dial + +[`templates/m5stack-dial.yaml`][m5stack-dial-template] is a board template, with every part on the StampS3 GPIO pins rather than behind an expander or a PMU. + +- **GPIO46 is the power hold, an output.** On battery the front button or the RTC alarm powers the board on, and it stays on only while the firmware drives GPIO46 high. ESPHome's device page and M5Stack's own example configure it as a "Hold Button" input instead. That would let a battery-powered board turn itself off. On USB or the DC input the pin does nothing, so the bench unit could not test it. +- **GPIO8 resets both the LCD and the WS1850S RFID reader.** The display owns the pin, so the reader takes no `reset_pin`. On the bench the reader set up without an error after the display's reset pulse. +- **The FT3267 touch controller runs on the `ft5x06` driver.** The boot I2C scan lists only 0x28 and 0x51, but the driver then reads vendor ID 0x11 at 0x38, and touch works. Do not chase the missing address. +- **The touch mapping matches the display with no calibration.** Taps on rings 30 pixels in from each edge and at the center landed on them. The reported coordinates sit one pixel below the raw ones. +- **`resolution: 1` gives one encoder step per detent.** M5Stack's example uses 4, which counts four steps per click. +- **The RTC is a BM8563, which the docs also call an RTC8563 and M5Stack's example drives as a PCF8563.** The template uses the `bm8563` platform, as the CoreS3 template does. + ### RGB LED Status [`templates/rgb-led-status.yaml`][rgb-led-status-template] derives from [Flo-R1der's package][flo-r1der-link] but does not import it, because that version binds its state to `wifi:` `on_connect` triggers and so cannot run on an Ethernet board. @@ -693,6 +704,7 @@ Sharp edges in the tooling around this repository, each one learned by tripping [heltec-template]: ./templates/heltec-wifi-lora32-v4-r8.yaml [m5stack-cores3-camera-template]: ./templates/m5stack-cores3-camera.yaml [m5stack-cores3-template]: ./templates/m5stack-cores3.yaml +[m5stack-dial-template]: ./templates/m5stack-dial.yaml [max17048-template]: ./templates/max17048.yaml [min-version-template]: ./templates/min-version.yaml [office-bluetooth-proxy]: ./office-bluetooth-proxy.yaml diff --git a/README.md b/README.md index 58e30b4..adad480 100755 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ ESPHome configuration templates and projects. - Added M5Stack CoreS3 support, the first touchscreen template, with an LVGL example device. - Added an M5Stack CoreS3 camera overlay for the GC0308 camera. +- Added M5Stack Dial support, with an LVGL example device the encoder navigates. See [HISTORY.md][history] for the release notes. @@ -252,6 +253,21 @@ Shared building-block includes, composed via `packages:` by the device templates - `camera_clock_pin`: `GPIO10` by default, a spare M5-Bus pin for the clock ESPHome requires although the camera clocks itself. Move it where a module uses `GPIO10`. - Note: the camera's pixel clock is GPIO45, the pin the ESP32-S3 reads at reset to pick its flash voltage. A reset taken while the camera streams can leave the board looping in its bootloader until it is power cycled. The overlay holds the camera in reset on every orderly shutdown, which covers OTA, restart, and API reboots. Turn the `Camera Enable` switch off before flashing over USB, since the reset after a USB flash is not an orderly one. Turning it back on resets the sensor's registers, so restart afterwards. +#### M5Stack Dial + +- [Template][m5stack-dial] for the [M5Stack Dial][m5stack-shop-dial-link], an ESP32-S3 StampS3 with 8MB Quad Flash and no PSRAM. It carries a 240x240 round capacitive touch display, a rotary encoder, an RFID reader, and a passive buzzer. The [Dial docs][m5stack-docs-dial-link] carry the pin map the template header repeats. +- Includes the display, the touchscreen, and the backlight as a dimmable light. +- Includes the encoder as a step count, the front button that pressing the screen down operates, and the StampS3 BOOT button. +- Includes the RTC written through from Home Assistant time, and the RFID reader publishing each tag UID as text. +- Includes the buzzer as a PWM output, and the StampS3 RGB LED through the [RGB LED status][rgb-led-status] package. +- Includes a Power Off button, which releases the power hold pin and turns off a board running on its battery. +- Optional substitutions: + - `backlight_brightness`: `80%` by default, the level the backlight turns on at during boot. +- Flash over USB-C. The port is the chip's native USB with no UART bridge, so no adapter is needed. +- The display carries no lambda, so a device draws on it with [LVGL][esphome-components-lvgl-link], as the [example device][test-m5stack-dial] does. There the encoder changes page and the front button plays a beep. +- Note: a V1 unit confirms USB and OTA flashing, the display, touch, the backlight, the encoder, the front button, the buzzer, and the RTC write. Reading an RFID tag, the power hold on battery, the BOOT button, and the RGB LED are untested. So is a V1.1 on the StampS3A. +- Note: the battery level is not sensed, because no ADC pin reaches the battery. + ## Devices My deployed devices use the templates and helpers, and are documented in [DEVICES.md][devices]. @@ -350,6 +366,7 @@ Licensed under the MIT License. See [LICENSE][license] for details. [logger]: ./templates/logger.yaml [m5stack-cores3]: ./templates/m5stack-cores3.yaml [m5stack-cores3-camera]: ./templates/m5stack-cores3-camera.yaml +[m5stack-dial]: ./templates/m5stack-dial.yaml [max17048]: ./templates/max17048.yaml [min-version]: ./templates/min-version.yaml [norvi-enet-ae06-r]: ./templates/norvi-enet-ae06-r.yaml @@ -368,6 +385,7 @@ Licensed under the MIT License. See [LICENSE][license] for details. [test]: ./test/ [test-heltec-wifi-lora32-v4-r8]: ./test/heltec-wifi-lora32-v4-r8.yaml [test-m5stack-cores3]: ./test/m5stack-cores3.yaml +[test-m5stack-dial]: ./test/m5stack-dial.yaml [test-norvi-enet-ae06-r]: ./test/norvi-enet-ae06-r.yaml [time]: ./templates/time.yaml [unexpectedmaker-pros3d]: ./templates/unexpectedmaker-pros3d.yaml @@ -424,7 +442,9 @@ Licensed under the MIT License. See [LICENSE][license] for details. [kincony-esp32-sd-card-rtc-ds3231-link]: https://www.kincony.com/esp32-sd-card-rtc-ds3231.html [konnected-products-smart-garage-door-opener-blaq-myq-alternative-link]: https://konnected.io/products/smart-garage-door-opener-blaq-myq-alternative [m5stack-docs-cores3-link]: https://docs.m5stack.com/en/core/CoreS3 +[m5stack-docs-dial-link]: https://docs.m5stack.com/en/core/Dial [m5stack-shop-cores3-link]: https://shop.m5stack.com/products/m5stack-cores3-esp32s3-iotdevelopment-kit +[m5stack-shop-dial-link]: https://shop.m5stack.com/products/m5stack-dial-v1-1 [markdown-all-in-one-link]: https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one [markdownlint-cli2-link]: https://github.com/DavidAnson/markdownlint-cli2 [microair-link]: https://www.microair.net diff --git a/cspell.json b/cspell.json index d9ee2d6..a859acf 100644 --- a/cspell.json +++ b/cspell.json @@ -197,6 +197,7 @@ "reflash", "regen", "resharper", + "RFID", "rhysd", "Rigol", "RMII", diff --git a/templates/m5stack-dial.yaml b/templates/m5stack-dial.yaml new file mode 100644 index 0000000..bc9d372 --- /dev/null +++ b/templates/m5stack-dial.yaml @@ -0,0 +1,259 @@ +# External usage: +# packages: +# device: github://ptr727/ESPHome-Config/templates/m5stack-dial.yaml@main +# - Secrets resolve from a secrets.yaml beside the including config, see secrets._yaml for the names. + +# Required substitutions: +# - device_name - the ESPHome node name. +# - device_friendly_name - the Home Assistant display name. + +# Optional substitutions: +# - backlight_brightness - 80%, the level the backlight turns on at during boot. +# - rgb_led_pin - GPIO21, the StampS3 module's RGB LED, used by rgb-led-status.yaml. + +# M5Stack Dial ESP32-S3 Smart Rotary Knob, V1 on a StampS3, V1.1 on a StampS3A +# ESP32-S3FN8, 8MB Quad Flash, no PSRAM +# https://shop.m5stack.com/products/m5stack-dial-v1-1 +# https://docs.m5stack.com/en/core/Dial + +# Board tested: 2026-10-24, a V1 on a StampS3, USB and OTA flash, display, touch, backlight, encoder, front button, buzzer, RTC write. +# - The RFID reader answers on the bus, and reading a tag is untested. +# - The power hold and the Power Off button are untested, since the bench unit ran on USB. +# - The BOOT button and the RGB LED sit inside the case, so neither is tested. +# - A V1.1 on a StampS3A is untested. + +# The display, the touchscreen and the backlight are configured here, so a device draws with LVGL, see test/m5stack-dial.yaml. +# - The display carries no lambda, so a lambda device extends board_display with lambda, update_interval and auto_clear_enabled true instead. + +# GPIO19, GPIO20 : native USB, so flashing needs no adapter and no UART bridge +# GPIO0 : BOOT button on the StampS3, held at power on for download mode +# GPIO3 : passive buzzer +# GPIO21 : RGB LED on the StampS3, inside the case +# GPIO40, GPIO41 : rotary encoder A and B +# GPIO42 : front button, pressing the whole screen down +# GPIO46 : power hold, see below + +# Power hold +# - On battery the front button or the RTC alarm powers the board on, and the board stays on only while GPIO46 is high. +# - The Power Off button drives it low, which turns a board on battery off and does nothing on USB or the DC input. +# - Battery level is not sensed, since no ADC pin reaches the battery. + +# Internal I2C bus +# GPIO11 : sda +# GPIO12 : scl +# - 0x38 FT3267 touch, 0x51 BM8563 RTC, 0x28 WS1850S RFID reader. + +# SPI bus, the LCD only +# GPIO6 : sclk +# GPIO5 : mosi +# GPIO4 : LCD dc +# GPIO7 : LCD cs +# GPIO8 : LCD reset, also wired to the WS1850S reset +# GPIO9 : LCD backlight, PWM + +# GC9A01 240x240 round IPS +# GPIO14 : touch interrupt +# GPIO10 : WS1850S interrupt, unclaimed here + +# Grove ports, left to the device +# Port A : GPIO13 sda, GPIO15 scl +# Port B : GPIO1, GPIO2 + +# https://esphome.io/components/substitutions +substitutions: + backlight_brightness: 80% + # Used by rgb-led-status.yaml + rgb_led_pin: GPIO21 + +# https://esphome.io/components/esphome +esphome: + name: ${device_name} + friendly_name: ${device_friendly_name} + + # https://esphome.io/components/esphome#esphome-on_boot + on_boot: + - priority: 600 + then: + - light.turn_on: + id: lcd_backlight + brightness: ${backlight_brightness} + + # The RTC sets the clock once at boot, so it never overwrites a network time source later. + - bm8563.read_time: rtc_time + +# https://esphome.io/components/esp32 +esp32: + # PlatformIO carries no Dial or StampS3 board definition, and the DevKitC-1 profile matches the module. + # https://docs.platformio.org/en/latest/boards/espressif32/esp32-s3-devkitc-1.html + board: esp32-s3-devkitc-1 + + # The board definition does not set this, ESPHome defaults to 4MB. + flash_size: 8MB + + # https://esphome.io/components/esp32#esp-idf-framework + framework: + type: esp-idf + +# https://esphome.io/components/packages#local-packages +packages: + common: !include common.yaml + temperature: !include temperature.yaml + rgbledstatus: !include rgb-led-status.yaml + +# USB is the chip's own port, with no UART bridge on this board. +# https://esphome.io/components/logger +logger: + hardware_uart: USB_SERIAL_JTAG # UART0, UART1, USB_CDC, USB_SERIAL_JTAG + +# A list entry rather than the mapping shorthand, so a device's own bus merges instead of replacing this. +# https://esphome.io/components/i2c +i2c: + - id: board_i2c + sda: GPIO11 + scl: GPIO12 + scan: true + frequency: 400kHz + +# https://esphome.io/components/spi +spi: + - id: board_spi + clk_pin: GPIO6 + mosi_pin: GPIO5 + +# https://esphome.io/components/output +output: + + # https://esphome.io/components/output/ledc + - platform: ledc + id: lcd_backlight_output + pin: GPIO9 + + # https://esphome.io/components/output/ledc + - platform: ledc + id: buzzer_output + pin: + number: GPIO3 # Strapping pin, fixed board wiring, so the warning is acknowledged. + ignore_strapping_warning: true + +# https://esphome.io/components/light/monochromatic +light: + - platform: monochromatic + name: "Backlight" + id: lcd_backlight + output: lcd_backlight_output + default_transition_length: 250ms + restore_mode: ALWAYS_OFF + +# https://esphome.io/components/switch/gpio +switch: + - platform: gpio + id: power_hold + pin: + number: GPIO46 # Strapping pin, fixed board wiring, so the warning is acknowledged. + ignore_strapping_warning: true + restore_mode: ALWAYS_ON + internal: true + +# https://esphome.io/components/button/template +button: + - platform: template + name: "Power Off" + id: power_off + entity_category: config + on_press: + - switch.turn_off: power_hold + +# https://esphome.io/components/binary_sensor/gpio +binary_sensor: + - platform: gpio + name: "Button" + id: front_button + pin: + number: GPIO42 + inverted: true + mode: + input: true + pullup: true + + - platform: gpio + name: "Boot Button" + id: boot_button + pin: + number: GPIO0 # Strapping pin, fixed board wiring, so the warning is acknowledged. + ignore_strapping_warning: true + inverted: true + mode: + input: true + pullup: true + entity_category: diagnostic + +# https://esphome.io/components/sensor/rotary_encoder +sensor: + - platform: rotary_encoder + name: "Encoder" + id: board_encoder + pin_a: + number: GPIO40 + mode: + input: true + pullup: true + pin_b: + number: GPIO41 + mode: + input: true + pullup: true + resolution: 1 + +# https://esphome.io/components/display/mipi_spi +display: + - platform: mipi_spi + id: board_display + spi_id: board_spi + model: GC9A01A + dc_pin: GPIO4 + cs_pin: GPIO7 + reset_pin: GPIO8 + invert_colors: true + data_rate: 40MHz + auto_clear_enabled: false + update_interval: never + +# https://esphome.io/components/touchscreen/ft5x06 +touchscreen: + - platform: ft5x06 + id: board_touch + i2c_id: board_i2c + address: 0x38 + display: board_display + interrupt_pin: GPIO14 + +# https://esphome.io/components/time +time: + + # Home Assistant time writes through to the RTC, so the clock survives a boot with no Home Assistant. + - id: !extend homeassistant_time + on_time_sync: + - bm8563.write_time: rtc_time + + # https://esphome.io/components/time/bm8563 + - platform: bm8563 + id: rtc_time + i2c_id: board_i2c + update_interval: never + +# The WS1850S answers as an MFRC522, and the tag UID reaches Home Assistant as text. +# https://esphome.io/components/binary_sensor/rc522 +rc522_i2c: + - id: board_rfid + i2c_id: board_i2c + address: 0x28 + on_tag: + - text_sensor.template.publish: + id: rfid_tag + state: !lambda return x; + +# https://esphome.io/components/text_sensor/template +text_sensor: + - platform: template + name: "RFID Tag" + id: rfid_tag diff --git a/test/m5stack-dial.yaml b/test/m5stack-dial.yaml new file mode 100644 index 0000000..fbb342b --- /dev/null +++ b/test/m5stack-dial.yaml @@ -0,0 +1,354 @@ +# CI compile-test for templates/m5stack-dial.yaml. +# The LVGL pages exercise each part of the board, so a flashed unit shows what works. +# - Turning the encoder changes page, and pressing the screen down plays a beep. +substitutions: + device_name: m5stack-dial-test + device_friendly_name: "M5Stack Dial Test" + +packages: + device: !include ../templates/m5stack-dial.yaml + +# The bench unit has no Home Assistant, so SNTP sets the clock and writes it through to the RTC. +# - Each source names itself on the status page, so a boot with no network shows the RTC restoring the time. +# https://esphome.io/components/time +time: + - id: !extend homeassistant_time + on_time_sync: + - lvgl.label.update: + id: status_source + text: "Time from Home Assistant" + + - id: !extend rtc_time + on_time_sync: + - lvgl.label.update: + id: status_source + text: "Time from RTC" + + # https://esphome.io/components/time/sntp + - platform: sntp + id: sntp_time + on_time_sync: + - bm8563.write_time: rtc_time + - lvgl.label.update: + id: status_source + text: "Time from SNTP, RTC written" + +# https://esphome.io/components/rtttl +rtttl: + id: board_rtttl + output: buzzer_output + +# The same beep as a button entity, so the buzzer can be tested without touching the board. +# https://esphome.io/components/button/template +button: + - platform: template + name: "Beep" + on_press: + - rtttl.play: + rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + +# A press or a turn on a dark screen only wakes it. +# https://esphome.io/components/binary_sensor +binary_sensor: + - id: !extend front_button + on_press: + - if: + condition: + light.is_off: lcd_backlight + then: + - light.turn_on: lcd_backlight + else: + - rtttl.play: + rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + +# https://esphome.io/components/sensor/rotary_encoder +sensor: + - id: !extend board_encoder + on_clockwise: + - if: + condition: + light.is_off: lcd_backlight + then: + - light.turn_on: lcd_backlight + else: + - lvgl.page.next: + animation: MOVE_LEFT + time: 200ms + on_anticlockwise: + - if: + condition: + light.is_off: lcd_backlight + then: + - light.turn_on: lcd_backlight + else: + - lvgl.page.previous: + animation: MOVE_RIGHT + time: 200ms + +# https://esphome.io/components/text_sensor +text_sensor: + - id: !extend rfid_tag + on_value: + - lvgl.label.update: + id: inputs_rfid + text: + format: "Tag %s" + args: ["x.c_str()"] + +# Coordinates land in the log, so a tap can be checked against the console. +# https://esphome.io/components/touchscreen +touchscreen: + - id: !extend board_touch + on_touch: + - logger.log: + format: "Touch at %d, %d, raw %d, %d" + args: [touch.x, touch.y, touch.x_raw, touch.y_raw] + on_update: + - lambda: |- + if (touches.empty()) return; + const int x = touches[0].x - lv_obj_get_width(id(touch_dot)) / 2; + const int y = touches[0].y - lv_obj_get_height(id(touch_dot)) / 2; + lv_obj_set_pos(id(touch_dot), x, y); + lv_label_set_text_fmt(id(touch_coords), "%d, %d", touches[0].x, touches[0].y); + on_release: + - if: + condition: + light.is_off: lcd_backlight + then: + - light.turn_on: lcd_backlight + +# LVGL sleeps once the backlight is off, whatever turned it off, and the arc follows the backlight. +# https://esphome.io/components/light +light: + - id: !extend lcd_backlight + on_turn_on: + - lvgl.resume: + on_turn_off: + - lvgl.pause: + on_state: + - lvgl.arc.update: + id: controls_backlight + value: !lambda return id(lcd_backlight).remote_values.get_brightness() * 100.0f; + +# https://esphome.io/components/lvgl +lvgl: + displays: [board_display] + touchscreens: [board_touch] + on_idle: + - timeout: 60s + then: + - light.turn_off: lcd_backlight + # The round screen shows the middle of each page, so every page but the touch one centers a single column. + pages: + + - id: page_status + layout: + type: flex + flex_flow: column + flex_align_main: center + flex_align_cross: center + flex_align_track: center + widgets: + - label: + id: status_clock + text_font: montserrat_28 + text: "--:--:--" + - label: + id: status_date + text: "----------" + - label: + id: status_ip + text: "IP --" + - label: + id: status_source + text: "Time not set" + - label: + id: status_uptime + text: "Up --" + + - id: page_inputs + layout: + type: flex + flex_flow: column + flex_align_main: center + flex_align_cross: center + flex_align_track: center + widgets: + - arc: + id: inputs_encoder_arc + width: 120 + height: 120 + start_angle: 0 + end_angle: 360 + min_value: 0 + max_value: 16 + value: 0 + clickable: false + widgets: + - label: + id: inputs_encoder + align: center + text_font: montserrat_28 + text: "0" + - label: + id: inputs_buttons + text: "Button -- Boot --" + - label: + id: inputs_rfid + text: "Tag --" + + - id: page_touch + scrollable: false + pad_all: 0 + widgets: + - label: + id: touch_coords + align: center + y: -36 + text: "Touch the screen" + # Targets 30 pixels in from the edge on each axis and one at the center, for checking the touch mapping. + - obj: + id: touch_target_top + align: top_mid + y: 24 + width: 12 + height: 12 + radius: 6 + bg_opa: transp + border_width: 2 + border_color: 0x0080FF + clickable: false + - obj: + id: touch_target_right + align: right_mid + x: -24 + width: 12 + height: 12 + radius: 6 + bg_opa: transp + border_width: 2 + border_color: 0x0080FF + clickable: false + - obj: + id: touch_target_bottom + align: bottom_mid + y: -24 + width: 12 + height: 12 + radius: 6 + bg_opa: transp + border_width: 2 + border_color: 0x0080FF + clickable: false + - obj: + id: touch_target_left + align: left_mid + x: 24 + width: 12 + height: 12 + radius: 6 + bg_opa: transp + border_width: 2 + border_color: 0x0080FF + clickable: false + - obj: + id: touch_target_center + align: center + width: 12 + height: 12 + radius: 6 + bg_opa: transp + border_width: 2 + border_color: 0x0080FF + clickable: false + - obj: + id: touch_dot + width: 20 + height: 20 + radius: 10 + bg_color: 0xFF4000 + border_width: 0 + clickable: false + + - id: page_controls + layout: + type: flex + flex_flow: column + flex_align_main: center + flex_align_cross: center + flex_align_track: center + widgets: + - arc: + id: controls_backlight + width: 160 + height: 160 + adjustable: true + min_value: 10 + max_value: 100 + value: 80 + on_release: + - light.turn_on: + id: lcd_backlight + brightness: !lambda return x / 100.0; + widgets: + - button: + id: controls_beep + align: center + widgets: + - label: + text: "Beep" + on_click: + - rtttl.play: + rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + +# The labels redraw on a timer rather than per entity. +# https://esphome.io/components/interval +interval: + - interval: 1s + then: + - if: + condition: + time.has_time: + id: sntp_time + then: + - lvgl.label.update: + id: status_clock + text: + time_format: "%H:%M:%S" + time: sntp_time + - lvgl.label.update: + id: status_date + text: + time_format: "%a %Y-%m-%d" + time: sntp_time + - lvgl.label.update: + id: status_ip + text: + format: "IP %s" + args: ["id(wifi_ip).state.c_str()"] + - lvgl.label.update: + id: status_uptime + text: + format: "Up %u s" + args: ["(unsigned) (millis() / 1000)"] + + # The inputs redraw faster than the labels, so a turn or a press shows at once. + - interval: 100ms + then: + - lvgl.label.update: + id: inputs_encoder + text: + format: "%.0f" + args: ["id(board_encoder).state"] + - lvgl.arc.update: + id: inputs_encoder_arc + value: !lambda |- + const int v = (int) id(board_encoder).state % 16; + return v < 0 ? v + 16 : v; + - lvgl.label.update: + id: inputs_buttons + text: + format: "Button %s Boot %s" + args: + - "id(front_button).state ? \"on\" : \"off\"" + - "id(boot_button).state ? \"on\" : \"off\"" From e4621a16668ef1f72d7f43a7a2f2bfcf4083f5dc Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 24 Oct 2026 20:35:49 -0700 Subject: [PATCH 2/4] Fix Dial Review Findings Before the First Push Read the backlight target state for wakes, count encoder turns as LVGL activity, move the buzzer onto its own LEDC timer, redraw the inputs per event rather than on a 100ms poll, fire a Home Assistant tag event per scan, and correct the power hold notes. Co-Authored-By: Claude Opus 5.5 --- OPERATIONS.md | 3 +- README.md | 4 +- templates/m5stack-dial.yaml | 19 ++++++-- test/m5stack-dial.yaml | 86 +++++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 39 deletions(-) diff --git a/OPERATIONS.md b/OPERATIONS.md index 75a0bd1..b2e178d 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -333,7 +333,8 @@ Do not `!remove` blocks from Apollo's package without checking what depends on t [`templates/m5stack-dial.yaml`][m5stack-dial-template] is a board template, with every part on the StampS3 GPIO pins rather than behind an expander or a PMU. -- **GPIO46 is the power hold, an output.** On battery the front button or the RTC alarm powers the board on, and it stays on only while the firmware drives GPIO46 high. ESPHome's device page and M5Stack's own example configure it as a "Hold Button" input instead. That would let a battery-powered board turn itself off. On USB or the DC input the pin does nothing, so the bench unit could not test it. +- **GPIO46 is the power hold, an output.** On battery the front button or the RTC alarm powers the board on, and it stays on only while the firmware drives GPIO46 high. ESPHome's device page configures it as a "Hold Button" input instead, so a board on battery turns off as soon as the button is released. On USB or the DC input the pin does nothing, so the bench unit could not test it. +- **A restart releases the power hold until the switch sets up again.** The reset returns GPIO46 to an input with its pulldown. Whether a board on battery stays powered through an OTA reboot or a restart is untested. - **GPIO8 resets both the LCD and the WS1850S RFID reader.** The display owns the pin, so the reader takes no `reset_pin`. On the bench the reader set up without an error after the display's reset pulse. - **The FT3267 touch controller runs on the `ft5x06` driver.** The boot I2C scan lists only 0x28 and 0x51, but the driver then reads vendor ID 0x11 at 0x38, and touch works. Do not chase the missing address. - **The touch mapping matches the display with no calibration.** Taps on rings 30 pixels in from each edge and at the center landed on them. The reported coordinates sit one pixel below the raw ones. diff --git a/README.md b/README.md index adad480..e4b2c9b 100755 --- a/README.md +++ b/README.md @@ -258,11 +258,13 @@ Shared building-block includes, composed via `packages:` by the device templates - [Template][m5stack-dial] for the [M5Stack Dial][m5stack-shop-dial-link], an ESP32-S3 StampS3 with 8MB Quad Flash and no PSRAM. It carries a 240x240 round capacitive touch display, a rotary encoder, an RFID reader, and a passive buzzer. The [Dial docs][m5stack-docs-dial-link] carry the pin map the template header repeats. - Includes the display, the touchscreen, and the backlight as a dimmable light. - Includes the encoder as a step count, the front button that pressing the screen down operates, and the StampS3 BOOT button. -- Includes the RTC written through from Home Assistant time, and the RFID reader publishing each tag UID as text. +- Includes the RTC written through from Home Assistant time. +- Includes the RFID reader, which fires a Home Assistant tag event on each scan and holds the UID as text while the tag is present. - Includes the buzzer as a PWM output, and the StampS3 RGB LED through the [RGB LED status][rgb-led-status] package. - Includes a Power Off button, which releases the power hold pin and turns off a board running on its battery. - Optional substitutions: - `backlight_brightness`: `80%` by default, the level the backlight turns on at during boot. + - `rgb_led_pin`: `GPIO21` by default, the StampS3 RGB LED. - Flash over USB-C. The port is the chip's native USB with no UART bridge, so no adapter is needed. - The display carries no lambda, so a device draws on it with [LVGL][esphome-components-lvgl-link], as the [example device][test-m5stack-dial] does. There the encoder changes page and the front button plays a beep. - Note: a V1 unit confirms USB and OTA flashing, the display, touch, the backlight, the encoder, the front button, the buzzer, and the RTC write. Reading an RFID tag, the power hold on battery, the BOOT button, and the RGB LED are untested. So is a V1.1 on the StampS3A. diff --git a/templates/m5stack-dial.yaml b/templates/m5stack-dial.yaml index bc9d372..00f27a3 100644 --- a/templates/m5stack-dial.yaml +++ b/templates/m5stack-dial.yaml @@ -22,8 +22,9 @@ # - The BOOT button and the RGB LED sit inside the case, so neither is tested. # - A V1.1 on a StampS3A is untested. -# The display, the touchscreen and the backlight are configured here, so a device draws with LVGL, see test/m5stack-dial.yaml. -# - The display carries no lambda, so a lambda device extends board_display with lambda, update_interval and auto_clear_enabled true instead. +# The display, the touchscreen and the backlight are configured here. +# - The display carries no lambda, so a device draws with LVGL, see test/m5stack-dial.yaml. +# - A lambda device extends board_display with lambda, update_interval and auto_clear_enabled true instead. # GPIO19, GPIO20 : native USB, so flashing needs no adapter and no UART bridge # GPIO0 : BOOT button on the StampS3, held at power on for download mode @@ -36,6 +37,7 @@ # Power hold # - On battery the front button or the RTC alarm powers the board on, and the board stays on only while GPIO46 is high. # - The Power Off button drives it low, which turns a board on battery off and does nothing on USB or the DC input. +# - A restart releases the pin until the switch sets up again, and whether a board on battery survives that is untested. # - Battery level is not sensed, since no ADC pin reaches the battery. # Internal I2C bus @@ -51,7 +53,7 @@ # GPIO8 : LCD reset, also wired to the WS1850S reset # GPIO9 : LCD backlight, PWM -# GC9A01 240x240 round IPS +# GC9A01 240x240 round IPS, with an FT3267 touch panel # GPIO14 : touch interrupt # GPIO10 : WS1850S interrupt, unclaimed here @@ -128,9 +130,11 @@ output: id: lcd_backlight_output pin: GPIO9 + # Channel 2 is on the second LEDC timer, so a buzzer note never retunes the backlight PWM. # https://esphome.io/components/output/ledc - platform: ledc id: buzzer_output + channel: 2 pin: number: GPIO3 # Strapping pin, fixed board wiring, so the warning is acknowledged. ignore_strapping_warning: true @@ -203,6 +207,7 @@ sensor: input: true pullup: true resolution: 1 + publish_initial_value: true # https://esphome.io/components/display/mipi_spi display: @@ -241,16 +246,22 @@ time: i2c_id: board_i2c update_interval: never -# The WS1850S answers as an MFRC522, and the tag UID reaches Home Assistant as text. +# The WS1850S answers as an MFRC522. +# - Each scan fires a Home Assistant tag event, and the text sensor holds the UID until the tag leaves. # https://esphome.io/components/binary_sensor/rc522 rc522_i2c: - id: board_rfid i2c_id: board_i2c address: 0x28 on_tag: + - homeassistant.tag_scanned: !lambda return x; - text_sensor.template.publish: id: rfid_tag state: !lambda return x; + on_tag_removed: + - text_sensor.template.publish: + id: rfid_tag + state: "" # https://esphome.io/components/text_sensor/template text_sensor: diff --git a/test/m5stack-dial.yaml b/test/m5stack-dial.yaml index fbb342b..a0859f1 100644 --- a/test/m5stack-dial.yaml +++ b/test/m5stack-dial.yaml @@ -38,49 +38,91 @@ rtttl: id: board_rtttl output: buzzer_output +# https://esphome.io/components/script +script: + - id: play_beep + then: + - rtttl.play: + rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + + # The inputs redraw on each turn or press, since every one of them runs this. + - id: show_inputs + then: + - lvgl.label.update: + id: inputs_encoder + text: + format: "%.0f" + args: ["id(board_encoder).state"] + - lvgl.arc.update: + id: inputs_encoder_arc + value: !lambda |- + const int v = (int) id(board_encoder).state % 16; + return v < 0 ? v + 16 : v; + - lvgl.label.update: + id: inputs_buttons + text: + format: "Button %s Boot %s" + args: + - "id(front_button).state ? \"on\" : \"off\"" + - "id(boot_button).state ? \"on\" : \"off\"" + # The same beep as a button entity, so the buzzer can be tested without touching the board. # https://esphome.io/components/button/template button: - platform: template name: "Beep" on_press: - - rtttl.play: - rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + - script.execute: play_beep # A press or a turn on a dark screen only wakes it. +# - The wake reads the backlight's target state, so a press during the fade turns it back on. +# - A turn is LVGL activity, since the encoder is not an LVGL input device and the idle timeout would otherwise ignore it. # https://esphome.io/components/binary_sensor binary_sensor: - id: !extend front_button on_press: + - script.execute: show_inputs - if: condition: - light.is_off: lcd_backlight + lambda: return !id(lcd_backlight).remote_values.is_on(); then: - light.turn_on: lcd_backlight else: - - rtttl.play: - rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + - lambda: lv_display_trigger_activity(nullptr); + - script.execute: play_beep + on_release: + - script.execute: show_inputs + + - id: !extend boot_button + on_press: + - script.execute: show_inputs + on_release: + - script.execute: show_inputs # https://esphome.io/components/sensor/rotary_encoder sensor: - id: !extend board_encoder on_clockwise: + - script.execute: show_inputs - if: condition: - light.is_off: lcd_backlight + lambda: return !id(lcd_backlight).remote_values.is_on(); then: - light.turn_on: lcd_backlight else: + - lambda: lv_display_trigger_activity(nullptr); - lvgl.page.next: animation: MOVE_LEFT time: 200ms on_anticlockwise: + - script.execute: show_inputs - if: condition: - light.is_off: lcd_backlight + lambda: return !id(lcd_backlight).remote_values.is_on(); then: - light.turn_on: lcd_backlight else: + - lambda: lv_display_trigger_activity(nullptr); - lvgl.page.previous: animation: MOVE_RIGHT time: 200ms @@ -93,7 +135,7 @@ text_sensor: id: inputs_rfid text: format: "Tag %s" - args: ["x.c_str()"] + args: ["x.empty() ? \"--\" : x.c_str()"] # Coordinates land in the log, so a tap can be checked against the console. # https://esphome.io/components/touchscreen @@ -113,7 +155,7 @@ touchscreen: on_release: - if: condition: - light.is_off: lcd_backlight + lambda: return !id(lcd_backlight).remote_values.is_on(); then: - light.turn_on: lcd_backlight @@ -298,10 +340,9 @@ lvgl: - label: text: "Beep" on_click: - - rtttl.play: - rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" + - script.execute: play_beep -# The labels redraw on a timer rather than per entity. +# The status labels redraw on a timer, since the clock and the uptime change every second. # https://esphome.io/components/interval interval: - interval: 1s @@ -331,24 +372,3 @@ interval: text: format: "Up %u s" args: ["(unsigned) (millis() / 1000)"] - - # The inputs redraw faster than the labels, so a turn or a press shows at once. - - interval: 100ms - then: - - lvgl.label.update: - id: inputs_encoder - text: - format: "%.0f" - args: ["id(board_encoder).state"] - - lvgl.arc.update: - id: inputs_encoder_arc - value: !lambda |- - const int v = (int) id(board_encoder).state % 16; - return v < 0 ? v + 16 : v; - - lvgl.label.update: - id: inputs_buttons - text: - format: "Button %s Boot %s" - args: - - "id(front_button).state ? \"on\" : \"off\"" - - "id(boot_button).state ? \"on\" : \"off\"" From 83666458b59999512604b36200d1f1acb4834f97 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 24 Oct 2026 20:40:57 -0700 Subject: [PATCH 3/4] Redraw Dial Inputs After the Encoder Publishes Run show_inputs from the encoder's on_value, which fires after the new count is published, and move the buzzer to LEDC channel 7 so a device's own auto-assigned ledc output cannot land on it. Co-Authored-By: Claude Opus 5.5 --- templates/m5stack-dial.yaml | 4 ++-- test/m5stack-dial.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/m5stack-dial.yaml b/templates/m5stack-dial.yaml index 00f27a3..d3622bc 100644 --- a/templates/m5stack-dial.yaml +++ b/templates/m5stack-dial.yaml @@ -130,11 +130,11 @@ output: id: lcd_backlight_output pin: GPIO9 - # Channel 2 is on the second LEDC timer, so a buzzer note never retunes the backlight PWM. + # Channel 7 is on the last LEDC timer, so a buzzer note never retunes the backlight or a device's own ledc output. # https://esphome.io/components/output/ledc - platform: ledc id: buzzer_output - channel: 2 + channel: 7 pin: number: GPIO3 # Strapping pin, fixed board wiring, so the warning is acknowledged. ignore_strapping_warning: true diff --git a/test/m5stack-dial.yaml b/test/m5stack-dial.yaml index a0859f1..6bbc4a6 100644 --- a/test/m5stack-dial.yaml +++ b/test/m5stack-dial.yaml @@ -45,7 +45,7 @@ script: - rtttl.play: rtttl: "beep:d=16,o=6,b=180:c,e,g,c7" - # The inputs redraw on each turn or press, since every one of them runs this. + # The inputs redraw on each count or press, since every one of them runs this. - id: show_inputs then: - lvgl.label.update: @@ -76,7 +76,7 @@ button: # A press or a turn on a dark screen only wakes it. # - The wake reads the backlight's target state, so a press during the fade turns it back on. -# - A turn is LVGL activity, since the encoder is not an LVGL input device and the idle timeout would otherwise ignore it. +# - A press or a turn on a lit screen counts as LVGL activity, since neither the button nor the encoder is an LVGL input device. # https://esphome.io/components/binary_sensor binary_sensor: - id: !extend front_button @@ -102,8 +102,9 @@ binary_sensor: # https://esphome.io/components/sensor/rotary_encoder sensor: - id: !extend board_encoder - on_clockwise: + on_value: - script.execute: show_inputs + on_clockwise: - if: condition: lambda: return !id(lcd_backlight).remote_values.is_on(); @@ -115,7 +116,6 @@ sensor: animation: MOVE_LEFT time: 200ms on_anticlockwise: - - script.execute: show_inputs - if: condition: lambda: return !id(lcd_backlight).remote_values.is_on(); From cca691430b6b02999330d1dbf87f156fc8e176e2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 24 Oct 2026 20:51:26 -0700 Subject: [PATCH 4/4] Clarify That the Dial Encoder Changes Pages Co-Authored-By: Claude Opus 5.5 --- HISTORY.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 963f62b..4fc7fce 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,7 +7,7 @@ ESPHome configuration templates and projects. - Version 2.4: - Added M5Stack CoreS3 support, the first touchscreen template, with an LVGL example device. - Added an M5Stack CoreS3 camera overlay for the GC0308 camera. - - Added M5Stack Dial support, with an LVGL example device the encoder navigates. + - Added M5Stack Dial support, with an LVGL example device that changes pages with the encoder. - Version 2.3: - Changed OTA to encryption only, using the API key, so only the upgrade bridge build still needs the `ota_password` secret. - Fixed the compile warnings the templates and the EasyStart component can address. diff --git a/README.md b/README.md index e4b2c9b..e82e197 100755 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ ESPHome configuration templates and projects. - Added M5Stack CoreS3 support, the first touchscreen template, with an LVGL example device. - Added an M5Stack CoreS3 camera overlay for the GC0308 camera. -- Added M5Stack Dial support, with an LVGL example device the encoder navigates. +- Added M5Stack Dial support, with an LVGL example device that changes pages with the encoder. See [HISTORY.md][history] for the release notes.