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.
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.
| Componente | Tipo | Publica | Se suscribe a |
|---|---|---|---|
| bus/ | núcleo | — | — |
| senses/touch_sense | Sense | touch.down · touch.pet · touch.poke | — |
| senses/rc522 | Sense | nfc.tag | — |
| expressions/round_face | Expression | — | face.emotion · face.say · face.look · boot.* |
| expressions/led_ring | Expression | — | face.emotion · led.mood |
| brain/brain_cloud | Brain | brain.reply · brain.error · led.mood | brain.ask |
| berry_host | Reflexes | (lo que emitan los scripts) | * · system.reload |
| webui | núcleo | system.reload · time.synced | — |
| main | cableado | face.* · 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.
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.
| Tarea | Núcleo | Stack | Prio | Periodo | Por qué existe |
|---|---|---|---|---|---|
| main | 0 | 8192 | 1 | una vez | levanta cada subsistema y termina |
| bus | 0 | 6144 | 5 | 10 ms ocioso | drena la cola; todos los handlers corren aquí |
| brain | 0 | 10240 | 4 | bloqueante | TLS + HTTP a Claude; stack profundo para mbedTLS |
| face | 1 | 6144 | 4 | 40 ms | parpadeo, sacada, render, push del frame |
| ring | 1 | 3072 | 3 | 40 ms | animación de respiración / cometa en el WS2812 |
| touch | 1 | 3072 | 4 | 25 ms | almohadilla capacitiva, antirrebote de 2 muestras |
| rc522 | 1 | 3072 | 4 | 200 ms | sondea 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.
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.
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.
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.
Adaptador en dispositivo
WiFi + una API key. Una conexión TLS persistente, reutilizada entre preguntas.
Servidor hub
Mismo contrato por LAN. Desbloquea webhooks, integraciones OAuth y memoria a largo plazo.
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.
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ón | An | Al | Abre | Guiño | Ceja | Parpadeo |
|---|
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.
A dónde se va la memoria
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).
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.
Añadir algo nuevo
Escribe un reflejo
Unas líneas de Berry en el editor web. Sin toolchain, sin recompilar, vivo en segundos.
Añade un Sense
Un .cpp, una tarea y bus().publish(…). Nada existente cambia.
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.