// public/docs-app/v1/doc/lru_alfiler_resiliencia_comunicaciones_s230.md
// 2026-07-16T13:44:40Z

# LRU · Alfiler · Resiliencia y tolerancia a fallos de comunicaciones · s230

**Ubicación canónica**: `public/docs-app/v1/doc/lru_alfiler_resiliencia_comunicaciones_s230.md`
**Sesión origen**: s230 · 2026-07-15 · Manuel & Claude
**Actualizado**: s231 · 2026-07-16 · **R0 ejecutada** (auditoría fina · hallazgos en §1bis · revisan §1, R4, R5 y D-abiertas)
**Actualizado**: s232 · 2026-07-16 · **R2 CONSUMADA — cutover ejecutado**: el broker de producción es la `.102` (RabbitMQ 4.2 docker) · la `.200` queda de respaldo real (L1/L2 separadas por fin) · dos decisiones en el día D: D-s232-A (TLS del web-mqtt 15676 nativo en el broker, revierte D-s231-C) y D-s232-B (login anónimo `anonymous_login_user=lru_app` — el hueco invisible del gemelo: la PWA conecta sin credenciales y eso no viaja en definitions) · inputs nuevos para R1 y D-abierta-4 (criterio 80/443 de Manuel)
**Naturaleza**: alfiler de estudio y estadificación · anillo 2 · **NO canónico · documento vivo para completar en sesiones futuras** (así nace por diseño, a petición de Manuel)
**Tensión**: `T-S230-RESILIENCIA-COMS` · este alfiler ES el estudio que la tensión pide antes de obra
**Relación con el corpus**: corona el eje OTA v2 (s226–s230: manifiesto+sha, OTA propia del clásico, criterio de salud + rollback en ambas clases) y generaliza su lección — *el sistema debe sobrevivir a sus averías* — de la OTA a TODA la comunicación. Dialoga con TD-s228-1 (caducidad del ancla TLS: un fallo single-point ya fechado), TD-s209-1 (relay wss broadcast), T-S220-FLAG-DISPONIBILIDAD y el motor de vitalidad s221 (la PWA ya sabe *contar* la salud de un canal). Hereda P12 (el hardware físico es sustrato: la vuelta al cable siempre existe).
**Propósito**: fijar el postulado, inventariar el sustrato real que YA existe, mapear capas de defensa contra tipos de fallo, y estadificar las rebanadas R0–R6 con triggers.
**No-propósito**: NO toca código en s230 · NO diseña mesh entre devices · NO aborda seguridad/autenticación (hilo hermano futuro) · NO abre frentes sin trigger (P11).

---

## §0 · Voz fundacional (Patrón 3, verbatim)

> *"hay que contemplar fallos de comunicaciones simples y catastroficos para que algun fallo no suponga una situacion critica"* — Manuel, s230

Destilado como postulado del eje:

> **«Ningún fallo único de comunicaciones debe suponer una situación crítica.»**

Y el corolario de proximidad:

> *"deberiamos soportar la conexion bluetooth como modo alternativo de comunicacion cuando estamos junto al hardware"* — Manuel, s230

## §1 · Estado actual · el sustrato YA existente (auditado s230)

La sorpresa del inventario: gran parte de la infraestructura existe; lo que falta es **doctrina articulada, paridad entre clases y observabilidad de la degradación**.

| Pieza | Clásico (rpc1cl · mgos 2.20) | S3 (01_factory · IDF 5) | PWA |
|---|---|---|---|
| Broker principal (internet) | `mqtt` boxdin.ddns.net:8080 | `MQTT_URI_PRIMARY` ídem | canal mqtt-ws |
| Broker alternativo | `mqtt1` 192.168.1.200:21883 · mgos **alterna nativo** | `MQTT_URI_BACKUP` ídem · alterna tras `MQTT_FAILS_TO_SWAP=5` fallos (s224) | — (un solo endpoint configurado) |
| Relay wss | — | — | s209b (TD-s209-1: broadcast sin filtrado) |
| BLE junto al hardware | GATT compilado (rpc-gatts · 7 métodos RPC era demo) · **apagado PERSISTENTE en runtime** (R0·H3) | chip BLE presente · **sin puerto GATT** | histórica; hoy sin camino BLE activo |
| AP wifi de emergencia | `wifi.ap` trigger GPIO 27 · 60 s | — | — |
| Serie USB | siempre (TeraTerm, D-s228-D) | siempre (idf.py monitor) | n/a |
| Salud/rollback OTA | s230: trial NVS + 120 s + rolled_back | s226+s230: nativo + watchdog zombi | pinta rolled_back, otaTrace |
| Vitalidad de canal | — | — | motor s221 (`disponibilidadCanal`) |

**Lectura**: los devices ya sobreviven a un broker caído (capa 1) y el criterio de salud OTA ya es multi-broker-coherente (el CONNACK de cualquier broker valida). Los huecos gordos: la **PWA no tiene failover** (si su endpoint cae, se queda ciega aunque los devices estén sanos en el backup), el **BLE existe pero no tiene camino operativo** (R0-s231: peor — está apagado persistentemente, ver §1bis·H3), y **nadie cuenta en qué capa está funcionando el sistema**.

## §1bis · R0 ejecutada · hallazgos empíricos (s231)

Auditoría solo-lectura sobre: lib mqtt de mgos 2.20 (fuente de la release), `esp_mqtt_port.cpp` (01_factory), bt-common 2.20 + build real del clásico (`build/gen/`), `src/comms/` de pwa6, y `C:\lru\docker\` (compose + rabbitmq + Caddyfile). Cada hallazgo revisa la fila correspondiente de §1.

**H1 · Alternancia del clásico — sin failback.** `mgos_mqtt_conn_reconnect()`: backoff 2→60 s doblando (±10% fuzz); al alcanzar el máximo (~122 s de esperas acumuladas por broker), `mqtt_switch_config()` conmuta mqtt↔mqtt1 y resetea el backoff al mínimo del cfg nuevo. Ping-pong perpetuo si ambos caen — nunca se rinde. **Al conectar en cualquiera se queda ahí indefinidamente**: no existe vuelta deliberada al primario (confirma la premisa de R5). Cola QoS1+ máx 5 mensajes (overflow descarta el más viejo). El MQTT del clásico va por 8080 **plano** — F2 hoy no lo muerde (confirma la nota de §4).

**H2 · Failover S3 — sin failback, endpoints en piedra.** Conmuta tras 5 `MQTT_EVENT_DISCONNECTED` exactos (`MQTT_EVENT_ERROR` no cuenta); reconexión esp-mqtt por defecto ~10 s → ~50 s por lado; ping-pong perpetuo; sin failback (simétrico a H1). **URIs y credenciales hardcodeadas en `#define`** — cambiar de broker en la S3 exige reflashear; en el clásico es config viva (`to/cfg`/RPC). Dato central para D-abierta-4.

**H3 · GATT del clásico — compilado pero MUERTO por persistencia.** El binario lleva NimBLE + `rpc-gatts` (canal RPC-sobre-GATT · sec_level 0 · frame 4096) con 7 métodos RPC registrados (`Example.Increment`, `Example.CallPeer`, `iot1`, `iot2`, `led0`, `led1`, `status1` — era demo: ninguno es el protocolo operativo). PERO `bt.keep_enabled=false` (default; mos.yml no lo toca) hace que bt-common, al primer `MGOS_NET_EV_IP_ACQUIRED`, ejecute `bt.enable=false` + **`save_cfg()`**: el apagado **se persiste en conf9** y sobrevive reboots — el BLE NO vuelve ni en F4. **La capa L3 está muerta en toda la flota provisionada.** Revivirla es barato (config `bt.keep_enabled=true` + `bt.enable=true` + reboot, sin reflashear) pero es obra de R4, no de R0. Corrección a §1: la fila BLE del clásico decía «vivo en firmware» — lo exacto es «compilado en firmware, deshabilitado persistentemente en runtime».

**H4 · PWA — endpoint único, reintento ingenuo, publish descarta.** `MqttAdapter` sobre mqtt.js 5.x con defaults: **reintento cada 1 s al MISMO endpoint, sin backoff, para siempre**; `publish()` sin conexión **descarta el mensaje** (warn en DebugBus, sin cola). Endpoint solo overridable en build (`VITE_MQTT_URL`), no en runtime. El transporte ws1 (relay Node-RED) sí lleva backoff propio (1 s ×1.5 → 30 s), también boxdin-only. Confirma y agrava el hueco que motiva R1.

**H5 · La foto real de los brokers (corrige la premisa de §1).** El broker vivo (boxdin.ddns.net:8080 · 192.168.1.200:21883 · ws :15676) es **otra máquina** (LAN `.200`), con los puertos del router redirigidos hacia ella. Detalle clave del NAT (capturas s231, router Movistar): la regla `mqtt_2` traduce **WAN 8080 → LAN 21883** en la `.200` — el `boxdin:8080` de los firmwares y el `.200:21883` del respaldo son **el mismo listener**; `mqtt_ws`/`mqtt_wss` redirigen 15675/15676 a la `.200`. La máquina de trabajo es la `.102` y ya recibe 80/443 (Caddy) y 1880 (Node-RED) del router → el cutover del broker es re-apuntar 3 reglas NAT a la `.102`, transparente para flota y PWA (plan en `lru_plan_migracion_broker_s231.md`). En la máquina de trabajo actual vive el **broker futuro, aún no en producción**: stack docker `C:\lru\docker\` = RabbitMQ 4.2 (MQTT :1883 · web-mqtt :15675 `/ws` · STOMP · Streams · **shovel disponible para puentear brokers durante una migración**) + Redis 7.4 + Caddy 2. En s231 Manuel abre la preparación de la migración (configurar y ensayar; cutover en sesión dedicada). Estado del stack al auditarlo: `definitions.json` sin el usuario `aula2/admin1` que usan ambas clases y la PWA (users placeholder con `password_hash` vacío); puertos distintos de los que la flota conoce — si la redirección del proxy se re-apunta a esta máquina, el cutover puede ser **transparente para la flota** (cero cambios de firmware).

**H6 · Incompatibilidad principal para la migración: retained + wildcard en RabbitMQ.** El plugin MQTT de RabbitMQ **no entrega mensajes retained a suscripciones con wildcard** (`#`/`+`) — limitación documentada y abierta (rabbitmq-server #8824). La PWA se suscribe a `#` y la presencia depende del `from/status` retained → tras el cutover, al recargar la PWA no pintaría presencia hasta el siguiente latido. Vías candidatas (se arbitran en la sesión de migración): consulta activa `to/status` al conectar (el firmware ya la soporta en ambas clases), suscripciones explícitas por device desde el registry, o re-publicación periódica de status. QoS2 tampoco existe en RabbitMQ-MQTT (el clásico declara `max_qos=2`; el tráfico real observado es QoS0/1 — vigilar, no bloquea).

## §2 · Taxonomía de fallos

- **F1 · simple**: cae el broker principal (proceso, hosting, DDNS). Internet y LAN vivos.
- **F2 · TLS/certificados**: el ancla rota o caduca (TD-s228-1, fechado 2027-11) — el transporte está vivo pero el canal seguro no valida. Es el fallo single-point más *predecible* del sistema.
- **F3 · sin internet, LAN viva**: router local funcionando, WAN caída. Los devices y la PWA comparten LAN.
- **F4 · catastrófico**: sin internet Y sin infraestructura LAN (router caído, apagón parcial). Solo queda la proximidad física.
- **F5 · junto al hardware**: no es un fallo sino un modo — el operario está al lado de la placa y quiere hablarle directamente, con o sin red.

## §3 · Capas de defensa (el postulado hecho arquitectura)

```
L0  broker principal (internet · boxdin)          ← F1 lo tumba
L1  broker alternativo (internet u otra ruta)     ← F1 sobrevive
L2  broker local en LAN (192.168.1.200 hoy)       ← F3 sobrevive
L3  BLE punto a punto (junto al hardware)         ← F4/F5 sobreviven
L4  AP propio del device + serie USB (P12)        ← último recurso, ya existe
```

Regla de oro de la escalera: **cada capa degrada capacidad, nunca la miente** — quien opera en L3 sabe que está en L3 (alcance de un device, sin flota), y el sistema lo *cuenta* (§R5). ~~Hoy L1/L2 están colapsadas en una sola pieza (el backup ES el broker local)~~ **Desde s232 (cutover R2): L0 = `.102` (RabbitMQ 4.2) · L1/L2 = `.200` encendida como respaldo real** — por primera vez el backup es una máquina distinta del primario.

## §4 · Matriz fallo × capa

| | L0 | L1 | L2 (LAN) | L3 (BLE) | L4 |
|---|---|---|---|---|---|
| F1 broker caído | ✕ | ✓ | ✓ | ✓ | ✓ |
| F2 TLS roto | ✕* | ✓ (si ancla distinta) | ✓ (LAN sin TLS público) | ✓ | ✓ |
| F3 sin internet | ✕ | ✕ | ✓ | ✓ | ✓ |
| F4 catastrófico | ✕ | ✕ | ✕ | ✓ | ✓ |
| F5 junto al hw | (✓) | (✓) | (✓) | ✓ **natural** | ✓ |

\* F2 hoy solo muerde la OTA https del clásico (MQTT va por 8080 plano); cuando el MQTT gane TLS, F2 se vuelve transversal — razón para que L1/L2 no compartan ancla con L0.

## §5 · Estadificación · rebanadas R0–R6 (cada una con trigger, ninguna abierta en s230)

- **R0 · Auditoría fina (solo lectura). ✔ EJECUTADA en s231** — hallazgos H1-H6 en §1bis. Respuestas cortas: failback NO existe en ninguna clase (se quedan donde conectan); S3 ping-pong perpetuo cada 5 desconexiones, endpoints en `#define`; el GATT del clásico está compilado pero apagado persistentemente (`bt.keep_enabled=false` + `save_cfg`); el `.200:21883` es OTRA máquina (broker vivo actual) y en la máquina de trabajo espera el broker futuro (RabbitMQ docker); la PWA reintenta cada 1 s a un único endpoint y descarta publishes sin conexión.
- **R1 · La PWA gana failover de endpoint.** Lista ordenada de brokers/relays en vez de endpoint único + reconexión con la misma escalera que los devices. Sustrato: `CommsEvent.channel` + motor de vitalidad s221. *Trigger natural: la primera vez que boxdin caiga con la flota sana.* **Input s232 (criterio 80/443 de Manuel)**: el único puerto garantizado en redes restrictivas es el 443 → la cascada natural es primario `wss://boxdin.ddns.net:15676/ws` (TLS nativo del broker) → respaldo `wss://boxdin.ddns.net/mqtt` (443 vía Caddy, atraviesa cualquier red) — la ruta `/mqtt` del Caddyfile quedó documentada como respaldo estratégico en el cutover.
- **R2 · Broker local formalizado. ✔ CONSUMADA en s232 (cutover ejecutado).** Concreción tras H5: (a) configuración para la flota **✔ s231**, (b) ensayo host-side **✔ s231 (T1-T6)**, (c) **cutover ✔ s232**: 3 reglas NAT re-apuntadas `.200`→`.102`, PWA reconectada sin tocar un byte, placas de referencia aterrizadas y **ciclo OTA completo verificado en ambas clases** a través del broker nuevo. Dos baches del día D, ambos resueltos en sesión: el 15676 pasó a TLS nativo del broker (D-s232-A, revierte D-s231-C — web-mqtt es competencia del broker, no del proxy; Caddy conserva el web 80/443) y el login anónimo de la PWA (D-s232-B: `anonymous_login_user=lru_app` en rabbitmq.conf — la PWA conecta sin credenciales; el broker viejo lo permitía por config invisible a definitions; el gemelo por usuarios no lo capturó → lección: **un gemelo se completa con config de auth, no solo con usuarios**). La `.200` queda ENCENDIDA como respaldo real → **L1 y L2 separadas por fin**. H6 quedó en no-problema (D-abierta-6 teórica). Camino verificado con `test-broker-nuevo.mjs --host boxdin.ddns.net --port 8080` (la ruta exacta de la flota, hairpin incluido).
- **R3 · Telemetría de degradación.** Cada parte publica/pinta en qué capa opera: el device en `from/status` (`broker: primario|respaldo`— la S3 ya lo loguea en serie, falta el campo), la PWA en la UI (chip de capa, hermano de los chips de base OTA). La degradación observable es la mitad del postulado. *Trigger: R1/R2 (sin capas no hay qué contar).*
- **R4 · BLE operativo junto al hardware.** El camino más ambicioso: Web Bluetooth en la PWA contra el GATT del clásico (que ya existe) + puerto GATT equivalente en la S3. Alcance honesto: mando y diagnóstico de UN device en proximidad, no flota. *Trigger: primera situación real F4, o necesidad de campo (instalación sin red).*
- **R5 · Política de failback.** Cuándo se vuelve a una capa superior (¿al reconectar? ¿con histéresis? ¿manual?). Premisa CONFIRMADA en R0 (H1/H2): ninguna clase vuelve a casa deliberadamente — se quedan donde conectaron hasta que esa capa falle. *Trigger: R1-R3 en uso real.*
- **R6 · Ensayo de fallos (la prueba negativa institucionalizada).** El patrón de s230 (envenenar a propósito y ver el sistema defenderse) elevado a método: guion de simulacros F1-F4 repetible por sesión o por release. *Trigger: cuando R1-R3 existan.*

## §6 · Invariantes y guardas

1. **Ningún fallo único → situación crítica** (postulado §0).
2. **Ninguna pieza depende de UNA URL, UN broker o UN ancla TLS** — TD-s228-1 es el contraejemplo fechado que lo motiva.
3. **Toda degradación es observable** — el sistema cuenta en qué capa está (lección del ciclo OTA observable s229).
4. **La capa degradada no miente**: capacidades reducidas se muestran reducidas; nada simula conexión plena.
5. **La vuelta al cable siempre existe** (P12): serie USB + AP de emergencia no se retiran jamás.
6. **La salud OTA es multi-capa**: llegar a *cualquier* broker de la escalera valida la imagen (ya cierto en s230; se preserva por diseño al añadir capas).

## §7 · Qué NO entra (P11)

- Mesh device↔device y store-and-forward entre placas.
- Colas offline con replay completo de telemetría (el grabador del eje tiempo es otro hilo).
- Seguridad/autenticación por capa (hilo hermano "seguridad" que Manuel nombró en s230 — merecerá su propio alfiler; aquí solo se anota que L2/L3 relajan TLS y eso tiene precio).
- Web STOMP / transportes nuevos (TD-s209-1 se cita, no se resuelve).
- Redundancia de hosting/DDNS del lado servidor.

## §8 · Decisiones abiertas (se arbitran al materializar)

- **D-abierta-1**: orden de la escalera por clase de device — ¿la S3 (pantalla, operario delante) prioriza igual que el clásico (ciego)?
- **D-abierta-2**: hogar físico del broker local L2 (máquina 192.168.1.200 actual vs dedicado tipo raspberry vs el propio servidor boxdin en LAN).
- **D-abierta-3**: Web Bluetooth en la PWA (compatibilidad, UX de emparejamiento) vs app auxiliar para L3.
- **D-abierta-4**: unificar la política de fallback de las dos clases (mgos alterna nativo con su esquema mqtt/mqtt1; la S3 lleva lógica propia `s_on_backup`) — ¿se declara un contrato común de escalera o cada firmware con su idioma? **Dato R0 (H2)**: la S3 lleva URIs/credenciales en `#define` — cualquier contrato común pide antes sacar los endpoints a config (NVS) en la S3. **Input s232 (criterio 80/443)**: si algún día un device opera en red ajena que bloquee el 8080, la vía realista es MQTT-sobre-WSS — la S3 lo tiene gratis (`esp_mqtt` de IDF soporta transporte ws/wss, solo cambia la URI); el clásico (mgos 2.20) no lo trae de serie. Anotado para cuando el contrato común se arbitre.
- **D-abierta-5**: el campo de capa en `from/status` (¿`broker`? ¿`commsLayer`?) y su viaje por types.ts/ingesta — aditivo, pero merece nombre pensado.
- **D-abierta-6 (nace en R0·H6 · degradada a TEÓRICA en s231)**: la sonda al broker vivo (S2, `scripts/sonda-broker-viejo.mjs`) demostró que el retained via `#` **tampoco se entrega hoy** — el broker de la `.200` resultó ser RabbitMQ 3.13.3 con la misma limitación. La PWA lleva viviendo sin ello desde siempre; el cutover no degrada nada. Queda como mejora futura de presencia (consulta activa `to/status` al conectar / suscripciones por device), ya sin atadura a la migración.

## §9 · Disciplina

Alfiler propone, núcleo dispone, Manuel arbitra. Nace deliberadamente como **documento vivo**: cada sesión futura que toque comunicaciones vuelve aquí, actualiza §1 (inventario), tacha rebanadas y refina D-abiertas. R0 puede revisar todo lo anterior — eso es el camino Y funcionando. Ninguna rebanada se abre sin trigger (P11); s230 entrega el estudio, no la obra.

---

*Anillo 2 · s230 · nace tras validar en placa el criterio de salud + rollback automático en ambas clases de la flota (el precedente que demuestra el patrón: contemplar el fallo, ensayarlo a propósito, y ver al sistema defenderse solo).*
