
Chain DualKey is a programmable dual-key input development board equipped with the ESP32-S3FN8 main control chip. The front integrates 2 hot-swappable blue switch mechanical keyboard keys and 2 programmable RGB LEDs, providing excellent interactive feedback. It has a built-in 350mAh lithium battery, combining with a low-power design for good battery life. The product comes with pre-installed Chain macro keyboard firmware, supports USB / BLE connections, and can emulate HID input devices. After the device is powered on, you can connect to the device's AP hotspot and configure the HID function mapping for the local device or expansion nodes via the built-in web page to achieve various control functions. This development board adopts the M5Stack Chain series expandable design, featuring two HY2.0-4P expansion ports that support lateral expansion and connection to other sensor devices. With the USB-OTG peripheral function built into ESP32-S3, it is suitable for smart home, keyboard peripherals, macro keyboards, and other scenarios.
Step 1. Create New Device

Step 2. Create Device Name
CONTINUE.
New Device Setup.
NEXT.
Step 3. Choose Device Type
ESP32-S3.
SKIP.
Step 4. Start Editing YAML File
EDIT. We can customize device functionality through YAML files.
Direction Selection
Choose the appropriate uart_id based on which side of the Master the expansion sensor is connected:
ID Numbering Rules
The chain_id represents the position of the expansion sensor relative to the Master:
Configuration Example
uart_id: chain_uart_leftchain_id: 1uart:
- id: chain_uart_right
tx_pin: GPIO6
rx_pin: GPIO5
baud_rate: 115200
- id: chain_uart_left
tx_pin: GPIO48
rx_pin: GPIO47
baud_rate: 115200
sensor:
- platform: adc
pin: GPIO10
name: "ADC_BAT"
update_interval: 1s
- platform: adc
pin: GPIO2
name: "ADC_VBUS"
update_interval: 1s
- platform: adc
pin: GPIO9
name: "ADC_CHARGE"
update_interval: 1s
output:
- platform: gpio
id: pwr_en
pin: GPIO40
light:
- platform: esp32_rmt_led_strip
id: key_light_raw
internal: true
pin: GPIO21
num_leds: 2
chipset: ws2812
rgb_order: GRB
restore_mode: ALWAYS_OFF
- platform: partition
name: "Key Light 1"
id: key_light_1
segments:
- id: key_light_raw
from: 0
to: 0
- platform: partition
name: "Key Light 2"
id: key_light_2
segments:
- id: key_light_raw
from: 1
to: 1
binary_sensor:
- platform: gpio
name: "KEY_2"
pin:
number: GPIO17
inverted: true
mode: INPUT_PULLUP
filters:
- delayed_on: 10ms
- delayed_off: 10ms
on_press:
- light.turn_on:
id: key_light_2
transition_length: 0ms
on_release:
- light.turn_off: key_light_2
- platform: gpio
name: "KEY_1"
pin:
number: GPIO0
inverted: true
mode: INPUT_PULLUP
filters:
- delayed_on: 10ms
- delayed_off: 10ms
on_press:
- light.turn_on:
id: key_light_1
transition_length: 0ms
on_release:
- light.turn_off: key_light_1
- platform: gpio
name: "SWITCH_1"
pin:
number: GPIO7
mode: INPUT
- platform: gpio
name: "SWITCH_2"
pin:
number: GPIO8
mode: INPUT external_components:
- source: github://m5stack/esphome-yaml/components
components: [m5stack_chain_key]
refresh: 0s
binary_sensor:
- platform: m5stack_chain_key
id: chain_key_1
name: "Chain Key Button"
uart_id: xx
chain_id: xx
update_interval: 50ms
output:
- platform: m5stack_chain_key
id: chain_key_rgb_r
chain_key_id: chain_key_1
channel: rgb_red
- platform: m5stack_chain_key
id: chain_key_rgb_g
chain_key_id: chain_key_1
channel: rgb_green
- platform: m5stack_chain_key
id: chain_key_rgb_b
chain_key_id: chain_key_1
channel: rgb_blue
light:
- platform: rgb
name: "Key RGB"
red: chain_key_rgb_r
green: chain_key_rgb_g
blue: chain_key_rgb_b external_components:
- source: github://m5stack/esphome-yaml/components
components: [m5stack_chain_angle]
refresh: 0s
sensor:
- platform: m5stack_chain_angle
id: chain_angle_1
name: "Chain Angle"
uart_id: xx
chain_id: xx
update_interval: 50ms
output:
- platform: m5stack_chain_angle
id: chain_angle_rgb_r
chain_angle_id: chain_angle_1
channel: rgb_red
- platform: m5stack_chain_angle
id: chain_angle_rgb_g
chain_angle_id: chain_angle_1
channel: rgb_green
- platform: m5stack_chain_angle
id: chain_angle_rgb_b
chain_angle_id: chain_angle_1
channel: rgb_blue
light:
- platform: rgb
name: "Angle RGB"
red: chain_angle_rgb_r
green: chain_angle_rgb_g
blue: chain_angle_rgb_b external_components:
- source: github://m5stack/esphome-yaml/components
components: [m5stack_chain_encoder]
refresh: 0s
sensor:
- platform: m5stack_chain_encoder
id: chain_encoder_1
name: "Chain Encoder"
uart_id: xx
chain_id: xx
update_interval: 100ms
output:
- platform: m5stack_chain_encoder
id: chain_encoder_rgb_r
chain_encoder_id: chain_encoder_1
channel: rgb_red
- platform: m5stack_chain_encoder
id: chain_encoder_rgb_g
chain_encoder_id: chain_encoder_1
channel: rgb_green
- platform: m5stack_chain_encoder
id: chain_encoder_rgb_b
chain_encoder_id: chain_encoder_1
channel: rgb_blue
light:
- platform: rgb
name: "Encoder RGB"
red: chain_encoder_rgb_r
green: chain_encoder_rgb_g
blue: chain_encoder_rgb_b
binary_sensor:
- platform: m5stack_chain_encoder
name: "Encoder Button"
chain_encoder_id: chain_encoder_1 external_components:
- source: github://m5stack/esphome-yaml/components
components: [m5stack_chain_joystick]
refresh: 0s
sensor:
- platform: m5stack_chain_joystick
id: chain_joystick_x
name: "Chain Joystick X"
uart_id: xx
chain_id: xx
axis: x
update_interval: 50ms
- platform: m5stack_chain_joystick
name: "Chain Joystick Y"
uart_id: xx
chain_id: xx
axis: y
update_interval: 50ms
output:
- platform: m5stack_chain_joystick
id: chain_joystick_rgb_r
chain_joystick_id: chain_joystick_x
channel: rgb_red
- platform: m5stack_chain_joystick
id: chain_joystick_rgb_g
chain_joystick_id: chain_joystick_x
channel: rgb_green
- platform: m5stack_chain_joystick
id: chain_joystick_rgb_b
chain_joystick_id: chain_joystick_x
channel: rgb_blue
light:
- platform: rgb
name: "Joystick RGB"
red: chain_joystick_rgb_r
green: chain_joystick_rgb_g
blue: chain_joystick_rgb_b
binary_sensor:
- platform: m5stack_chain_joystick
name: "Joystick Button"
chain_joystick_id: chain_joystick_x external_components:
- source: github://m5stack/esphome-yaml/components
components: [m5stack_chain_tof]
refresh: 0s
sensor:
- platform: m5stack_chain_tof
id: chain_tof_1
name: "Chain ToF"
uart_id: xx
chain_id: xx
update_interval: 100ms
output:
- platform: m5stack_chain_tof
id: chain_tof_rgb_r
m5stack_chain_tof_id: chain_tof_1
channel: rgb_red
- platform: m5stack_chain_tof
id: chain_tof_rgb_g
m5stack_chain_tof_id: chain_tof_1
channel: rgb_green
- platform: m5stack_chain_tof
id: chain_tof_rgb_b
m5stack_chain_tof_id: chain_tof_1
channel: rgb_blue
light:
- platform: rgb
name: "ToF RGB"
red: chain_tof_rgb_r
green: chain_tof_rgb_g
blue: chain_tof_rgb_b The following code example is configured according to the connection order shown in the diagram above.

Modules used: Chain Angle, Chain Encoder, Chain ToF, Chain Joystick, Chain Key.
This block imports all required external components for the Chain series modules. If you do not use a certain module (for example, Chain Encoder or Chain ToF), you can remove it from the components list.
external_components:
- source: github://m5stack/esphome-yaml/components
components: [m5stack_chain_angle,m5stack_chain_encoder,m5stack_chain_tof,m5stack_chain_joystick,m5stack_chain_key]
refresh: 0s Modules used: Shared UART bus for all Chain series modules on left/right HY2.0 ports.
captive_portal:
uart:
- id: chain_uart_right
tx_pin: GPIO6
rx_pin: GPIO5
baud_rate: 115200
- id: chain_uart_left
tx_pin: GPIO48
rx_pin: GPIO47
baud_rate: 115200 Modules used: Chain Encoder, Chain Angle, Chain ToF, Chain Joystick (X/Y), DualKey battery ADC sensors.
In this example, the Master is connected to the following Chain modules:
In addition, the built‑in battery-related ADC channels (BAT, VBUS, CHARGE) are also enabled as sensors.
sensor:
- platform: m5stack_chain_encoder
id: chain_encoder_1
name: "Encoder"
uart_id: chain_uart_right
chain_id: 1
update_interval: 100ms
- platform: m5stack_chain_tof
id: chain_tof_1
name: "ToF Distance"
uart_id: chain_uart_right
chain_id: 3
update_interval: 100ms
- platform: m5stack_chain_angle
id: chain_angle_1
name: "Angle"
uart_id: chain_uart_right
chain_id: 2
update_interval: 100ms
- platform: m5stack_chain_joystick
id: chain_joystick_x
name: "Joystick X"
uart_id: chain_uart_left
chain_id: 1
axis: x
update_interval: 100ms
- platform: m5stack_chain_joystick
name: "Joystick Y"
uart_id: chain_uart_left
chain_id: 1
axis: y
update_interval: 100ms
- platform: adc
pin: GPIO10
name: "ADC_BAT"
update_interval: 1s
- platform: adc
pin: GPIO2
name: "ADC_VBUS"
update_interval: 1s
- platform: adc
pin: GPIO9
name: "ADC_CHARGE"
update_interval: 1s Modules used: RGB LEDs on Chain Encoder, Chain Key, Chain Joystick, Chain Angle, Chain ToF, plus DualKey power control.
output:
- platform: gpio
id: pwr_en
pin: GPIO40
- platform: m5stack_chain_encoder
id: chain_encoder_rgb_r
chain_encoder_id: chain_encoder_1
channel: rgb_red
- platform: m5stack_chain_encoder
id: chain_encoder_rgb_g
chain_encoder_id: chain_encoder_1
channel: rgb_green
- platform: m5stack_chain_encoder
id: chain_encoder_rgb_b
chain_encoder_id: chain_encoder_1
channel: rgb_blue
- platform: m5stack_chain_key
id: chain_key_rgb_r
chain_key_id: chain_key_1
channel: rgb_red
- platform: m5stack_chain_key
id: chain_key_rgb_g
chain_key_id: chain_key_1
channel: rgb_green
- platform: m5stack_chain_key
id: chain_key_rgb_b
chain_key_id: chain_key_1
channel: rgb_blue
- platform: m5stack_chain_joystick
id: chain_joystick_rgb_r
chain_joystick_id: chain_joystick_x
channel: rgb_red
- platform: m5stack_chain_joystick
id: chain_joystick_rgb_g
chain_joystick_id: chain_joystick_x
channel: rgb_green
- platform: m5stack_chain_joystick
id: chain_joystick_rgb_b
chain_joystick_id: chain_joystick_x
channel: rgb_blue
- platform: m5stack_chain_angle
id: chain_angle_rgb_r
chain_angle_id: chain_angle_1
channel: rgb_red
- platform: m5stack_chain_angle
id: chain_angle_rgb_g
chain_angle_id: chain_angle_1
channel: rgb_green
- platform: m5stack_chain_angle
id: chain_angle_rgb_b
chain_angle_id: chain_angle_1
channel: rgb_blue
- platform: m5stack_chain_tof
id: chain_tof_rgb_r
m5stack_chain_tof_id: chain_tof_1
channel: rgb_red
- platform: m5stack_chain_tof
id: chain_tof_rgb_g
m5stack_chain_tof_id: chain_tof_1
channel: rgb_green
- platform: m5stack_chain_tof
id: chain_tof_rgb_b
m5stack_chain_tof_id: chain_tof_1
channel: rgb_blue Here the pwr_en GPIO output is used to control the power supply for the Chain expansion bus. Usually this output needs to be turned on so that the connected Chain modules can work normally.
Modules used: DualKey WS2812 key LEDs and RGB indicator LEDs on each Chain module.
light:
- platform: esp32_rmt_led_strip
id: key_light_raw
internal: true
pin: GPIO21
num_leds: 2
chipset: ws2812
rgb_order: GRB
restore_mode: ALWAYS_OFF
- platform: partition
name: "Key1 LED"
id: key_light_1
segments:
- id: key_light_raw
from: 1
to: 1
- platform: partition
name: "Key2 LED"
id: key_light_2
segments:
- id: key_light_raw
from: 0
to: 0
- platform: rgb
name: "Encoder RGB"
red: chain_encoder_rgb_r
green: chain_encoder_rgb_g
blue: chain_encoder_rgb_b
- platform: rgb
name: "Key RGB"
red: chain_key_rgb_r
green: chain_key_rgb_g
blue: chain_key_rgb_b
- platform: rgb
name: "Joystick RGB"
red: chain_joystick_rgb_r
green: chain_joystick_rgb_g
blue: chain_joystick_rgb_b
- platform: rgb
name: "Angle RGB"
red: chain_angle_rgb_r
green: chain_angle_rgb_g
blue: chain_angle_rgb_b
- platform: rgb
name: "ToF RGB"
red: chain_tof_rgb_r
green: chain_tof_rgb_g
blue: chain_tof_rgb_b This section configures the per-key RGB backlight. key_light_raw represents the underlying LED strip, while key_light_1 and key_light_2 map each LED to the corresponding key so you can control them independently.
Modules used: DualKey mechanical keys and side switches, plus buttons on Chain Key, Chain Encoder, Chain Joystick.
This part defines all key-related inputs: the two mechanical keys (KEY 1, KEY 2) with RGB feedback, one Chain Key module on the bus, and the two side switches (SWITCH 1, SWITCH 2). You can rename these entities in Home Assistant according to your use case.
binary_sensor:
- platform: gpio
name: "KEY 2"
pin:
number: GPIO17
inverted: true
mode: INPUT_PULLUP
filters:
- delayed_on: 10ms
- delayed_off: 10ms
on_press:
- light.turn_on:
id: key_light_2
transition_length: 0ms
on_release:
- light.turn_off: key_light_2
- platform: gpio
name: "KEY 1"
pin:
number: GPIO0
inverted: true
mode: INPUT_PULLUP
filters:
- delayed_on: 10ms
- delayed_off: 10ms
on_press:
- light.turn_on:
id: key_light_1
transition_length: 0ms
on_release:
- light.turn_off: key_light_1
- platform: m5stack_chain_key
id: chain_key_1
name: "Key Module Button"
uart_id: chain_uart_left
chain_id: 2
update_interval: 50ms
- platform: m5stack_chain_encoder
name: "Encoder Button"
chain_encoder_id: chain_encoder_1
- platform: m5stack_chain_joystick
name: "Joystick Button"
chain_joystick_id: chain_joystick_x
- platform: gpio
name: "SWITCH 1"
pin:
number: GPIO7
mode: INPUT
- platform: gpio
name: "SWITCH 2"
pin:
number: GPIO8
mode: INPUT INSTALL again to flash and wait for it to complete.
SAVE and INSTALL in the top-right corner, then choose Manual Download in the popup.
Factory format(Previously Modern)
CONNECT to connect to the device.

INSTALL

Settings -> Device & services to check the device.
Discover section.

