M5Stack Dial - ESP32-S3 Smart Rotary Knob

The annoying thing about m5stack devices is they use Grove connectors. So I had to cut up some of the proprietary connectors and add a 1.27mm connector to one end (because that matched the pitch of the connectors on my LD2410).



I found this in my tool box
Don’t know what is this but it’s fits. Thank you

@dgaust , sorry for silly question, but which pins on the ld2410 go to which pins on Port B?

Thank you

RX on the LD2410 goes to G2, TX goes to G1, power is obvious. Then UART is defined as

uart:
  tx_pin: GPIO2
  rx_pin: GPIO1
  baud_rate: 256000
  parity: NONE
  stop_bits: 1


This is how I connected: black wire is a ground, red-5V, white is RX and Yellow - TX.
As soon as I connect LD2410 to M5 dial , display is go off and I hear a peach noise from M5.
I have few more LD2410 in my house and they are working great. Just don’t understand what is going on here, why it’s not working properly. TY

well, from that photo it looks like you’re running 5v to the LD2410 ground pin. The red and black wires need to be swapped on the LD2410

I am an IDIOT. You right, my mistake. Thank you again for your help. I still try to learn some coding to add fan and more lights to M5 dial, so far it’s not easy. If I want to add more lights, how I can add them to this area:

substitutions:
  # Add the ids of the devices you want to control here
  light_control: light.living_room_wall_lights
  climate_control: climate.sensi_08ae19
  cover_control: cover.shades_reversed
  script_delay: 1s

Also ceiling fan ?
Thank you

Really nice idea! I did not think of that, but I like it! :smiley:

It has RTC, so you definitely can.

I have been wanting some sort of physical knob to control volume on my outdoor speakers in the back yard. This will not only do that but could maybe even show artist/track information (?), changing source, as well as many other things, I suppose. I think I will pick one up.

Thanks to everyone contributing to figuring this neat little device out!

This YouTuber ‘Volos Projects’ (video I linked above) seems to be quite good at designing UI and plays with a lot of small devices like this on his channel. His implementation seems to be much more responsive as well. I am sure I will study it at some point, I think he shares his code publicly.

1 Like

Ha! Just came across this project which looks amazing: GitHub - scottbez1/smartknob: Haptic input knob with software-defined endstops and virtual detents and was thinking to dive down the rabbit hole and then I spotted the M5Dial and now, for 36 quid you can get something very similar off-the-shelf! :exploding_head:

I’m working on a light control that can do brightness, color temp, color, and potentially more. Here’s a prototype demo and here’s the code.

m5dial_light_control

I’m running into some problems using ESPHome for this though. The display takes >200ms to render, and it keeps rendering over and over in a loop no mater what I do. This makes it so clunky as to be almost unusable. If anyone knows what might be going on, please let me know.

As it is, I’ll probably abandon the ESPHome code in favor of some straight C++, and maybe even look into M5’s UI Flow.

2 Likes

Hi everyone, thank you all for sharing your work, helped me a lot to move forward in my M5dial project.
I recently faced a new problem: i can’t update my code anymore…


I did some backups before i add anything… none of them are working now :confused:
Here’s the code:

Deprecated Code
See my next post

How big are the images you’re including?

2 Likes

Not so many but maybe they were too big…
I just didn’t think a sec to this point.

It’s OK now, thank you… forced a resize to 120x120px, as this is just for testing.

  - id: img_startup
    file: m5dial_files/cs_smiley.png
    type: RGBA
    resize: 120x120 #added to save flash memory

image

So, after lot of digging in this post and espHome documentation: here’s my code.
Just a base for what i want to do, i only achieved the welcome page.

Code
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: !secret ip_m5stack_dial
    gateway: !secret wifi_gtw
    subnet: !secret wifi_sub
  ap:
    ssid: "m5stack-dial-fallback"
    password: !secret wifi_ap_password

ota:
  password: !secret ota_password

logger:
  level: DEBUG

api:
  encryption:
    key: !secret api_encryption_key

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  on_boot:
    then:
      - light.turn_on: backlight
      - delay: 3s
      - display.page.show: page_01
      - pcf8563.read_time: my_time
      
external_components:
  - source: github://dgaust/esphome@gc9a01
    components: [ gc9a01, ft3267 ]
    refresh: 0s

<<: !include m5dial_files/colors_and_fonts.yaml
<<: !include m5dial_files/images.yaml

spi: #required by display.ili9xxx
  mosi_pin: GPIO5
  clk_pin: GPIO6

i2c:
  - id: bus_internal #required by touchscreen.ft3267
    sda: GPIO11
    scl: GPIO12
    scan: false

rc522_i2c: # RFiD
  i2c_id:  bus_internal
  address: 0x28
  on_tag:
    then:
      - rtttl.play: 'two_short:d=4,o=5,b=100:16e6,16e6'
      - homeassistant.tag_scanned: !lambda 'return x;'

rtttl: # buzzer
  output: rtttl_out
  id: my_rtttl

output:
  - id: lcdbacklight # screen backlight
    platform: ledc
    pin: GPIO9
    min_power: 0
    max_power: 1
  - platform: ledc # buzzer
    pin: GPIO3
    id: rtttl_out

substitutions:
  name: tab-m5stack-dial
  friendly_name: M5Stack Dial
  lux_sensor_id: sensor.ip030_mmw_aqara_living_luminance
  alarm_panel_id: alarm_control_panel.alarme_maison
  gate_sensor_id: binary_sensor.tplt_gate_open_too_long
  windows_group_id: binary_sensor.windows_upstairs

# ======================================================================================
# ======================================================================================
# ================================================================================= time

time:
  - platform: pcf8563
    id: my_time
    address: 0x38
    update_interval: never # repeated synchronization is not necessary unless the external RTC is much more accurate than the internal clock
  - platform: homeassistant
    on_time_sync: # instead try to synchronize via network repeatedly ...
      then:
        - pcf8563.write_time: my_time # ... and update the RTC when the synchronization was successful
        - logger.log: "time synced"

# ======================================================================================
# ======================================================================================
# =============================================================================== lights

light:

  # ------------------------------------------------------------------ backlight
  - platform: monochromatic
    id: backlight
    name: "Backlight"
    output: lcdbacklight
    default_transition_length: 500ms

# ======================================================================================
# ======================================================================================
# ============================================================================== selects

select:
  - platform: template
    name: Page Selector
    id: page_selector
    options:
     - "page_off" # index 0
     - "page_00" # index 1
     - "page_01" # index 2
     - "page_a1" # index 3
     - "page_b1" # index 4
     - "page_c1" # index 5
     - "page_d1" # index 6
    initial_option: "page_00"
    optimistic: true
    on_value:
      - if:
          condition:
            - lambda: 'return id(page_selector).state == "page_off";'
          then:
            - light.turn_off: backlight
          else:
            - if:
                condition:
                  - light.is_off: backlight
                then:
                  - light.turn_on: backlight
                  - display.page.show: page_01
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_00";'
                then:
                  - display.page.show: page_00
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_01";'
                then:
                  - display.page.show: page_01
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_a1";'
                then:
                  - display.page.show: page_a1
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_b1";'
                then:
                  - display.page.show: page_b1
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_c1";'
                then:
                  - display.page.show: page_c1
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_d1";'
                then:
                  - display.page.show: page_d1

# ======================================================================================
# ======================================================================================
# ====================================================================== display + pages

touchscreen:
  platform: ft3267
  on_touch:
    then:
      - logger.log:
          format: 'Touch ID: %d at _____________________ (%d, %d)'
          args: [touch.id, touch.x, touch.y]
      - if:
          condition:
            - light.is_off: backlight
          then:
            - light.turn_on: backlight

display:
  - platform: ili9xxx
    model: gc9a01a
    reset_pin: GPIO8
    id: screen
    cs_pin: GPIO7
    dc_pin: GPIO4
    dimensions:
      height: 240
      width: 240
    on_page_change:
      - to: page_00
        then:
          - select.set:
              id: page_selector
              option: page_00
      - to: page_01
        then:
          - select.set:
              id: page_selector
              option: page_01
      - to: page_a1
        then:
          - select.set:
              id: page_selector
              option: page_a1
      - to: page_b1
        then:
          - select.set:
              id: page_selector
              option: page_b1
      - to: page_c1
        then:
          - select.set:
              id: page_selector
              option: page_c1
      - to: page_d1
        then:
          - select.set:
              id: page_selector
              option: page_d1
    pages:

      # _____________________________________________________ startup page
      - id: page_00
        lambda: |-
          it.image(120, 120, img_00, ImageAlign::CENTER);

      # ___________________________________________________ welcome page
      - id: page_01
        lambda: |-
          // variables =========================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // background draws ==================================================
          it.filled_rectangle(0, 0, halfscreenwidth, halfscreenheight, pri_color);
          it.filled_rectangle(halfscreenwidth, halfscreenheight, 155, 155, pri_color);
          it.filled_rectangle(0, halfscreenheight, halfscreenwidth, 155, pri_color);
          it.filled_rectangle(halfscreenwidth, 0, 155, halfscreenheight, pri_color);     
          it.line(0, halfscreenheight, screenwidth, halfscreenheight, off_color);
          it.line(halfscreenwidth, 0, halfscreenwidth, screenheight, off_color);
          // icons =============================================================
          // alarm icon ----------------------------------------------------
          if (id(alarm_text).state == "armed_away") {
          it.image(65, 75, mdi_shield_lock, ImageAlign::CENTER, nova_color);
          } else if (id(alarm_text).state == "armed_night") {
          it.image(65, 75, mdi_shield_moon, ImageAlign::CENTER, nova_color);
          } else if (id(alarm_text).state == "armed_home") {
          it.image(65, 75, mdi_shield_account, ImageAlign::CENTER, blue_color);
          } else if (id(alarm_text).state == "arming") {
          it.image(65, 75, mdi_shield_sync, ImageAlign::CENTER, text_color);
          } else if (id(alarm_text).state == "pending") {
          it.image(65, 75, mdi_shield_lock_open, ImageAlign::CENTER, yellow_color);
          } else if (id(alarm_text).state == "triggered") {
          it.image(65, 75, mdi_shield_alert, ImageAlign::CENTER, red_color);
          } else {
          it.image(65, 75, mdi_shield_off, ImageAlign::CENTER, off_color);
          }
          // gate icon -----------------------------------------------------
          if (id(gate_text).state == "closed") {
          it.image(175, 75, mdi_gate_closed, ImageAlign::CENTER, text_color);
          } else if (id(gate_text).state == "open") {
          it.image(175, 75, mdi_gate_open, ImageAlign::CENTER, blue_color);
          } else {
          it.image(175, 75, mdi_gate_alert, ImageAlign::CENTER, yellow_color);
          }
          // thermostat icon -----------------------------------------------
          it.image(65, 165, mdi_thermostat, ImageAlign::CENTER, text_color);
          // other icon ----------------------------------------------------
          if (id(windows_text).state == "off") {
          it.image(175, 165, mdi_window_closed, ImageAlign::CENTER, text_color);
          } else {
          it.image(175, 165, mdi_window_open, ImageAlign::CENTER, blue_color);
          }
          // foreground draws ==================================================
          it.filled_regular_polygon(halfscreenwidth, halfscreenheight -220, 144, EDGES_OCTAGON, VARIATION_FLAT_TOP, sec_color);
          it.filled_regular_polygon(halfscreenwidth, halfscreenheight +220, 144, EDGES_OCTAGON, VARIATION_FLAT_TOP, sec_color);
          it.regular_polygon(halfscreenwidth, halfscreenheight -220, 145, EDGES_OCTAGON, VARIATION_FLAT_TOP, off_color);
          it.regular_polygon(halfscreenwidth, halfscreenheight +220, 145, EDGES_OCTAGON, VARIATION_FLAT_TOP, off_color);
          // top text draw =====================================================
          it.strftime(halfscreenwidth, 20, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "%d/%m %X", id(my_time).now());
          // bottom text draw ==================================================
          it.print(halfscreenwidth, screenheight -20, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "\U000F0046 ETEINDRE");

      # ___________________________________________________ alarm page
      - id: page_a1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "A1 PAGE");

      # ___________________________________________________ gate page
      - id: page_b1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "B1 PAGE");

      # ___________________________________________________ thermostat page
      - id: page_c1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "C1 PAGE");

      # ___________________________________________________ windows page
      - id: page_d1
        lambda: |-
          // variables ======================================================
          float screenheight = it.get_height();
          float screenwidth = it.get_width();
          float halfscreenheight = screenheight / 2;
          float halfscreenwidth = screenwidth /2;
          // text draw ==================================================
          it.print(halfscreenwidth, halfscreenheight, id(roboto_16_with_icons), orange_color, TextAlign::CENTER, "D1 PAGE");

# ======================================================================================
# ======================================================================================
# ============================================================================== sensors

sensor:

  # ----------------------------------------------------------------- lux sensor
  - platform: homeassistant
    entity_id: $lux_sensor_id
    name: "Lux Sensor"
    id: sensor_lux
    accuracy_decimals: 0
    on_value:
      - logger.log: 
          format: "$lux_sensor_id: %.0f"
          args: [ 'id(sensor_lux).state' ]
      - if:
          condition:
            - light.is_on: backlight
          then:
            - lambda: |- 
                float lux = id(sensor_lux).state;
                if (lux < 10) { 
                  id(lcdbacklight).set_level(0.1);
                }
                else if (lux < 40) {
                  id(lcdbacklight).set_level(0.3);
                }
                else if (lux < 80) {
                  id(lcdbacklight).set_level(0.5);
                }
                else {
                  id(lcdbacklight).set_level(0.7);
                }

  # ------------------------------------------------------------- rotary encoder
  - platform: rotary_encoder
    name: Rotary Encoder
    id: rotaryencoder
    resolution: 1
    pin_a:
      number: GPIO40
      mode:
       input: true
       pullup: true
    pin_b:
      number: GPIO41
      mode:
       input: true
       pullup: true
    accuracy_decimals: 0
    # accuracy_decimals: 1
    on_clockwise:
      - logger.log: "rotary_encoder : _____________________ turned_clockwise"
      - if:
          <<: &select_off
            condition:
              - lambda: 'return id(page_selector).state == "page_off";'
            then:
              - select.set:
                  id: page_selector
                  option: page_01
    on_anticlockwise:
      - logger.log: "rotary_encoder : _____________________ turned_ANTIclockwise"
      - if:
          <<: *select_off

# ======================================================================================
# ======================================================================================
# ======================================================================= binary sensors

binary_sensor:

  # --------------------------------------------------------------------- button
  - platform: gpio
    pin:
      number: GPIO42
      inverted: true
    name: M5 Button
    on_press:
      - logger.log: "button_pushed _____________________ button_pushed"
      - if:
          condition:
            - lambda: 'return id(page_selector).state == "page_00";'
          then:
            - select.set:
                id: page_selector
                option: page_off
          else:
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_01";'
                then:
                  - select.set:
                      id: page_selector
                      option: page_00
                else:
                  - select.set:
                      id: page_selector
                      option: page_01

# ================================================================= touch inputs
  # -------------------------------------------------------------- startup
  - platform: touchscreen
    id: touch_00
    internal: true
    x_min: 0
    x_max: 240
    y_min: 0
    y_max: 240
    page_id: page_00
    on_press:
      - logger.log: "touch_input OVER page_00 : _____________________ startup"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_01

  # -------------------------------------------------------------- welcome
  - platform: touchscreen
    id: touch_01_tl
    internal: true
    x_min: 0
    x_max: 118
    y_min: 0
    y_max: 118
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ top_left"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_a1
  - platform: touchscreen
    id: touch_01_tr
    internal: true
    x_min: 122
    x_max: 240
    y_min: 0
    y_max: 118
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ top_right"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_b1
  - platform: touchscreen
    id: touch_01_bl
    internal: true
    x_min: 0
    x_max: 118
    y_min: 122
    y_max: 240
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ bottom_left"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_c1
  - platform: touchscreen
    id: touch_01_br
    internal: true
    x_min: 122
    x_max: 240
    y_min: 122
    y_max: 240
    page_id: page_01
    on_press:
      - logger.log: "touch_input OVER page_01 : _____________________ bottom_right"
    on_release:
      - if:
          condition:
            - light.is_on: backlight
          then:
            - display.page.show: page_d1

# ======================================================================================
# ======================================================================================
# ============================================================================== numbers

number:
  - platform: template
    id: color_hue
    initial_value: 0
    min_value: 0
    max_value: 359.9
    step: 0.1
    optimistic: True
  - platform: template
    id: color_saturation
    initial_value: 0
    min_value: 0
    max_value: 100
    step: 0.1
    optimistic: True
  - platform: template
    id: dimmer_value
    initial_value: 0
    min_value: 0
    max_value: 1
    step: 0.001
    optimistic: True

# ======================================================================================
# ======================================================================================
# ============================================================================== scripts

script:
  - id: update_touch_script
    mode: restart
    parameters:
      x: int
      y: int
    then:
      - lambda: |-
          // adjust center or coordinate plane to (120, 120)
          float polar_x = x - 120;
          float polar_y = 120 - y; // Screen y is opposite of cartesian.
          // convert to polar coords
          float r = sqrt(pow(polar_x, 2) + pow(polar_y, 2));
          float theta = atan(polar_y / polar_x);
          if (polar_x < 0) {
            theta += M_PI;  // Adjust atan for quadrants 2 & 3.
          } else if (polar_y < 0) {
            theta += 2 * M_PI;  // Make quadrant 4 value positive.
          }
          // Update dimmer value.
          float min_theta = M_PI * 310 / 180;
          float max_theta = M_PI * 590 / 180;
          // Normalize the relative positioning of the values.
          float test_theta = theta;
          if (test_theta < min_theta) {
            test_theta += M_PI * 2;
          }
          ESP_LOGD("THETA", "%f < %f < %f", min_theta, test_theta, max_theta);

          // page interactions
          if (id(screen).get_active_page() == id(page_00)) {
            id(color_hue).publish_state(360 - theta / M_PI * 180);
            id(color_saturation).publish_state(min(r, (float)100));
          }
          if (id(screen).get_active_page() == id(page_01)) {
            id(color_hue).publish_state(360 - theta / M_PI * 180);
            id(color_saturation).publish_state(min(r, (float)100));
          }
      # - component.update: screen

# ======================================================================================
# ======================================================================================
# ==============================================================================  text sensors

text_sensor:
  - platform: homeassistant
    id: alarm_text
    entity_id: $alarm_panel_id
  - platform: homeassistant
    id: gate_text
    entity_id: $gate_sensor_id
    attribute: esphome_text_sensor
  - platform: homeassistant
    id: windows_text
    entity_id: $windows_group_id

I didn’t find the way to reduce those parts with lambdas… Any guess?
Select component

    on_value:
      - if:
          condition:
            - lambda: 'return id(page_selector).state == "page_off";'
          then:
            - light.turn_off: backlight
          else:
            - if:
                condition:
                  - light.is_off: backlight
                then:
                  - light.turn_on: backlight
                  - display.page.show: page_01
            - if:
                condition:
                  - lambda: 'return id(page_selector).state == "page_00";'
                then:
                  - display.page.show: page_00
            - if:
                condition:
                  - lambda: 'return id(page_selector)....... etc etc etc

Display component:

    on_page_change:
      - to: page_00
        then:
          - select.set:
              id: page_selector
              option: page_00
      - to: page_01
        then:
          - select.set:
              id: page_selector
              option: page_01
      - to: page_a1.......  etc etc etc
1 Like

In the video I shared a few posts up, the guy ‘Volos Projects’ has made not only a nice looking, but very responsive UI. I am not sure how he implemented it, but I am pretty sure he shares his code somewhere, maybe you could look at it and see?

This videos are only in German but the github repo is in english:
SmartHome-yourself/m5-dial-for-esphome
But has added touch switch between entities (swiping left and right) and switch between modes (swiping up and down)

https://www.youtube.com/watch?v=4dE7YONEYVk <= lights
https://www.youtube.com/watch?v=7TZfAWmYNzo <= update with more entities

1 Like

Hello, has anyone already managed to operate the NFC tag via the M5Stack dial? I would like to perform this option with him. Thanks in advance

I’ve now started looking at the lvgl component that is in development for esphome

It’s only early days of bringing things across, but it’s a lot faster than the lambda display method that we needed to use

edit: updated with a loading screen and a little more visible due to colour changes (still can’t work out how to change the blue colour for the arc :frowning:)

edit 2: I figured out how to change the blue colour 32s after posting the initial edit :man_facepalming:)

4 Likes

Yeah… Look at post #48!

that’s awesome ! could you share the code ? i have been looking on how to implement a squareline studio ui to the M5 for so long and feel like you have found the way !