# LRU · Publicación de firmware — documento vivo

*Nace en s227 (eje OTA v2). Describe el ciclo completo de publicación de firmware para que cualquier sesión (humana o Claude) lo repita sin reconstruir contexto. La herramienta es `scripts/gen-manifest-s227.ps1` (PowerShell 5.1).*

---

## Piezas

| Pieza | Dónde | Verdad de versión |
|---|---|---|
| Firmware S3 (ESP-IDF) | `C:\esp32\s3_lcd35` → `build\factory.bin` (01_factory retirado a referencia en s238; el script `$s3Project` corregido en s241) | `esp_app_desc` embebido en el bin (declarado en `CMakeLists.txt` `PROJECT_VER`) |
| Firmware clásico (Mongoose) | `C:\Users\manue\.mos\apps-2.20.0\rpc1cl` → `build\fw.zip` → **se publica el bin de app crudo de dentro** (`parts.app.src`, s228) | `manifest.json` dentro del zip (declarado en `mos.yml` `version`) |
| Carpeta de publicación | `C:\react\pwa6\public\firmware\` (dentro del repo, D-s227-A) | — |
| Manifiesto | `public/firmware/firmware.json` | Derivado de los artefactos |

**Convención de versión (D-s227)**: `0.<sesión>.<slice>` unificada para toda la flota.
**Convención de nombre**: `<clase>-<versión_con_guiones_bajos>.bin` → `s3_lcd35-0_227_1.bin`, `rpc1cl-0_227_1.bin`. El primer `-` separa clase de versión; las clases usan `_`, nunca `-`. Guiones bajos en la versión para evitar conflictos de extensión en los SO.

## Ciclo de publicación

```
1. BUMP        .\gen-manifest-s227.ps1 -BumpS3 auto        (o -BumpS3 0.227.2, o -BumpRpc ...)
               edita PROJECT_VER / mos.yml y SALE sin publicar
2. BUILD       idf.py build   (S3)  ·  mos build   (rpc1cl)
3. PUBLICAR    .\gen-manifest-s227.ps1
               copia el artefacto a public\firmware con nombre derivado del
               PROPIO binario, poda la versión anterior de la clase y
               regenera firmware.json (SHA-256 incluido)
4. COMMIT      bin + firmware.json + código de la sesión viajan juntos
5. DEPLOY      el deploy normal de la PWA publica /pwa6/firmware/ (mismo origin)
6. FLASHEAR    TabFirmware (la S3 valida SHA-256 tras la descarga, rebanada B)
```

## Cerrojos de coherencia (por qué el bump nunca publica)

La versión vive **dentro** del artefacto compilado. Subir el número y publicar en el mismo paso produciría un fichero cuyo nombre dice X y cuyo descriptor dice Y — la clase de incidente de la auto-degradación de s226. Por eso:

- El nombre publicado se **extrae del artefacto** (offset `0x30` del app image en la S3; `manifest.json` del zip en el clásico), nunca de los fuentes.
- S3: si `CMakeLists.txt` declara distinta versión que el bin → **throw** («recompila»).
- rpc1cl: si `mos.yml` ≠ zip → aviso de build rancio; se publica la verdad del zip.
- rpc1cl (s228): se publica el **bin de app crudo** extraído del fw.zip (la OTA propia `ota1.c` hace `esp_ota_write` directo y no entiende zips) · **cerrojo**: sha256 del bin extraído vs `parts.app.cs_sha256` del manifest del zip → throw si difieren.
- `firmware.json` deriva clase y versión de los **nombres** de los ficheros ya publicados (regex `^<clase>-<x_y_z>\.bin$`), y elige la versión más alta por clase.

## Reglas de la carpeta `public/firmware/`

- Solo la **última versión por clase** (la poda es automática). El histórico vive en git.
- El precache del service worker no toca `.bin`/`.json` (los `globPatterns` de `vite.config.ts` no los incluyen) — nadie descarga firmware por abrir la app.
- El fetch del manifiesto desde la PWA es same-origin → sin CORS. Cache-buster (`?t=`) obligatorio en el fetch: la caché HTTP sí puede servir un `firmware.json` rancio.

## Redundancia y robustez de OTA — objetivo vivo (articulado s241)

> Requisito de Manuel (s241): la OTA debe poder hacerse desde **los dos orígenes** (boxdin.com y boxdin.ddns.net), por **HTTP y HTTPS**, más **BLE para rescate**, con el **cable como último recurso siempre disponible**. Se documenta aquí para irlo endureciendo en próximas sesiones (robusto y seguro). No se implementa todo de golpe; es el mapa a completar.

### Matriz de vías

| Vía | Estado | Qué falta para operativa |
|---|---|---|
| boxdin.com · HTTPS | Habilitada desde **0.241.3** (mbedTLS a PSRAM; antes fallaba con `MBEDTLS_ERR_SSL_ALLOC_FAILED` por falta de DRAM interna) | Verificar cadena TLS completa del hosting (fullchain) por robustez |
| boxdin.com · HTTP | Firmware lo permite (`CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y`) | Que el hosting sirva `:80` sin forzar 301->https (`esp_https_ota` no sigue redirecciones por defecto) |
| boxdin.ddns.net · HTTP | Camino de diseño original (s226); firmware listo | Servir `/firmware` en la caja ddns + desplegar el bin ahí |
| boxdin.ddns.net · HTTPS | Firmware listo (TLS ya en PSRAM desde 0.241.3) | Cert TLS válido en la caja auto-hospedada |
| BLE · rescate | **No implementado** | Feature nueva (ver abajo) |
| Cable | Siempre disponible (`idf.py flash` / esptool) | — (último recurso, siempre) |

El firmware es **agnóstico al origen**: descarga de la URL que le da el `to/ota`, http o https, cualquier host. Por eso HTTP+HTTPS desde ambos sitios es sobre todo (a) **desplegar el bin en cada ubicación**, (b) que la PWA ofrezca el variant (chips `pwa·https`/`com·https`/`ddns·http` ya existen; faltan `ddns·https` y quizá `com·http`), (c) config de servidor. **Deploy**: `gen-manifest` publica hoy solo a `public/firmware` (boxdin.com/pwa6); la redundancia real pide un **segundo destino** (la caja ddns).

### BLE-OTA de rescate (frente s242)

El canal RPC-GATT tiene tope de sobre 4 KB y el modelo s236 prohíbe transferencias masivas por BLE **como respuesta RPC**. Una **OTA-por-BLE de streaming** es un camino distinto y compatible: el cliente empuja chunks a una característica, el firmware hace `esp_ota_begin`/`esp_ota_write`/`esp_ota_end` incrementalmente con acuse por chunk. Ventajas: **más ligera que HTTPS** (sin búferes TLS -> cabe holgada en la DRAM de la S3) y rescata sin red. Coste: servicio/método GATT nuevo + cliente PWA; a MTU 256 la imagen (~1.8 MB) tarda minutos (aceptable para rescate). Diseño dedicado: servicio GATT de OTA, o extender el plano de mando con `Ota.Begin/Write/End`.

### Robustez y seguridad (a endurecer)

- **Integridad**: la OTA ya verifica **SHA-256** tras la descarga (rebanada B). Mantener.
- **TLS**: para HTTPS robusto el servidor debe mandar la **cadena completa** (el TLS embebido no perdona cadenas incompletas como el navegador). La caja ddns necesitaría su propio cert para servir HTTPS.
- **Autenticidad**: hoy la OTA **no está firmada** (quien controle la URL/DNS podría servir un bin). Futuro: app signing / Secure Boot v2 -> sólo se flashean imágenes firmadas. Alto valor, pero irreversible (fusibles): sesión propia con mucho cuidado.
- **Exposición**: BLE sin pairing (7º motivo del hilo seguridad) y HTTP :80 sin auth (8º) siguen abiertos; un BLE-OTA sin emparejar sería un vector — el hilo seguridad debe cubrir el rescate BLE cuando se diseñe.
- **DNS dinámico**: `boxdin.ddns.net` depende del No-IP DUC; si la IP pública cambia y el DUC no actualiza, las vías ddns caen. `boxdin.com` (hosting fijo) no. La **redundancia de orígenes cubre este fallo** — razón de fondo de querer ambos.

---

## Historia

- Hasta s226 los bins se publicaban a mano en el hosting (`boxdin.com/firmware`) con nombre tecleado — el nombre podía mentir (los `rpc1cl-2.1.0` publicados convivían con `mos.yml version: 1.0`).
- s227 mueve la publicación dentro del repo y automatiza nombre+manifiesto con cerrojos.
- s228: el clásico gana OTA propia (`ota1.c`, sin la lib con candado de licencia de Mongoose) → el artefacto publicado pasa del fw.zip al **bin de app crudo**; su OTA verifica TLS contra `fs/ota-ca.pem` propio (el `ca.pem` de mgos 2.20 no valida las cadenas de 2026; el hosting de boxdin.com sirve DigiCert con cadena incompleta). Monitor serie de referencia durante OTAs: TeraTerm/PuTTY — el console del mos tool está roto (D-s228-D).

*Documento vivo · nace s227 · mantener al cierre de sesión si el ciclo cambia*
