Ein vollständiges Ökosystem für handverdrahtete ergonomische Dactyl Manuform Split Keyboards mit ESP32 (WEMOS LOLIN32 Lite):
- ESP32 Firmware: Bluetooth Low Energy (BLE HID) Tastatur mit eigenem GATT-Konfigurationsdienst, NVS-Flash-Speicherung, Layer-Navigation und voller Mac-Unterstützung (inkl. Mac Globe/Fn-Taste).
- Web Configurator & Live-Monitor: Moderne Web-App (oder native macOS-App) mit Web-Bluetooth & Web-Serial für Tastenbelegung in Echtzeit (< 0,1s), Live-Keystroke-HUD und Flashen per Knopfdruck.
Die vollständige Beschreibung aller Features steht in FEATURES.md. Offene Arbeiten stehen in ROADMAP.md.
- ⚡ Kabelloses Live-Remapping: Über Web-Bluetooth GATT können Tasten und Ebenen-Namen im laufenden Betrieb geändert werden – ohne Neukompilieren oder Kabel. Jede Änderung überträgt die vollständige Konfiguration der verbundenen Hälfte und speichert sie dauerhaft im NVS-Flash des ESP32.
- 🔌 USB Live-Monitor: Serieller Echtzeit-Stream (115.200 Baud) zur Anzeige physischer Matrix-Events (
KEY_PRESS,KEY_RELEASE,LAYER), entprellt und mit HUD-Visualisierung. - 💾 Flashen per Knopfdruck: Integrierter lokaler Python-Server generiert C++-Keymaps und flasht den ESP32 automatisch per
arduino-cli. - 📑 Multi-Layer Engine: 8 dynamische Ebenen mit Momentary Layer Switching (
MO_0..MO_7) und Toggle Layer Switching (TG_0..TG_7). - 🍎 Volle Apple macOS Unterstützung:
- Standard macOS Modifier (
Cmd,Alt/Option,Ctrl,Shift). - Media Keys (Play/Pause, Mute, Volume Up/Down, Next, Prev, Stop).
- Spezielle Apple Globe / Fn-Taste (
0x029D/KEY_MEDIA_GLOBE) mit Press & Release Unterstützung für Chords und Emoji-Auswahl.
- Standard macOS Modifier (
- 🔒 Sicherheit & Stabilität:
- FreeRTOS Mutex-Schutz zwischen Matrix-Scan im Arduino-Main-Loop und BLE/Serial GATT-Callbacks.
- Server bindet an
127.0.0.1mit strikter Host- und Origin-Validierung gegen CSRF. - Eingabevalidierung und Identifier-Sanitizing im C++-Codegenerator.
Die Firmware ist für das WEMOS LOLIN32 Lite (ESP32-D0WDQ6-V3) Board ausgelegt.
(Vektorgrafik: docs/lolin32_lite_wiring.svg)
Die Kabel sind im Diagramm und in der folgenden Tabelle exakt nach der linken und rechten Seite des ESP32 aufgeteilt (Pins 1 bis 13 von oben nach unten):
| Pin | Beschriftung | Signal / Matrix | Kabelfarbe | Funktion / Tasten |
|---|---|---|---|---|
| 1 | GND |
Masse | ⚫ Schwarz | Gemeinsame Bezugsmasse für Logik & Taster |
| 2 | 3V3 |
Power | 🔴 Rot | 3,3V Logik-Versorgungsspannung |
| 3 | IO0 (GPIO 0) |
– | – | Nicht belegt (Boot-Pin) |
| 4 | IO2 (GPIO 2) |
– | – | Nicht belegt (frei) |
| 5 | IO4 (GPIO 4) |
Col 5 | 🟣 Lila | Spalte 5 ganz rechts (ß, Ü, Ä, Shift, Alt / R4C5) |
| 6 | IO5 (GPIO 5) |
– | – | Nicht belegt (frei) |
| 7 | IO12 (GPIO 12) |
– | – | Nicht belegt (frei) |
| 8 | IO13 (GPIO 13) |
Row 0 | 🟡 Gelb | Zahlenreihe ganz oben (6, 7, 8, 9, 0, ß / R0C0) |
| 9 | IO14 (GPIO 14) |
Row 1 | 🟠 Orange | Obere Buchstabenreihe (Z, U, I, O, P, Ü) |
| 10 | IO15 (GPIO 15) |
– | – | Nicht belegt (frei) |
| 11 | IO16 (GPIO 16) |
Col 4 | 🔵 Blau | 5. Spalte von links (0, P, Ö, -, →, Daumen Tab / R5C4) |
| 12 | IO17 (GPIO 17) |
Col 3 | 🟤 Braun | 4. Spalte von links (9, O, L, ., ↑, Daumen Space 2U / R5C3) |
| 13 | IO18 (GPIO 18) |
Col 2 | 🟢 Grün | 3. Spalte von links (8, I, K, ,, ↓, Daumen Enter 2U / R5C2) |
| Pin | Beschriftung | Signal / Matrix | Kabelfarbe | Funktion / Tasten |
|---|---|---|---|---|
| 1 | IO19 (GPIO 19) |
Col 1 | 🟠 Orange | 2. Spalte von links (7, U, J, M, ←, Daumen Alt / R5C1) |
| 2 | IO22 (GPIO 22) |
Onboard LED | – | Interne blaue Status-LED |
| 3 | IO23 (GPIO 23) |
Col 0 | 🟡 Gelb | Spalte ganz links innen (6, Z, H, N, Daumen Cmd / R5C0) |
| 4 | IO25 (GPIO 25) |
Col 6 | 🔴 Rot | Daumen-Spalte oben links (Esc / R5C6) • An Schalterpin 2 |
| 5 | IO26 (GPIO 26) |
Row 3 | 🟤 Braun | Untere Buchstabenreihe (N, M, ,, ., -, Shift) |
| 6 | IO27 (GPIO 27) |
Row 2 | 🟢 Grün | Home Row Grundreihe (H, J, K, L, Ö, Ä) |
| 7 | IO32 (GPIO 32) |
Row 5 | 🟣 Lila | Daumen-Cluster (Gemeinsame Zeile aller 6 Daumentasten) |
| 8 | IO33 (GPIO 33) |
Row 4 | 🔵 Blau | Unterste Finger-Reihe (Pfeiltasten ←, ↓, ↑, →, R4C5) |
| 9 | EN (RST) |
Reset | – | Chip Enable / Reset (intern mit Pullup) |
| 10 | IO34 (GPI34) |
– | – | Nicht belegt (Input-Only) |
| 11 | IO35 (GPI35) |
– | – | Nicht belegt (Input-Only) |
| 12 | IO36 (VP) |
– | – | Nicht belegt (Input-Only) |
| 13 | IO39 (VN) |
– | – | Nicht belegt (Input-Only) |
Note
Schalter- und Diodenanschluss:
- Zeilen (Rows): Werden mit den Dioden-Kathoden verbunden (schwarzer Ring der 1N4148 zeigt zur Zeile, Anode an Schalterkontakt 1).
- Spalten (Columns): Werden direkt an Schalterkontakt 2 gelötet (ohne Diode, interner Pullup des ESP32).
- Akkubetrieb & Power Management: Der LiPo-Akku wird direkt in die weiße JST PH-2.0-Buchse des Boards gesteckt. Der Ladevorgang erfolgt automatisch bei USB-Anschluss über den integrierten TP4054-Ladecontroller.
Note
- Die linke Hälfte (
firmware/dactyl_left) hat eine eigene Keymap mit 8 Layern, Tap-Hold, Double-Tap, Combos und NVS. Sie nutzt dieselbe Logik wie rechts (DactylLogic.h). - Die linke Hälfte sendet die fertigen Tastendrücke per ESP-NOW an die rechte Hälfte. Am Mac erscheint nur ein Bluetooth-Gerät.
- Ebenen gelten auf beiden Hälften:
MO_n,TG_n,LAYER_UPundLAYER_DOWNauf einer Hälfte aktivieren die Ebene auch auf der anderen (SPLIT_OP_LAYER_STATE). - Den Config Mode der linken Hälfte startest du mit
R0C5 + R4C0(0,5 s). Sie meldet sich dann alsDactyl Config L. - Wähle in der App oben Linke Hand. Die App bearbeitet dann die linke Belegung. Sie überträgt sie wie rechts per Combo-Auto-Sync über Bluetooth, per USB oder mit „Auf Dactyl flashen“. Beim Flashen wählt der Server automatisch den USB-Port der linken Hälfte.
- Auf der linken Hälfte sitzen die Dioden an den Spalten (schwarzer Ring zum Spaltendraht). Die Spalten liegen auf RTC-Pins, damit jede Taste aus dem Deep Sleep weckt. Die Löttabelle steht in
AGENTS.md(„Left Half Firmware“). - Beide Hälften brauchen das Partitionsschema
min_spiffs. Der Flash aus der App setzt es automatisch.
- Python 3: Für den lokalen Server (
server.py). - Arduino CLI (optional fürs Flashen):
brew install arduino-cli arduino-cli core install esp32:esp32
- Arduino Core:
esp32:esp32@3.3.12(keine externe Bibliothek nötig; die Composite-BLE-HID-Implementierung liegt infirmware/dactyl_right/DactylBleComposite.h, Keycodes inDactylKeycodes.h). - Browser: Chromium-basierter Browser (Google Chrome, Arc, Brave, Edge, Opera) mit aktivierter Web-Bluetooth- und Web-Serial-Unterstützung.
cd dactyl_configurator
python3 server.pyDer Server startet unter http://localhost:8088.
- Kabellos via Bluetooth (One-Shot Auto-Sync & Live-Sync):
- Einmalige Kopplung: Halte auf der Tastatur
R4C5 + R0C0für 0,5 Sekunden gedrückt, klicke in der App auf „⚡ Bluetooth Live-Sync“ und wähle „Dactyl Config“ aus. - Alltags-Workflow (Staged One-Shot Auto-Sync):
- Ändere Tastenbelegungen, Layer-Namen oder Timeouts bequem in der App, während die Tastatur ganz normal als Mac-Tastatur verbunden ist (Änderungen werden als „⏳ Bereit für Combo-Sync“ vorgemerkt).
- Halte anschließend an der Tastatur
R4C5 + R0C0(0,5s) gedrückt: - Die Tastatur kappt kurz die Mac-Verbindung, verbindet sich vollautomatisch über die moderne Web-Bluetooth API (
navigator.bluetooth.getDevices()+watchAdvertisements()) mit der App, überträgt alle Änderungen per Schnell-Batch (SET_K+SAVE_NVS) in den ESP32-Flash, trennt die App-Verbindung wieder und kehrt sofort als Tastatur zum Mac zurück – ohne Klick in der App!
- Einmalige Kopplung: Halte auf der Tastatur
- Per USB-Kabel (Live-Monitor):
- ESP32 per USB-Kabel an den Rechner anschließen.
- Auf „🔌 USB Live-Monitor“ klicken und den seriellen Port auswählen.
- Physische Tastendrücke und Layer-Wechsel werden im Visualizer sofort hervorgehoben.
- ESP-NOW-Schlüssel (einmalig, vor dem ersten Flashen):
- Führe
bash scripts/generate-split-key.shaus. Das Skript schreibtfirmware/dactyl_right/DactylSplitKey.h(nicht versioniert, Modus 0600). - Beide Hälften verschlüsseln die Funkstrecke damit. Ohne Schlüssel bricht das Kompilieren ab.
npm run build:appundnpm run rebuild:apperzeugen den Schlüssel vorher selbst, wenn er fehlt. Ein vorhandener Schlüssel bleibt unverändert.- Flashe nach einem neuen Schlüssel beide Hälften. Danach
npm run build:app, damit die App den Schlüssel beim Flashen kennt.
- Führe
- Flashen per USB:
- Mit einem Klick auf „💾 Auf Dactyl flashen“ kompiliert der lokale Server die C++-Keymap und flasht die Firmware per
arduino-clidirekt auf den ESP32. - Ohne App:
npm run flash -- Lodernpm run flash -- Rflasht die Firmware aus dem Repository. Die Keymap im NVS bleibt erhalten. - Beide Wege sichern vor dem Upload die NVS-Partition (Keymap und macOS-Kopplung) im Datenordner der App.
- Mit einem Klick auf „💾 Auf Dactyl flashen“ kompiliert der lokale Server die C++-Keymap und flasht die Firmware per
- Nach einer Änderung des HID-Report-Descriptors erneut koppeln:
- macOS speichert die Report-Map beim ersten Koppeln und liest sie bei Reconnects nicht neu.
- Die Firmware mit Mod-Tap erweitert den Keyboard-Report um F13–F24 (Usages
0x68–0x73). - Entferne die Tastatur nach dem Flashen dieser Version in den macOS-Bluetooth-Einstellungen („Dieses Gerät ignorieren …“) und kopple sie neu. Sonst ignoriert macOS F13–F24.
Unter macOS (sowohl in Google Chrome als auch in nativen Electron-Apps) sperrt das Betriebssystem aktive BLE-HID-Tastaturen (HID Service 0x1812 / Appearance 0x03C1) für generische GATT-Dienste wie den Web-Bluetooth Config-Service. Zudem verbindet sich bluetoothd unter macOS sofort automatisch wieder, solange die Tastatur unter derselben gebondeten Public-MAC-Adresse wirbt.
Die Firmware löst dieses Problem zuverlässig ohne physischen Ein/Aus-Schalter:
-
Hardware-Auslöser
R4C5 + R0C0:R0C0: Zeile 0, Spalte 0 (Taste6ganz links oben in der Zahlenreihe).R4C5: Zeile 4, Spalte 5 (unterste rechte Randtaste neben den Pfeiltasten).- Werden beide Tasten für 0,5 Sekunden gleichzeitig gehalten, wechselt die Firmware sofort in den Konfigurationsmodus.
- 100% Layer-unabhängig: Die Abfrage erfolgt direkt auf den GPIO-Pins der Hardware-Matrix. Es spielt keine Rolle, welcher Layer aktiv ist oder welche logische Taste darauf gemappt ist.
-
Verhalten im Config-Modus:
- Der ESP32 stoppt zuerst das HID-Advertising, trennt alle aktiven HID-Peers (macOS) und wartet, bis der asynchrone HCI-Disconnect vollständig abgeschlossen ist.
- Anschließend schaltet der ESP32 auf eine deterministische Static Random BLE-Adresse (
BLE_ADDR_TYPE_RANDOM, letztes Byte^ 0x55) um und wirbt als"Dactyl Config"mitAppearance 0x0000und ausschließlich demCONFIG_SERVICE_UUID(ohne HID0x1812). - Dadurch funkt macOS
bluetoothdniemals dazwischen, und die App erkennt das Gerät sofort überwatchAdvertisements(). - In der App leuchtet oben im Header und im Banner die Anzeige „⚡ KONFIG-MODUS AKTIV“.
-
One-Shot Auto-Sync vs. Interaktiver Konfig-Modus:
- One-Shot Auto-Sync (Standard, Checkbox aktiv): Sobald du
R4C5 + R0C0drückst, verbindet sich die App automatisch, überträgt alle gespeicherten Einstellungen mit Fortschrittsbalken in ~1,5 Sekunden an den ESP32 und sendet automatischEXIT_CONFIG, sodass die Tastatur sofort wieder mit dem Mac verbunden ist. - Interaktiver Live-Modus (Checkbox deaktiviert): Die Tastatur bleibt im Konfig-Modus mit der App verbunden, bis du auf „Trennen & zurück zur Mac-Tastatur“ klickst oder erneut
R4C5 + R0C0drückst (oder nach 3 Minuten Auto-Timeout).
- One-Shot Auto-Sync (Standard, Checkbox aktiv): Sobald du
Für den echten kabellosen LiPo-Akkubetrieb verfügt die Firmware über eine intelligente, zweistufige Stromspararchitektur:
-
Stufe 1: Standby / Light Sleep (Standard: 5 Minuten)
- Senkt den Matrix-Scan auf ca. 25 Hz. Die rechte Hälfte drosselt die CPU zusätzlich von 240 MHz auf 80 MHz. Die linke Hälfte bleibt auf 240 MHz, weil ein Taktwechsel bei laufendem Wi-Fi den ESP-NOW-Versand stört.
- Die BLE-Verbindung zu macOS bleibt vollständig aktiv (kein Reconnect nötig). Der Funk bleibt bei beiden Hälften an (BLE und ESP-NOW).
- Rechte Hälfte: Der erste Tastenanschlag setzt die CPU sofort wieder auf 240 MHz.
- Ersparnis (Schätzung nach ESP32-Datenblatt, nicht gemessen): Der Funk verbraucht in beiden Stufen gleich viel; nur der CPU-Anteil sinkt.
- Rechts: Die Datenblattwerte gelten für die CPU ohne Funk (der Funk kommt in beiden Stufen hinzu): ca. 30–68 mA bei 240 MHz, ca. 20–31 mA bei 80 MHz. Das Board spart damit ca. 10–37 mA.
- Links: Der Takt bleibt bei 240 MHz. Nur der langsamere Scan spart Strom; die Ersparnis ist gering.
-
Stufe 2: Deep Sleep (Standard: 60 Minuten)
- Schaltet den ESP32 bei längerer Abwesenheit in den tiefen Tiefschlaf (~10–15 µA Stromaufnahme).
- Alle 6 Matrix-Zeilen (
GPIO 13, 14, 27, 26, 33, 32) sind RTC-Pins und werden alsEXT1 ANY_HIGHWakeup-Quellen konfiguriert. - Die Spalten-Pins werden im Schlaf per Hardware-Hold auf
HIGHgehalten. - Aufwecken: Ein Druck auf jede beliebige Taste der Tastatur weckt den ESP32 innerhalb von ~150 ms auf und verbindet ihn automatisch wieder mit dem Mac.
-
Akkubetrieb & Laden:
- Der LiPo-Akku versorgt das Board direkt über die integrierte JST PH-2.0-Buchse.
- Das zweistufige Power-Management (Standby nach 5 Min, Deep Sleep nach 60 Min) arbeitet rein zeit- und inaktivitätsbasiert; es ist kein zusätzliches Löten an ADC-Pins erforderlich.
- Sobald das USB-Kabel angesteckt wird, lädt der Onboard-Ladechip (TP4054) den Akku automatisch auf.
dactyl_configurator/
├── firmware/
│ └── dactyl_right/
│ └── dactyl_right.ino # ESP32 Arduino Firmware (BLE HID & GATT Config)
├── docs/
│ ├── lolin32_lite_wiring.svg # Vektorgrafik des Löt- & Anschlussplans (Links/Rechts)
│ └── lolin32_lite_wiring.png # Hochauflösendes PNG des Anschlussplans
├── AGENTS.md # Vollständige Architektur- & Schnittstellendokumentation
├── app.js # Configurator UI-Logik, Web-Bluetooth & Web-Serial
├── index.html # Single-Page-App Dashboard
├── style.css # Modernes Dark-Mode CSS Layout
├── server.py # Lokaler Python Backend-Server (Flash & Sync API)
├── default_config.json # Versionierte Vorlage für Keymaps, Combos & Einstellungen
├── current_config.json # Lokaler Stand der App (nicht versioniert, .gitignore)
├── .gitignore # Git Ignore Definitionen
└── README.md # Diese Dokumentation
Upgrade-Hinweis: Ältere Stände versionierten
current_config.json. Eingit pullüber den Commit, der die Datei aus Git entfernt, löscht die lokale Datei. Sichern Siecurrent_config.jsonvor dem Pull (z. B.cp current_config.json ~/dactyl_config.backup.json) und kopieren Sie sie danach zurück.
| Bereich | Typ | Beispiele | Verhalten |
|---|---|---|---|
0x00 |
Keystroke leer | 0 (NONE) |
Transparent (fällt auf Layer 0 zurück) |
0x20 .. 0x7E |
ASCII Zeichen | 'a', '1', ' ' |
Normaler Tastenanschlag |
0x80 .. 0x87 |
HID Modifier | KEY_LEFT_GUI, KEY_LEFT_ALT |
Modifier-Hold |
0x100 .. 0x107 |
Momentary Layer | ACT_MO_0 .. ACT_MO_7 |
Ebene aktiv, solange Taste gehalten wird |
0x110 .. 0x117 |
Toggle Layer | ACT_TG_0 .. ACT_TG_7 |
Einrastende Ebene; TG_0 setzt auf Base zurück |
0x193 .. 0x19A |
Tap-Toggle Layer | ACT_TT_BASE + n (TT_0 .. TT_7) |
Halten: Ebene wie MO_n; zweimal schnell tippen: Ebene einrasten wie TG_n |
0x118 |
Layer +1 Hoch | ACT_LAYER_UP (LAYER_UP) |
Schaltet 1 Ebene höher (0 ➔ 1 ➔ ... ➔ 7 ➔ 0) |
0x119 |
Layer -1 Runter | ACT_LAYER_DOWN (LAYER_DOWN) |
Schaltet 1 Ebene tiefer (0 ➔ 7 ➔ ... ➔ 1 ➔ 0) |
0x16A |
Layer Lock | ACT_LAYER_LOCK (LAYER_LOCK) |
Rastet die gehaltene MO_n-Ebene ein; zweiter Druck löst sie |
0x180 .. 0x187 |
One-Shot-Ebene | OSL_0 .. OSL_7 |
Tippen merkt die Ebene für den nächsten Tastendruck beider Hälften vor; zweites Tippen nimmt sie zurück; Halten wirkt wie MO_n |
0x120 .. 0x126 |
Media Keys | Play/Pause, Mute, Vol+, Vol- |
Consumer Control HID |
0x127 |
Apple Mac Globe | ACT_MAC_GLOBE (0x029D) |
Emojis / Fn-Tastenkombinationen |
0x160 .. 0x167 |
One-Shot-Modifier | ACT_OSM_LCTRL .. ACT_OSM_RGUI (OSM_*) |
Tippen: Modifier nur für die nächste Taste (beide Hälften); Halten: normaler Modifier |
0x168 |
Caps Word | ACT_CAPS_WORD (CAPS_WORD) |
Buchstaben groß bis zum Wortende (Leertaste, Satzzeichen) |
0x169 |
Repeat | ACT_REPEAT (REPEAT) |
Wiederholt die zuletzt ausgeführte Aktion, auch die der anderen Hälfte (Layer-Aktionen und reine Modifier zählen nicht) |
0x16B |
Alt Repeat | ACT_ALT_REPEAT (ALT_REPEAT) |
Wiederholt das Gegenteil der letzten Aktion (←/→, ↑/↓, Bild↑/↓, Pos1/Ende, Backspace/Entf, Scrollen, Lautstärke, Titel); ohne Gegenteil sendet die Taste nichts |
0x16C |
Makro aufnehmen | ACT_DM_REC (DM_REC) |
Startet und beendet die Aufnahme eines Makros auf dem Master: die Tasten beider Hälften landen in einem RAM-Slot (höchstens 32 Schritte). Nur die rechte Hälfte; der Slot geht bei Neustart, Deep Sleep und Config Mode verloren |
0x16D |
Makro abspielen | ACT_DM_PLAY (DM_PLAY) |
Spielt den aufgenommenen Slot mit der Makro-Engine ab; während der Aufnahme beendet die Taste nur die Aufnahme |
0x16F |
Key Lock | ACT_KEY_LOCK (KEY_LOCK) |
Die nächste Taste oder Maustaste beider Hälften bleibt nach dem Loslassen gedrückt; ein zweiter Druck löst sie. Ebenen-Tasten, Key Lock selbst, Makros und Medientasten werden nicht gesperrt; RELEASE_ALL, Link-Ausfall, Mac-Verlust, Standby und Config Mode lösen alle Sperren |
0x16E |
Leader | ACT_LEADER (LEADER) |
Startet eine Folge aus bis zu vier Tasten derselben Hälfte (Zeitlimit einstellbar, Standard 1000 ms); eine passende Folge führt ihre Aktion aus (Taste, Tastenpaar oder Makro), die Tasten der Folge senden nichts; ohne Treffer passiert nichts |
0x170 .. 0x17F |
Makro | ACT_MACRO_BASE + n (MACRO_0 .. MACRO_15) |
Spielt eine Folge aus Tippen, Drücken, Loslassen und Pausen (max. 32 Schritte) |
MIT License. Frei verwendbar für DIY-Keyboard-Enthusiasten und Maker.