Coby nadšenec do chytré domácnosti poměrně často spoléhám na chytré osvětlení. Když jsem ale potřeboval vybavit každou místnost v domě ovladačem světel, a někdy hned několika, narazil jsem na, řekněme, rozpočtový problém. Komerční ovladače jsou poměrně drahé. Rozhodl jsem se proto využít 3D tiskárnu, přidat trochu elektroniky a postavit si cool ovladač za zlomek ceny.
Vytvořil jsem malý macropad s využitím spínačů pro mechanické klávesnice, desky ESP32, rotačního enkodéru pro nastavení jasu a kompaktního OLED displeje, který by zobrazoval aktuálně zvolenou světelnou scénu. Enkodér má navíc zabudované tlačítko, po jehož stisknutí se přepnete do režimu nastavení jasu samotného displeje, a dalším stisknutím se vrátíte zpět k základnímu ovládání světel.
Aby to ale ve finále nebyla jen další nudná plastová krabička, rozhodl jsem se použít několik materiálů Prusament Woodfill, aby výsledná barevná kombinace odpovídala dřevěné desce mého stolu. Měl jsem také možnost testovat jednu z beta jednotek Prusa CORE One+ INDX, takže jsem naplno využil její multimateriálové přednosti. Jistě, tento projekt můžete vytisknout na Original Prusa XL, na tiskárně vybavené jednotkou MMU3, nebo dokonce na standardní tiskárně s jedním nástrojem bez jakýchkoliv multimateriálových možností. V takovém případě budete muset během tisku několikrát manuálně vyměnit filament, což není žádná věda, ale počítejte s větším množstvím odpadu. Díky použití systému INDX se ale množství mého odpadu blížílo k nule, což je skvělý bonus pro celý projekt. Ušetřil jsem materiál, který můžu využít na tisk dalších macropadů nebo jiných věcí.
Hardware
Smyslem tohoto projektu je jeho dostupnost, a proto veškeré elektronické součástky pořídíte za pár stovek. Pokud byste ale přeci jen chtěli ušetřit, můžete ignorovat displej i enkodér a spokojit se pouze se spínači, případně ještě omezit jejich počet.
Řídící jednotkou tohoto macropadu může být téměř jakákoliv ESP32 deska nebo ekvivalent vybavený dostatkem vstupů a Wi‑Fi připojením. Kvalitní desky od výrobců jako Adafruit (například kompaktní QT Py ESP32-S3 nebo klasika Feather ESP32-S3) nebo SparkFun (Thing Plus ESP32-S3) se pro tento projekt hodí skvěle. V mém případě, ještě předtím, než bych šel nakupovat, jsem se ale ponořil do mého nekonečného šuplíku náhradních dílů a našel podobnou desku – Xiao ESP32-CS od Seeed Studio – kterou jsem se nakonec rozhodl pro tento projekt využít.
Co se týče spínačů, rozhodl jsem se pro modré, taktilní. Může jít o určitou specialitu, protože standardní tlačítka by posloužila stejně dobře, ale mít na macropadu mechanické klávesy je pro mě s ohledem na pocit i zvuk strašně uspokojivé.
Níže najdete seznam součástek, které potřebujete na verzi ovladače z tohoto článku:
| Součástka | Přibližná cena |
| ESP32 | $8,00 |
| 0.91″ OLED displej | $4,00 |
| Rotační enkodér KY-040 | $1,30 |
| Spínače pro mechanické klávesnice | $3,50 |
| Filament | ~$3,00 |
| Celková cena součástek | ~$19.80 |
3D tisk a přizpůsobení designu
Nyní přichází ke slovu příprava 3D modelů k tisku. Můžete si je sami vytvořit v 3D modelovacím programu, jako je například Fusion, nebo si stáhnout již připravené modely k tomuto projektu z Printables.
Nejprve stažené soubory naimportujte do PrusaSliceru. Pro čistý a elegantní vzhled krabičky můžete vytisknout jednobarevnou krabičku ovladače z Prusamentu Woodfill Linden Light. Protože se však fantazii meze nekladou, stejně tak si můžete připravit i pestrobarevné varianty, například ve stylu Avengers, které se skvěle hodí do dětského pokoje. Poté přidejte na tlačítka a enkodér jednoduché symboly pomocí funkce Přidat modifikátor > SVG. Pokud chcete detailnější návod, jak takové modifikátory aplikovat, doporučujeme vám podívat se do našeho článku o výrobě senzorických hraček pro děti. V elegantní variantě doporučujeme použít tmavší Prusament Chocolate Brown Woodfill pro tlačítka i enkodér, a stejnou světlou barvu Linden Light pro symboly, čímž dosáhnete pěkného kontrastu. Díky multimateriálovému tisku na tiskárnách, jako jsou Prusa CORE One+ s rozšířením INDX nebo Original Prusa XL, bude výsledek vždy precizní s nulovou kontaminací barev a jen s minimem odpadu. Tlačítka tiskněte horní stranou otočenou k podložce, na obrázku jsou umístěna opačně, aby byly patrné SVG symboly. Až budete mít všechny části vytištěny, můžete se pustit do sestavování.
Sestavení a zapojení
Nejprve nainstalujte mechanické spínače, jednoduše tak, že je zatlačíte do připravených otvorů v šasi ovladače, dokud nezacvaknou na místo. Poté nainstalujte displej, rotační enkodér a samotnou desku ESP32, kterou zasunete do připraveného otvoru na boku šasi, aby její konektor USB-C směřoval ven. Zatímco displej a enkodér drží pevně samy o sobě, osazení desky ESP32 vyžaduje trochu péče. Já jsem použil jednoduchý držák, který jen zasunete mezi stěnou krabičky a deskou, abyste ji upevnili na místě. A zároveň tím zajistíte, že se deska nezatlačí dozadu, když k ní budete chtít připojit USB-C kabel.
Celá sestava zabírá celkem jedenáct pinů – šest pro spínače, tři pro enkodér (2 pro otáčení, 1 pro spínač) a dva pro I2C displej (datum/hodiny). To přesně odpovídá počtu digitálních vstupů, které jsou k dispozici na mé ESP32 desce. Níže najdete schéma zapojení vytvořené ve Wokwi, skvělém bezplatném editoru, který běží přímo v prohlížeči. Wokwi zahrnuje také vestavěný simulátor, takže můžete propojit všechny komponenty a napsat si testovací kód, abyste si ověřili správnost zapojení ještě před sestavením fyzické jednotky.
Dávejte dobrý pozor při pohledu na desku: fyzické popisky vytištěné přímo na plošném spoji (D0-D10) se téměř vždycky liší od interních registrů (GPIO) použitých v kódu. Zde je hardwarové mapování:
5V / 5V0 – VCC (společná napájecí větev pro OLED displej a rotační enkodér)
GND – GND (společné uzemnění pro všechny komponenty a mechanické klávesy)
D0 (interní GPIO 2) – Klávesa 1D1(interní GPIO 3) – Klávesa 2D2(interní GPIO 4) – Klávesa 3D3(interní GPIO 5) – Klávesa 4D4(interní GPIO 6) – OLED SDA (datová sběrnice)
D5(interní GPIO 7) – OLED SCL (hodinový signál)
D6(interní GPIO 21) – enkodér CLK (směr otáčení A)
D7(interní GPIO 20) – enkodér DT (směr otáčení B)
D8(interní GPIO 8) – enkodér SW (vestavěné tlačítko enkodéru)
D9(interní GPIO 9) – Klávesa 5D10(interní GPIO 10) – Klávesa 6
Pro počáteční testování můžete všechno propojit pomocí konektorů Dupont. Prostor uvnitř krabičky je však poměrně těsný, a takové zapojení by nebylo při běžném používání dostatečně spolehlivé. Chcete-li zajistit 100% spolehlivost a pohodlné uspořádání, důrazně doporučujeme vše připájet.
Konfigurace softwaru (ESPHome)
Teď, když už máte všechno zapojeno, je čas na (vibe)coding! 😎 Připojení ESP32 k PC nebo Macu je snadné, stačí použít kabel USB-C, stáhnout potřebné nástroje a pomocí terminálu nahrát do desky kód. Na Macu můžete použít Python a balíček ESPHome, který nainstalujete pomocí správce balíčků pip příkazem pip3 install esphome.
Po dokončení instalace vytvořte dva YAML soubory: keyboard.yaml a secrets.yaml. Soubor keyboard.yaml obsahuje veškerý kód pro ESP32 a secrets.yaml bezpečně ukládá vaše přihlašovací údaje k Wi-Fi. Budete také potřebovat font s písmem – já jsem pro tento projekt použil Nunito z knihovny Google Fonts a stáhnout jej můžete odsud: https://fonts.google.com/specimen/Nunito. Archiv s písmem si stáhněte, rozbalte, základní řez písma (Nunito-Regular.ttf) umístěte do stejné složky jako ostatní soubory a přejmenujte jej na font.ttf.
Poznámka: Tento kód je specificky napsaný pro použití s deskou Seeed Studio XIAO ESP32-C3. Pokud používáte jinou ESP32 desku, budete muset upravit parametr board: a mapování GPIO pinů tak, aby odpovídaly vašemu konkrétnímu hardwaru. Pokud nevíte, jak na to, neváhejte si o pomoc říct vašemu oblíbenému AI asistentovi.
Níže je kompletní kód souboru keyboard.yaml. Nenechte se úplně vystrašit dlouhými sekvencemi označenými jako lambda:. Ty se starají o logiku na pozadí, jako je převod surových dat o jasu na přehledná procenta a formátování textu na OLED displeji.
esphome:
name: keyboard
on_boot:
priority: -100
then:
- sensor.rotary_encoder.set_value:
id: rotary_knob
value: 10
- sensor.template.publish:
id: ha_knob
state: 10
- binary_sensor.template.publish:
id: ha_service_mode
state: false
esp32:
board: seeed_xiao_esp32c3
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
captive_portal:
api:
on_client_connected:
- lambda: |-
id(ha_service_mode).publish_state(id(service_mode));
id(ha_active_key_sensor).publish_state(id(active_text));
services:
- service: set_display_text
variables:
new_text: string
then:
- lambda: |-
id(active_text) = new_text;
id(ha_active_key_sensor).publish_state(new_text);
id(oled_display).update();
logger:
baud_rate: 115200
i2c:
sda: 6
scl: 7
globals:
- id: active_text
type: std::string
initial_value: '"OFF"'
- id: service_mode
type: bool
initial_value: 'false'
- id: saved_light_steps
type: int
initial_value: '10'
- id: display_steps
type: int
initial_value: '10'
script:
- id: block_feedback_script
mode: restart
then:
- delay: 800ms
font:
- file: "font.ttf"
id: font_large
size: 14
- file: "font.ttf"
id: font_sub
size: 10
text_sensor:
- platform: template
name: "Active Key"
id: ha_active_key_sensor
sensor:
- platform: template
name: "Rotary Knob"
id: ha_knob
- platform: rotary_encoder
id: rotary_knob
pin_a: 21
pin_b: 20
min_value: 0
max_value: 10
resolution: 2
on_value:
then:
- lambda: |-
if (isnan(x)) return;
if (id(service_mode)) {
if (x == 0) id(rotary_knob).set_value(1);
id(oled_display).set_contrast(id(rotary_knob).state / 10.0);
} else {
if (x == 0 and id(ha_light_state).state) id(rotary_knob).set_value(1);
id(block_feedback_script).execute();
id(ha_knob).publish_state(id(rotary_knob).state);
}
- component.update: oled_display
- platform: homeassistant
id: ha_light_brightness
entity_id: light.office
attribute: brightness
on_value:
then:
- lambda: |-
if (not id(service_mode) and not id(block_feedback_script).is_running() and not isnan(x)) {
int steps = round((x / 255.0) * 10.0);
id(rotary_knob).set_value(steps);
id(ha_knob).publish_state(steps);
}
- component.update: oled_display
binary_sensor:
- platform: template
name: "Service Mode"
id: ha_service_mode
- platform: status
id: ha_connection_status
- platform: homeassistant
id: ha_light_state
entity_id: light.office
filters:
- delayed_off: 500ms
on_state:
then:
- lambda: |-
if (not x) {
if (not id(service_mode)) {
id(rotary_knob).set_value(0);
id(ha_knob).publish_state(0);
} else {
id(saved_light_steps) = 0;
}
id(active_text) = "OFF";
id(ha_active_key_sensor).publish_state("OFF");
id(oled_display).update();
}
- platform: gpio
name: "Encoder Press"
pin: {number: 8, mode: INPUT_PULLUP, inverted: true}
on_press:
then:
- lambda: |-
float current_state = isnan(id(rotary_knob).state) ? 10.0 : id(rotary_knob).state;
if (not id(service_mode)) {
id(saved_light_steps) = (int)current_state;
id(service_mode) = true;
id(rotary_knob).set_value(id(display_steps));
id(ha_service_mode).publish_state(true);
} else {
id(display_steps) = (int)current_state;
id(service_mode) = false;
id(rotary_knob).set_value(id(saved_light_steps));
id(ha_knob).publish_state(id(saved_light_steps));
id(ha_service_mode).publish_state(false);
}
- component.update: oled_display
- platform: gpio
name: "Key 1"
pin: {number: 2, mode: INPUT_PULLUP, inverted: true}
- platform: gpio
name: "Key 2"
pin: {number: 3, mode: INPUT_PULLUP, inverted: true}
- platform: gpio
name: "Key 3"
pin: {number: 4, mode: INPUT_PULLUP, inverted: true}
- platform: gpio
name: "Key 4"
pin: {number: 5, mode: INPUT_PULLUP, inverted: true}
- platform: gpio
name: "Key 5"
pin: {number: 9, mode: INPUT_PULLUP, inverted: true}
- platform: gpio
name: "Key 6"
pin: {number: 10, mode: INPUT_PULLUP, inverted: true}
display:
- platform: ssd1306_i2c
id: oled_display
model: "SSD1306 128x32"
address: 0x3C
rotation: 180
lambda: |-
if (id(service_mode)) {
it.print(0, 0, id(font_large), "DISPLAY");
} else {
it.print(0, 0, id(font_large), id(active_text).c_str());
}
if (id(ha_connection_status).state) {
it.print(82, 0, id(font_sub), "HA: OK");
} else {
it.print(82, 0, id(font_sub), "HA: --");
}
it.printf(0, 19, id(font_sub), "Int: %.0f%%", isnan(id(rotary_knob).state) ? 0.0 : id(rotary_knob).state * 10.0);
if (wifi::global_wifi_component->is_connected()) {
it.print(75, 19, id(font_sub), "Wi-Fi: OK");
} else {
it.print(75, 19, id(font_sub), "Wi-Fi: --");
}
Heslo k Wi-Fi se nachází v samostatném souboru secrets.yaml. Vytvořte jej a upravte podle svých vlastních údajů.
wifi_ssid: "my_wi-fi"
wifi_password: "my_secret_password"
Nyní můžete kód zkompilovat a nahrát do ESP32 pomocí příkazu python3 -m esphome run keyboard.yaml. Nezapomeňte, že tento příkaz musíte spustit přímo ze složky, ve které se nachází váš soubor YAML. Po výzvě v terminálu zadejte číslo odpovídající vašemu připojenému USB portu a stiskněte Enter. Pokud se vše úspěšně nahrálo, měli byste v terminálu vidět komunikační log na trase počítač–ESP32 a měl by se také rozsvítit displej.
Kromě jiného displej zobrazuje informace o stavu připojení k Wi-Fi a měl by po několika sekundách ukázat „OK“. Uvidíte také řádek „HA“, který představuje stav připojení k Home Assistant. V tuto chvíli nemusíte tento status řešit, ovšem jakmile všechno nakonfigurujeme, měl by také ukazovat OK. Pokud vidíte logy a displej zobrazuje informace, úspěšně jste flashnuli ESP32 a můžete přejít ke konfiguraci zmiňované služby Home Assistant. Nyní můžete nechat ESP32 připojené kabelem USB k počítači nebo k napájení použít standardní 5V USB adaptér.
Nastavení Home Assistant
1. Přidání ESP32 do Home Assistant
Díky nativní integraci ESPHome by měl Home Assistant váš nový macropad rozpoznat automaticky.
- V administraci Home Assistant přejděte do Nastavení > Zařízení a služby
- Podívejte se na horní část karty Integrace. Měli byste vidět nově objevené zařízení ESPHome s názvem keyboard.
- Klikněte na Konfigurovat a postupujte podle pokynů na obrazovce
- Poznámka: Pokud se zařízení neobjeví automaticky, klikněte v pravém dolním rohu na tlačítko Přidat integraci, vyhledejte ESPHome a zadejte místní IP adresu vašeho ESP32.
Jakmile macropad přidáte, Home Assistant automaticky načte všechny jeho mechanické klávesy, rotační enkodér i přepínač servisního režimu.
2. Vytvoření pomocníka Light Group
Aby byl tento projekt elegantní a univerzální, chceme se vyhnout zápisu konkrétních fyzických chytrých žárovek přímo do kódu. Namísto toho vytvoříme pomocníka Light Group, takže pokud v budoucnu žárovky vyměníte, pouze zaktualizujete tuto skupinu a vaše automatizace zůstane nedotčená.
- Přejděte do Nastavení > Zařízení a služby a nahoře klikněte na kartu Pomocníci
- V pravém dolním rohu klikněte na + Vytvořit pomocníka
- Sjeďte dolů, vyberte Skupina a poté zvolte Skupina světel
- Pojmenujte pomocníka Office (tím se automaticky vygeneruje požadované ID entity: light.office)
- Pod položkou Členové můžete vybrat vlastní fyzická chytrá světla, která chcete macropadem ovládat.
- Klikněte na Uložit / Vytvořit
S připojeným macropadem a připravenou skupinou světel nyní můžeme vše propojit pomocí našeho hlavního automatizačního skriptu.
- Přejděte do Nastavení > Automatizace scény a klikněte na Vytvořit automatizaci
- Zvolte Vytvořit novou automatizaci, poté klikněte na tři tečky v pravém horním rohu a vyberte Upravit v YAML.
- Smažte jakýkoliv výchozí kód, vložte skript níže a klikněte na Uložit.
Pro účely tohoto návodu jsem namapoval šest mechanických kláves tak, aby aktivovaly jednoduché barevné scény (modrá, zelená, červená atd.), ale vy si můžete vše upravit tak, aby se spouštěly úplně jiné scény, ať už takhle jednoduché, nebo mnohem pokročilejší.
alias: Light Office - Master Control
description: Master Control for Macropad
triggers:
- entity_id: binary_sensor.keyboard_key_1
to: "on"
id: press_k1
trigger: state
- entity_id: binary_sensor.keyboard_key_2
to: "on"
id: press_k2
trigger: state
- entity_id: binary_sensor.keyboard_key_3
to: "on"
id: press_k3
trigger: state
- entity_id: binary_sensor.keyboard_key_4
to: "on"
id: press_k4
trigger: state
- entity_id: binary_sensor.keyboard_key_5
to: "on"
id: press_k5
trigger: state
- entity_id: binary_sensor.keyboard_key_6
to: "on"
id: press_k6
trigger: state
- entity_id: sensor.keyboard_rotary_knob
id: knob_rotation
trigger: state
conditions:
- condition: state
entity_id: binary_sensor.keyboard_service_mode
state: "off"
actions:
- choose:
- conditions:
- condition: trigger
id: knob_rotation
sequence:
- action: light.turn_on
metadata: {}
target:
entity_id: light.office
data:
brightness_pct: "{{ (trigger.to_state.state | int(10)) * 10 }}"
- conditions:
- condition: trigger
id: press_k1
sequence:
- choose:
- conditions:
- condition: state
entity_id: sensor.keyboard_active_key
state: Blue
sequence:
- action: light.turn_off
target:
entity_id: light.office
default:
- action: light.turn_on
target:
entity_id: light.office
data:
brightness_pct: 100
rgb_color:
- 4
- 51
- 255
- action: esphome.keyboard_set_display_text
data:
new_text: Blue
- conditions:
- condition: trigger
id: press_k2
sequence:
- choose:
- conditions:
- condition: state
entity_id: sensor.keyboard_active_key
state: Green
sequence:
- action: light.turn_off
target:
entity_id: light.office
default:
- action: light.turn_on
target:
entity_id: light.office
data:
brightness_pct: 100
rgb_color:
- 0
- 249
- 0
- action: esphome.keyboard_set_display_text
data:
new_text: Green
- conditions:
- condition: trigger
id: press_k3
sequence:
- choose:
- conditions:
- condition: state
entity_id: sensor.keyboard_active_key
state: Red
sequence:
- action: light.turn_off
target:
entity_id: light.office
default:
- action: light.turn_on
target:
entity_id: light.office
data:
brightness_pct: 100
rgb_color:
- 255
- 0
- 0
- action: esphome.keyboard_set_display_text
data:
new_text: Red
- conditions:
- condition: trigger
id: press_k4
sequence:
- choose:
- conditions:
- condition: state
entity_id: sensor.keyboard_active_key
state: Yellow
sequence:
- action: light.turn_off
target:
entity_id: light.office
default:
- action: light.turn_on
target:
entity_id: light.office
data:
brightness_pct: 100
rgb_color:
- 255
- 255
- 0
- action: esphome.keyboard_set_display_text
data:
new_text: Yellow
- conditions:
- condition: trigger
id: press_k5
sequence:
- choose:
- conditions:
- condition: state
entity_id: sensor.keyboard_active_key
state: Purple
sequence:
- action: light.turn_off
target:
entity_id: light.office
default:
- action: light.turn_on
target:
entity_id: light.office
data:
brightness_pct: 100
rgb_color:
- 255
- 0
- 255
- action: esphome.keyboard_set_display_text
data:
new_text: Purple
- conditions:
- condition: trigger
id: press_k6
sequence:
- choose:
- conditions:
- condition: state
entity_id: sensor.keyboard_active_key
state: White
sequence:
- action: light.turn_off
target:
entity_id: light.office
default:
- action: light.turn_on
target:
entity_id: light.office
data:
brightness_pct: 100
rgb_color:
- 255
- 255
- 255
- action: esphome.keyboard_set_display_text
data:
new_text: White
mode: restart
YAML editor v Home Assistant vypadá takto:
Testování a závěr
Až všechno nahrajete a nastavíte, přichází čas macropad otestovat. Stiskněte klávesu a světelná scéna by se měla okamžitě přepnout. Otočte enkodérem a změní se intenzita světla. A nakonec OLED displej by měl také správně zobrazovat aktuálně vybranou scénu.
Pokud vše funguje, zbývá už jen vytisknout spodní kryt a zacvaknout ho na místo. Design počítá s tím, že bude držet pevně bez použití lepidla či šroubů. Protože jsem svou testovací verzi nechal propojenou většími Dupont kabely, potřeboval jsem uvnitř krabičky o trochu víc místa. Vytvořil jsem proto o něco vyšší spodní díl, čehož jsem ale na druhou stranu využil a opět zkombinoval různé materiály Prusament Woodfill, abych vytvořil pěkný přechodový proužek. Pokud se ale rozhodnete komponenty sletovat, můžete soubor spodního krytu ztenčit a výrazně tak snížit celkovou výšku macropadu.
Ačkoliv já osobně používám tento macropad jen k ovládání světel, můžete je samozřejmě namapovat na jakoukoli funkci chytré domácnosti. Vzhledem k tomu, že logiku zpracovává Home Assistant, může stisknutí klávesy nebo otočení enkodéru vytáhnout žaluzie, spustit robotický vysavač nebo automatizaci pro Spotify, která vašim hostům pustí legendární Rickroll. Možnosti jsou zkrátka neomezené! Jakmile si postavíte svůj první podobný projekt, bude těžké nezačít se poohlížet po domě a nehledat další místa, kde by 3D tiskárna a trocha elektroniky mohly pomoci.
Tisku zdar!

Pro přidávání komentářů se musíte nejdříve přihlásit.