Desktop Buddy

Arquitectura del firmware

ESP-IDF v6.0.2 · targets esp32s3 (por defecto) / esp32 · núcleo C++ + scripting Berry · app 1,29 MB de 3 MB
§1 La única regla

Todo es un evento en el bus

Ningún driver llama nunca a otro driver. Un Sense publica; lo que le importe se suscribe. Una sola tarea drena la cola, así que los handlers corren en un único contexto y nunca necesitan locks propios. Esta única decisión es la razón de que se pueda añadir un sensor nuevo sin tocar una línea del código existente.

Las suscripciones casan con un nombre exacto (touch.pet), una familia (touch.*) o con todo (* — así escuchan el tracer de depuración y el host de Berry). Los eventos publicados dentro de un handler caen en el siguiente drenado, no en el actual, lo que evita que las cascadas recursen.

Publicar
Traza serie
Elige un evento arriba para verlo propagarse. El borde izquierdo marca el núcleo en el que corre la tarea.
§2 Componentes

Cuatro primitivas, un componente cada una

Cada funcionalidad tiene que presentarse como un Sense (entrada → eventos), una Expression (acciones → salida), un Reflex (lógica evento → acción, en script) o una Skill (capacidad mediada por el LLM, v2+). La estructura de directorios sigue la misma división.

ComponenteTipoPublicaSe suscribe a
bus/núcleo
senses/touch_senseSensetouch.down · touch.pet · touch.poke
senses/rc522Sensenfc.tag
expressions/round_faceExpressionface.emotion · face.say · face.look · boot.*
expressions/led_ringExpressionface.emotion · led.mood
brain/brain_cloudBrainbrain.reply · brain.error · led.moodbrain.ask
berry_hostReflexes(lo que emitan los scripts)* · system.reload
webuinúcleosystem.reload · time.synced
maincableadoface.* · led.mood · boot.*brain.reply · *(traza)

Fíjate en lo que main es: no un objeto-dios, solo cableado más el único reflejo propiedad del framework, el que convierte una respuesta del Brain en expresiones. Todo lo demás es un componente que se podría borrar sin romper a sus vecinos. El contrato completo — con dueños por prefijo — está en docs/event-registry.md.

§3 Concurrencia

Siete tareas en dos núcleos

La red y TLS viven en el núcleo 0 junto al despachador del bus; todo lo que debe ir fluido — el renderizado, los sensores, el anillo LED — vive en el núcleo 1. La regla general: cualquier cosa que pueda bloquear durante segundos va al núcleo 0 y habla con el resto del sistema solo a través del bus.

TareaNúcleoStackPrioPeriodoPor qué existe
main081921una vezlevanta cada subsistema y termina
bus06144510 ms ociosodrena la cola; todos los handlers corren aquí
brain0102404bloqueanteTLS + HTTP a Claude; stack profundo para mbedTLS
face16144440 msparpadeo, sacada, render, push del frame
ring13072340 msanimación de respiración / cometa en el WS2812
touch13072425 msalmohadilla capacitiva, antirrebote de 2 muestras
rc522130724200 mssondea tags NFC por SPI

El Brain nunca bloquea el bus: los handlers de brain.ask solo hacen strdup del prompt a una cola de 4 y vuelven. Si la cola está llena la pregunta se descarta, en vez de atascar a la criatura entera.

§4 Portabilidad

La placa es una opción de compilación, no un fork

Dos targets soportados, un solo código. El ESP32-S3 es la referencia; el ESP32 clásico corre la misma cara a color, pero sin PSRAM tiene que construir el frame por bandas horizontales en vez de cachear los niveles de ojo. Lo que cambia son constantes y la tabla de particiones — no hay una segunda ruta de renderizado que mantener.

Target de compilación

        

El truco que hace posible el clásico: los 460 KB de PSRAM que la cara parece necesitar son la caché de niveles de ojo, no el renderizado. Dibujar necesita un frame, y un frame se puede construir por bandas. En el S3 kBandH == H — una sola banda, exactamente la ruta de siempre, para que el bandeado no pueda regresionar el target que ya funciona.

§5 El Brain

Un contrato, tres cerebros posibles

El firmware nunca nombra a un proveedor de IA. Publica brain.ask y espera brain.reply. Hoy un adaptador en el dispositivo cumple ese contrato llamando a Claude Haiku directamente por TLS; mañana un servidor hub puede servir el contrato idéntico; sin red alguna, nadie lo cumple y los reflejos simplemente siguen corriendo.

Por defecto · hoy

Adaptador en dispositivo

WiFi + una API key. Una conexión TLS persistente, reutilizada entre preguntas.

Opcional · v2

Servidor hub

Mismo contrato por LAN. Desbloquea webhooks, integraciones OAuth y memoria a largo plazo.

Siempre

Sin cerebro

Sin red, los reflejos siguen disparando. El buddy está más callado, nunca roto.

// device → brain
{ "event": "touch.pet", "personality": "…", "sensors": {…} }

// brain → device (se parsea a la defensiva: al modelo le encantan las vallas
// ```json, así que el framework extrae el {…} más exterior en vez de fiarse)
{ "utterance": "All forgiven!", "emotion": "happy" }

En esta ruta viven dos detalles ganados a pulso: el parser de respuestas tolera vallas de markdown, y la conexión TLS se mantiene viva porque en el ESP32 clásico un handshake nuevo cuesta 3–6 s de matemática de curva elíptica por software — suficiente para disparar el watchdog de tareas.

§6 El comportamiento es datos

Las emociones son una tabla, no una ruta de código

Una emoción son cinco enteros, un temperamento de parpadeo y un color. El renderer y el anillo LED leen la misma fila, y por eso los ojos y el halo no pueden discrepar nunca. Pulsa una fila — los ojos de abajo los dibuja el mismo algoritmo que corre el firmware.

EmociónAnAlAbreGuiñoCejaParpadeo
neutral

Por encima de la tabla está la capa que nadie recompila: los reflejos Berry. Los scripts se suben desde la propia web del dispositivo, que escribe el archivo y publica system.reload; el host derriba la VM y la reconstruye en sitio. Editar comportamiento nunca implica un cable de flasheo.

# packs/zero/reflexes/main.be — recarga en caliente, sin recompilar
buddy.on("touch.pet", def (ev)
  buddy.face.emotion("happy")
  buddy.led.mood("excited")
  buddy.say("The user just petted you gently.")   # → brain.ask
end)

La VM vive entera en la tarea de despacho del bus, así que los scripts no necesitan locks — y, en igual medida, un script que entre en bucle infinito congela todos los reflejos. Esa restricción es el precio de la simplicidad.

§7 Presupuestos

A dónde se va la memoria

Imagen de aplicación · ESP32-S3
1,29 MB usadospartición de 3 MB

Incluye la VM de Berry, mbedTLS y la pila gráfica. En el ESP32 clásico son 1,27 MB de una partición de 1,87 MB (35% libre).

RAM interna · stacks de tareas
≈ 39 KB entre 7 tareasde 512 KB de SRAM

Más unos 50 KB adicionales mientras hay una sesión TLS abierta.

El frame de trabajo de 240×240 (115 KB) vive en RAM interna — es ~2× más rápida de dibujar que la PSRAM y cabe; lo que va a PSRAM son las tres cachés de nivel de ojo (3 × 115 KB). En el ESP32 clásico no hay PSRAM: el frame se construye en bandas de 240×48 (23 KB) y no hay caché. Los buffers de DMA y WiFi deben quedarse siempre en RAM interna. Dos crashes enseñaron estos límites por las malas: app_main desbordando su stack por defecto de 3584 bytes al levantar la pantalla y la VM a la vez, y un desbordamiento del buffer de ajuste de texto que corrompía el heap con respuestas largas. Ambos están arreglados y documentados en las notas de hardware para que nadie los reaprenda.

§8 Extender

Añadir algo nuevo

Comportamiento nuevo

Escribe un reflejo

Unas líneas de Berry en el editor web. Sin toolchain, sin recompilar, vivo en segundos.

Sensor nuevo

Añade un Sense

Un .cpp, una tarea y bus().publish(…). Nada existente cambia.

Salida nueva

Añade una Expression

Suscríbete a un evento de acción y mueve el hardware. El mismo aislamiento.

Los límites honestos del diseño actual, que conviene conocer antes de construir encima: la tabla de emociones sigue compilada dentro en vez de venir de un pack; la llamada al Brain no es en streaming, así que la cara no puede reaccionar a media frase; y todavía no hay ruta de audio — los chirps y el bucle de voz push-to-talk son el siguiente hito. Los agujeros conocidos del bus están listados en docs/event-registry.md.