PROTOTYPE

HIOS PAD — Macropad / Control-Deck ESP32-S3

Macropad de escritorio con pantalla a color, encoder, joystick analógico, 10 teclas de acción (+ 2 ALT) y 2 parlantes, que actúa como **teclado/mouse/multimedia HID** por **USB, Bluetooth (BLE) y WiFi**. Capas por contexto (edición, dev, multimedia, navegador, videollamadas) navegables desde la propia pantalla, con feedback real del estado de la PC vía un daemon companion.
1 / 7
Der Moment

"Das erste Mal, als sich der Cursor vom Stick über Bluetooth bewegte — kein Kabel, kein Dongle, keine Treiber — und der PC einfach reagierte. Nach ein paar Stunden BLE-Tuning unter Linux zwischen Docs, KI, Tests und Builds hat es zu sehen den Kreis geschlossen."

Dieselbe Hardware lief unter Windows, aber unter Linux tauchte sie im Scan nicht auf. Es stellte sich als feine Kombination heraus: Der Adapter nahm das Legacy-Advertising nicht an, also bin ich auf BLE 5 Extended Advertising umgestiegen; und obendrein ein bekannter NimBLE-Bug, der den ESP bei jeder Verbindung neu startete und das Ganze wie ein 'verbindet nicht' aussehen ließ. Eine gute Weile btmon und Lesen, um es einzugrenzen.

HIOS PAD — Macropad / Control-Deck ESP32-S3

Macropad de escritorio con pantalla a color, encoder, joystick analógico, 10 teclas de acción (+ 2 ALT) y 2 parlantes, que actúa como teclado/mouse/multimedia HID por USB, Bluetooth (BLE) y WiFi. Capas por contexto (edición, dev, multimedia, navegador, videollamadas) navegables desde la propia pantalla, con feedback real del estado de la PC vía un daemon companion.

Quick Start

cd projects/pad

# Compilar y flashear (PlatformIO)
pio run -t upload

# Monitor serial
pio device monitor -b 115200
Al arrancar, el monitor muestra token API/OTA. Copialo en companion/config.json antes de iniciar el daemon o abrir la PWA desde otra máquina. El token se genera una única vez, queda guardado en NVS y protege la API web y OTA.
Flasheo en WSL: el puerto serie del DevKit (CH343, UART) no aparece solo en WSL. Desde PowerShell (Windows): usbipd listusbipd attach --wsl --busid <id> (al reconectar el cable hay que re-attachear). Recién ahí aparece /dev/ttyACM0. El USB nativo del S3 (303a:1001) es el HID; el CH343 es para flashear/serial.

Flujo de trabajo

Del cero a la placa andando:
  1. Soldar — seguí la guía verificada /pinouts/pad: ordena los módulos por paso, trae el checklist de armado y las mediciones (buck a 5.0V, SD de los amplis, diodos de la matriz). Es la única hoja de cableado y se auto-verifica contra src/app/Pins.h + platformio.ini en cada npm run test:wiring (desde la raíz del repo web).
  2. Primer flasheo (por cable)pio run -t upload (ver Quick Start). En WSL, attachá el CH343 con usbipd primero.
    ⚠️ Eléctrico: con el pack 2S conectado, no enchufes el USB sin abrir antes SW-CELDAS — el VBUS del USB y la salida del buck pelearían en el pin 5V. Flasheá con el pack apagado, o subí por OTA sin tocar cables.
  3. Updates (OTA) — ya con WiFi: en platformio.ini, agregá --auth=<token API/OTA> a upload_flags de [env:ota], luego ejecutá pio run -e ota -t upload --upload-port hiospad.local. El USB-C nativo + el botón BOOT quedan accesibles por si un OTA sale mal.
  4. Companion (opcional) — arrancá el daemon para feedback real y mute global (ver Apps companion). El pad es local-first: anda perfecto sin nada de esto.

¿Qué es?

Un control-deck que reemplaza atajos y controles dispersos por capas físicas. Cada capa mapea las 10 teclas de acción + los 2 ALT + encoder + stick a acciones (atajos de teclado, multimedia, mouse, macros). Es HID nativo (la PC lo ve como teclado/mouse), así que funciona sin drivers.
  • Multi-transporte: USB (TinyUSB) y BLE (NimBLE, HID compuesto teclado+mouse+consumer) en simultáneo; auto-switch (enchufado → USB, desenchufado → BLE). WiFi para feedback/control mediado (no es HID).
  • Local-first: todo funciona sin red ni companion. El WiFi y el daemon son una capa opcional que mejora (feedback real, mute global), nunca bloquea.
  • Stick como mouse: el joystick mueve el puntero; tap = click izq, doble = click der, long = toggle modo mouse.
  • Encoder contextual: gira según la capa (volumen/scroll/zoom/pestañas); doble-click cicla entre comportamientos; press abre el menú.

Capas y menú

Las capas se agrupan por tipo. El menú (encoder-press) es un picker de un nivel: muestra las capas del grupo sobre las 10 teclas físicas y girás el encoder para pasar de grupo. Apretás un botón → saltás a esa capa.
GrupoCapas
TrabajoEdición, Dev, Apps
MultimediaMultimedia, YouTube, Netflix
WebNavegador
LlamadasMeet, Slack, Zoom, Teams
SistemaRGB
Ajustes (página final)Brillo, Tema, Color, Skin, Dimmer, Hora, WiFi, Calibrar
  • Girar = cambiar de grupo/página · Teclas 1-10 = saltar a la capa · Encoder-press = abrir Ajustes · Long-press = volver/cerrar.

Videollamadas (grupo Llamadas)

Una capa por app con sus atajos. El mic tiene doble vía: tap = atajo de la app (Meet Ctrl+D, Zoom Alt+A, Teams Ctrl+Shift+M); hold (long-press) = mute global a nivel OS vía companion (Core Audio), que funciona en cualquier app y refleja el estado real. Slack no tiene atajo de mic → usa el mute global. La cámara va por atajo de la app.
Zoom requiere activar global shortcuts (Settings → Keyboard Shortcuts) para mutear sin foco en la ventana.

Apps companion (opcionales)

Todo esto es local-first: el pad funciona solo. Las apps lo potencian; si se caen, el pad sigue.
  • pad-companion — daemon (headless). Lee el estado real de la PC (volumen, mic muteado, temps y carga de CPU/GPU en Windows Core Audio / Linux PipeWire) y lo empuja por POST /api/state: el display pasa de estado optimista (lo que el pad cree haber dejado) a datos reales. El mismo canal lleva comandos pad→OS en la respuesta del POST — el más usado es el mute global de mic a nivel sistema, que funciona en cualquier app (Slack, Meet, etc.). Si el daemon se cae, el pad vuelve al estado optimista en ~4s. Contrato de la API, dependencias por OS y autostart (systemd en Linux / Tarea Programada en Windows) en companion/README.md.
  • Admin + mirror — web (dentro del companion). Servidor liviano (companion/src/web) con UI para editar mapeos/capas/textos y un mirror/emulador del pad: editás el mismo modelo de datos que corre el firmware y se lo mandás, sin recompilar.
  • host/openrgb-rgb-layer.ahk — helper. Script AutoHotkey que enlaza la capa RGB del pad con OpenRGB en la PC (ilumina periféricos según la capa activa).

Arquitectura

FreeRTOS, tareas separadas por core:
  • inputTask (core1): lee botones/encoder/stick → Dispatcher resuelve la acción de la capa → cola de acciones; arma el snapshot de UI.
  • transportTask (core1): consume acciones → transporte HID activo (USB/BLE) vía TransportRouter.
  • uiTask (core0): dibuja dashboard/menu/portal con TFT_eSprite (sin parpadeo).
  • netTask (core0): WiFi STA + portal cautivo + NTP + WebServer (/api/state).
Directorios: actions/ (modelo de Action), mapping/ (KeyMap/Dispatcher), inputs/ (botones/encoder/stick), transport/ (USB/BLE/router), net/, ui/ (skins, menu, dock, iconos vectoriales), storage/ (config por defecto), app/ (config, pines, estado).

Hardware

  • ESP32-S3-DevKitC-1 (N16R8: 16MB flash + 8MB PSRAM octal, AP Memory 3.3V — confirmado por chip dump). La PSRAM octal ocupa los GPIO 33–37 (y la flash los 26–32): no están disponibles.
  • Display ILI9488 4" 480×320 SPI (HSPI, 27MHz). No es ST7796.
  • Encoder KY-040 · Joystick HW-504 (alimentado a 3V3, no 5V) · 12 pulsadores NA a GND: 10 de acción en matriz 2×5 con diodos (cátodo hacia la fila) + 2 ALT directos.
  • 2× MAX98357A (I2S, bus compartido; el canal lo elige el pin SD de cada uno: L = SD a Vin, R = SD por 390k a Vin).
  • Pines en src/app/Pins.h (fuente de verdad). Cableado y alimentación paso a paso: la guía /pinouts/pad, verificada contra el firmware por self-test.

Gotchas (aprendidos a los golpes)

  • Sprite + fuente: new TFT_eSprite deja gfxFont sin inicializar → boot loop. Llamar setTextFont(1) tras crear cada sprite.
  • Joystick a 3V3: a 5V sobre-voltea el ADC del S3 y acopla los ejes (diagonales fantasma). El stick va a 3V3.
  • BLE symbol clash: USBHIDKeyboard.h y las libs BLE chocan (KEY_*/KeyReport); se aíslan con fábricas (transport/), main nunca ve ambos headers.
  • Heap del menú: el sprite del carrusel (~60KB) se libera al cerrar el menú; si queda alocado, el heap steady-state (con BLE+WiFi) se agota.
  • Brownout: un cable USB fino / fuente floja tira la tensión en los picos de corriente (WiFi+BLE) → boot loop. No es software.

Estado

Funcionando: USB + BLE HID (auto-switch), WiFi + portal + NTP, stick→mouse, capas + menú, feedback real y mute global por companion, config editable por JSON (GET/POST /api/config en LittleFS, se edita y empuja desde el companion sin recompilar) y espejo de pantalla (el companion sirve un mirror live del display por SSE).
La medición de batería en el pad se descartó: la pantalla de la fuente ya muestra la tensión del pack 2S, y GPIO9 (ex-divisor) hoy maneja el NeoPixel — cfg::BATTERY_ENABLED=false y no reactivar sin reasignar el ADC (si no, le metés 2.7V DC a la línea de datos del NeoPixel).

Roadmap

  • PWA directa al pad — servida por el propio pad (net/WebUi.cpp, sin PC): ver estado live, saltar de capa, pad virtual 2×5 que dispara las teclas/encoder, y editor de config (nombre/color/labels, preservando acciones). Endpoints GET /api/ui + POST /api/cmd + /api/config. Frontend verificado headless por npm run test:padwebui (Playwright + mock del contrato, extrae la página real del firmware). Falta confirmar el apply on-device (flasheo pendiente).
  • Gestures editables — el long-press hardcodeado se quitó a propósito; re-agregar acciones secundarias como gestos editables (no hardcode) es rediseño, despriorizado.
  • Pad2 (reinicio limpio) — bifurcación mayor: pad virtual primero, data-driven, stack JS+JSDoc zero-build. Pensado, sin arrancar.

Créditos

La perilla impresa del joystick analógico sale de este modelo en Thingiverse. Gracias a su autor por publicarlo.

Spezifikationen

MCUESP32-S3 (N16, 16MB)
DisplayILI9488 480×320 SPI
EingängeEncoder + Joystick + 5 Tasten
TransporteUSB HID · BLE HID · WiFi
Stromversorgung2S Li-Ion Akku
FirmwareArduino + FreeRTOS dual-core
Demnächst
JSON-Konfiguration
Steuerung per Handy (PWA)
Remote-Screen / Mirror
Mehr Ebenen-Maps
Was funktioniert
  • HID über USB und BLE gleichzeitig, mit Auto-Switch
  • Kontext-Ebenen, vom Display aus navigierbar
  • Analog-Stick als Maus, mit einstellbarer Beschleunigung
  • WiFi + Captive Portal + NTP + OTA
  • Echtes PC-Feedback über den Companion-Daemon
  • Globales Mikrofon-Stummschalten auf OS-Ebene
Was verbessert werden muss
  • Stick-Klick wirkt in X11-Apps nicht (in Prüfung)
  • Akkumessung bereit, aber standardmäßig aus
  • JSON-Konfiguration noch ausstehend (heute zur Compile-Zeit)

Willst du eines?

Bau es selbst. Es gehört jetzt dir.<br/>Alle Codes, Schaltpläne und Anleitungen sind verfügbar.

Project Toolbox

Archivos técnicos listos para flujo embebido diario: abrir, descargar o visualizar por tipo.
3D / STL
Body1.stl
public
Body2.stl
public
box.stl
public
display-case_1.stl
public
display-case.stl
public
front_plate.stl
public
tapa-superior.stl
public
Firmware / Config
companion/config.example.json
source
companion/package-lock.json
source
companion/package.json
source
companion/tsconfig.json
source
platformio.ini
source
src/actions/Action.h
source
src/actions/MacroEngine.cpp
source
src/actions/MacroEngine.h
source
src/app/AppState.cpp
source
src/app/AppState.h
source
src/app/Calibration.cpp
source
src/app/Calibration.h
source
src/app/Config.h
source
src/app/EventBus.cpp
source
src/app/EventBus.h
source
src/app/Pins.h
source
src/app/SettingsRegistry.cpp
source
src/app/SettingsRegistry.h
source
src/app/StateManager.cpp
source
src/app/StateManager.h
source
src/app/Theme.cpp
source
src/app/Theme.h
source
src/app/Types.h
source
src/audio/Audio.cpp
source
src/audio/Audio.h
source
src/inputs/AnalogStick.cpp
source
src/inputs/AnalogStick.h
source
src/inputs/ButtonMatrix.cpp
source
src/inputs/ButtonMatrix.h
source
src/inputs/Encoder.cpp
source
src/inputs/Encoder.h
source
src/inputs/InputManager.cpp
source
src/inputs/InputManager.h
source
src/inputs/StickCalibration.h
source
src/main.cpp
source
src/mapping/Dispatcher.cpp
source
src/mapping/Dispatcher.h
source
src/mapping/KeyMap.cpp
source
src/mapping/KeyMap.h
source
src/net/Net.cpp
source
src/net/Net.h
source
src/net/UiMirror.cpp
source
src/net/UiMirror.h
source
src/net/WebUi.cpp
source
src/net/WebUi.h
source
src/storage/ConfigCodec.cpp
source
src/storage/ConfigCodec.h
source
src/storage/ConfigStore.cpp
source
src/storage/ConfigStore.h
source
src/storage/DefaultConfig.cpp
source
src/storage/DefaultConfig.h
source
src/storage/Nvs.cpp
source
src/storage/Nvs.h
source
src/transport/BleHidTransport.cpp
source
src/transport/BleHidTransport.h
source
src/transport/ITransport.h
source
src/transport/TransportRouter.cpp
source
src/transport/TransportRouter.h
source
src/transport/Transports.h
source
src/transport/UsbHidTransport.cpp
source
src/transport/UsbHidTransport.h
source
src/ui/IconKit.h
source
src/ui/Layout.h
source
src/ui/Leds.cpp
source
src/ui/Leds.h
source
src/ui/Menu.cpp
source
src/ui/Menu.h
source
src/ui/MenuModel.cpp
source
src/ui/MenuModel.h
source
src/ui/monitor.cpp
source
src/ui/monitor.h
source
src/ui/NavModel.cpp
source
src/ui/NavModel.h
source
src/ui/Skin.h
source
src/ui/Skins.cpp
source
src/ui/StatusPanel.cpp
source
src/ui/StatusPanel.h
source
src/ui/UiKit.h
source
Documentación
companion/README.md
source
README.md
source
MAKER / PRINTSPiezas 3D imprimibles7 piezas de este proyecto con visor 3D y descarga directa.
Ver en Maker →
Funktionsfähiger Prototyp. Bau deinen eigenen.
Soll ich es für dich bauen? Schreib mir