01 · Punto de partida
Qué necesita recordar un dispositivo
En el tema 32 la regla local producía eventos de activación y recuperación. El envío quedaba fuera de la evaluación, para que la alarma siguiera funcionando sin red. Ahora construimos el siguiente eslabón: conservar datos localmente y entregarlos cuando vuelva la conexión, una estrategia conocida como store and forward (guardar y reenviar).
No todo lo que se guarda tiene el mismo propósito. Una configuración es pequeña y cambia poco; una historia de temperaturas crece todo el tiempo; una cola de alarmas conserva elementos hasta recibir una confirmación. Elegir un medio sin distinguir esos usos termina en archivos gigantes, escrituras innecesarias o pérdidas difíciles de explicar.
| Datos | Para qué se guardan | Cuándo se retiran |
|---|---|---|
| Configuración y calibración | Arrancar con parámetros conocidos | Al reemplazarlos por una versión válida |
| Historia reciente de lecturas | Diagnóstico y análisis de un incidente | Al cumplir una antigüedad o agotar su presupuesto |
| Cola de eventos pendientes | Reintentar la entrega sin perder la identidad | Después de la confirmación definida por la aplicación |
El caso guía sigue siendo una cámara frigorífica. Primero calculamos espacio para una lectura cada 10 s; después usamos una traza corta de doce registros para observar cada reinicio y cada reenvío. Los tamaños y límites son decisiones didácticas, no una garantía de capacidad para cualquier placa.
02 · Elegir la herramienta
RAM, flash, NVS, LittleFS y SD
Flash es un medio físico; LittleFS es un sistema de archivos. No son alternativas del mismo nivel. Una partición de la flash del ESP32 puede contener un sistema LittleFS; otra, NVS; otras están destinadas al firmware. La capacidad anunciada de la placa no está completamente disponible para tus registros.
| Opción | Ventaja | Límite principal | Uso habitual |
|---|---|---|---|
| RAM | Rápida; no desgasta la flash | Volátil ante un corte o reinicio normal | Buffer de adquisición y preparación de lotes |
| NVS / Preferences | Acceso por clave y valor | No es la opción natural para un historial grande | Configuración, calibración y versiones |
| Flash con LittleFS | Archivos locales sin añadir una tarjeta | Partición acotada y escrituras con costo | Colas y registros de volumen moderado |
| Tarjeta SD con FAT | Gran capacidad y extracción para analizar en una PC | Conector, alimentación y manejo de extracción/cortes | Historial largo o datos voluminosos |
Valores pequeños: Preferences
En Arduino-ESP32, Preferences utiliza NVS y está orientada a muchos valores pequeños. Es adecuada para guardar el umbral y la versión de una regla; para grandes cantidades de datos, Espressif sugiere un sistema de archivos. Ver la documentación oficial de Preferences.
Guardar diez mil lecturas como diez mil claves distintas complica el recorrido y la retención. Un archivo secuencial permite leer los registros en orden y eliminar segmentos completos cuando dejan de hacer falta. Tampoco conviene actualizar un contador en almacenamiento no volátil en cada vuelta del bucle solo porque existe una función para hacerlo.
Archivos en flash: LittleFS
LittleFS está diseñado para microcontroladores, con resistencia a cortes y distribución dinámica del desgaste. Sus actualizaciones se confirman mediante sincronización o cierre; la integridad depende también del controlador del medio. Esas propiedades no convierten varias operaciones de tu programa en una transacción única. La fuente primaria es el proyecto LittleFS.
Una biblioteca puede conservar un sistema de archivos montable después de un corte y, aun así, tu aplicación perder el último registro que seguía en RAM. Integridad del sistema de archivos y durabilidad del último dato son requisitos distintos.
03 · Hacer las cuentas
Calcular capacidad y autonomía sin red
La pregunta no es solo «¿cuántos megabytes tengo?», sino «¿cuánto tiempo puedo seguir registrando si no puedo liberar nada?». Para registros de tamaño fijo:
Cobertura = cantidad × período de adquisición
Bytes necesarios = ceil(tiempo sin red / período) × bytes por registro
Supongamos una partición de 256 KiB, de la que presupuestamos 192 KiB para registros y 64 KiB para metadatos, rotación y margen. Un KiB equivale a 1024 bytes. Nuestro formato binario tendrá 24 bytes por registro:
8192 × 10 s = 81.920 s = 22 h 45 min 20 s
Es una estimación lógica: la asignación real por bloques y los metadatos del sistema de archivos deben medirse en la partición elegida. Reservar un porcentaje no garantiza que toda esa cantidad se pueda escribir en cualquier implementación.
| Formato / período | Registros | Tiempo cubierto |
|---|---|---|
| 24 bytes cada 1 s | 8192 | 2 h 16 min 32 s |
| 24 bytes cada 10 s | 8192 | 22 h 45 min 20 s |
| 24 bytes cada 60 s | 8192 | 5 días 16 h 32 min |
| 96 bytes cada 10 s (otro formato) | 2048 | 5 h 41 min 20 s |
Para cubrir 24 horas con una lectura cada 10 s hacen falta 8640 registros: 207.360 bytes, o 202,5 KiB, solo de datos. Si querés mantener un margen nominal del 25 %, el presupuesto total asciende a 270 KiB. Con la partición de 256 KiB y ese margen no alcanza: hay que aumentar espacio, reducir frecuencia, resumir como en el tema 31 o aceptar menos cobertura.
Cuando vuelve la conexión
La cola solo se vacía si se entrega más rápido de lo que se genera. Si entran 6 registros por minuto y el enlace entrega 18, el drenaje neto es de 12 por minuto. Una deuda de 120 registros tarda 10 minutos en desaparecer, no 120 / 18. Si el envío solo alcanza 6 por minuto, el atraso nunca baja.
Calculá con la tasa de registros confirmados, incluyendo reintentos, límites del servidor y tiempo de conexión. Recuperarse de un corte no debería saturar el enlace ni impedir que sigan llegando muestras nuevas.
04 · Un formato que se pueda recuperar
Diseñar los registros
JSON por línea es cómodo de inspeccionar y puede ser suficiente para volúmenes pequeños. Debe acotarse el tamaño de cada línea y descartarse una última línea incompleta; si necesitás detectar alteraciones del contenido, agregá una comprobación. CSV ocupa menos, pero requiere documentar columnas y versión. Un formato binario de tamaño fijo simplifica el recorrido y el presupuesto.
Usaremos un registro de 24 bytes, con enteros en orden little-endian (el byte menos significativo primero). Su esquema es parte del protocolo de almacenamiento, no el resultado accidental de cómo el compilador acomoda una estructura:
| Offset | Campo | Bytes | Significado |
|---|---|---|---|
| 0 | Marca I3 | 2 | Reconocer el formato |
| 2 | Versión | 1 | Vale 1 en este tema |
| 3 | Tipo | 1 | 1 = lectura; 2 = evento |
| 4 | Identificador de arranque | 4 | Parte de la identidad estable del registro |
| 8 | Secuencia | 4 | Contador de registros en ese arranque |
| 12 | Tiempo desde arranque | 4 | Milisegundos; no es una fecha UTC |
| 16 | Temperatura con signo | 2 | Décimas de °C |
| 18 | Indicadores | 2 | Bit 0: calidad válida; otros bits reservados |
| 20 | CRC-32 | 4 | Comprobación de los primeros 20 bytes |
La temperatura puede ser negativa, así que se almacena como entero de 16 bits con signo. Los demás campos numéricos del ejemplo son enteros sin signo. El CRC usa el polinomio reflejado 0xEDB88320, inicio 0xFFFFFFFF e inversión final; coincide con zlib.crc32 de Python.
No escribas directamente sizeof(struct) si el archivo debe ser portable: el relleno, la alineación y el orden de bytes pueden variar. El ejemplo serializa cada campo explícitamente. Tampoco alcanza con que un bloque mida 24 bytes: se verifican marca, versión, tipo y CRC antes de aceptarlo.
Identidad y tiempo
El servidor combina identidad del dispositivo + arranque + secuencia. Al reintentar se conservan esos campos. El dispositivo puede asignar un contador de arranque persistido antes de generar registros; debe comprobar que esa reserva terminó correctamente y definir qué pasa al agotar el contador. Un número al azar de 32 bits no garantiza unicidad indefinida.
El identificador de dispositivo se asocia al archivo o a su cabecera; este registro corto no lo repite. Si copiás archivos entre equipos, conservá esa asociación. Para incidentes que pueden durar más que la vuelta a cero del contador de milisegundos, o si hace falta una cronología absoluta, el esquema debe ampliarse con tiempo de 64 bits, fecha y calidad de reloj. El tiempo de envío no reemplaza al tiempo de adquisición.
Un CRC ayuda a detectar daños accidentales; no autentica el registro ni lo cifra. Quien puede modificar el archivo también puede recalcular el CRC. La protección frente a modificaciones deliberadas corresponde a otro requisito.
05 · Guardar y reenviar
Una cola que sobrevive al reinicio
Una cola persistente tiene dos avances distintos: hasta dónde se escribió y sincronizó y hasta dónde se confirmó la entrega. Confundirlos puede borrar datos que solo estaban en un buffer o reenviar toda la historia en cada arranque.
- Generar: asignar identidad y capturar el dato o evento.
- Persistir: agregar el registro a un segmento y confirmar la sincronización. Si falla, informar almacenamiento degradado.
- Enviar: leer desde el primer registro pendiente, sin modificar su identidad.
- Confirmar: comprobar que la respuesta corresponde al registro enviado y al nivel de entrega requerido.
- Avanzar: persistir el cursor confirmado y liberar solo los segmentos cuyo contenido ya quedó cubierto.
La confirmación depende del requisito: un PUBACK de MQTT confirma el tramo de protocolo con el broker; una respuesta de la aplicación puede afirmar que el registro ya está guardado en la base de datos. No son equivalentes. En los ejemplos de este tema, ACK significa que el servidor de aplicación aceptó el registro.
El caso inevitable: el servidor recibió, el dispositivo no sabe
Si el servidor guarda el evento y se pierde el ACK, el dispositivo debe reintentar. El servidor reconoce la identidad y evita crear otro evento lógico. Puede volver a responder ACK al duplicado. Así se obtiene una entrega con reintentos y deduplicación, sin prometer que la red enviará cada paquete una sola vez.
Si el ACK llegó pero se corta la energía antes de persistir el cursor, también se reenvía ese registro después del arranque. Es preferible esa repetición identificable a adelantar el cursor antes de confirmar y perder el dato. Si confirmás varios registros juntos, el ACK debe indicar qué rango quedó aceptado de manera contigua; no alcanza con el mayor número visto si hay huecos.
Cola llena: elegir qué información conservar
Una cola finita no cubre una desconexión infinita. Rechazar el dato nuevo conserva la historia anterior; descartar el más antiguo conserva lo reciente. Ninguna opción conserva todo. Para alarmas importantes puede reservarse una cola o cuota independiente de la telemetría, con un diagnóstico explícito si también se agota.
Un buffer circular de RAM es útil para el historial reciente: cabeza y cola avanzan dentro de un arreglo de tamaño fijo. Llevar esa idea a flash requiere respetar borrado por bloques, metadatos y recuperación; no conviene actualizar dos índices en el mismo sector en cada muestra. Para archivos, una serie acotada de segmentos secuenciales suele ser más fácil de mantener.
06 · Recuperar después de un corte
Cortes de energía y recuperación
Entre «la función recibió el dato» y «el dato quedó en el medio» puede haber varios buffers: los de tu programa, la biblioteca, el sistema de archivos y el controlador. Una escritura exitosa indica lo que promete esa API, no una garantía universal de permanencia frente a cualquier falla eléctrica.
| Momento del corte | Resultado posible | Respuesta prevista |
|---|---|---|
| Antes de escribir un lote RAM | Se pierde el lote pendiente | Acotar su cantidad y su antigüedad |
| Durante la escritura | Último registro incompleto o estado anterior recuperado | Verificar formato y CRC al arrancar |
| Después de sincronizar registros | Registros recuperables según las garantías del medio | Reanudar desde el cursor confirmado |
| Después de enviar y antes de guardar el ACK | Reenvío después del arranque | Deduplicar con identidad estable |
| Durante la actualización de metadatos | Puede quedar la versión anterior | Elegir la copia válida más reciente |
Un arranque que valida antes de continuar
- Montar el sistema de archivos. Si falla, informar y conservar el medio para diagnóstico.
- Enumerar segmentos y validar su versión, identidad de dispositivo y orden.
- Leer registros completos y comprobar marca, versión y CRC.
- Detenerse ante el primer registro inválido; conservar el prefijo válido y aislar el resto para diagnóstico.
- Recuperar un cursor válido, compatible con los segmentos presentes, y reanudar los pendientes.
En el formato fijo del ejemplo, un archivo de 79 bytes puede contener tres registros completos de 24 bytes y siete bytes de una escritura interrumpida. El prefijo recuperable mide 72 bytes. No se agrega otro registro detrás de esos siete bytes: primero se repara la cola del archivo de forma controlada o se inicia un segmento nuevo.
Un CRC incorrecto en medio de un segmento no demuestra que todo el resto sea basura, pero nuestro recuperador adopta una política conservadora: se detiene ahí y no interpreta más registros. Buscar otra marca I3 sin un procedimiento de recuperación podría confundir bytes del contenido con una cabecera.
Metadatos y configuración
Para un cursor o una configuración, se pueden mantener dos copias con generación, contenido y CRC. Se escribe y sincroniza la copia nueva; al arrancar se elige la generación válida más reciente. Otra alternativa es un archivo temporal validado y un reemplazo mediante una operación cuyas garantías se hayan verificado en el sistema usado.
Esto requiere espacio para coexistencia y un orden de operaciones definido. Borrar primero la única copia buena y luego escribir la nueva deja una ventana sin recuperación. Tampoco se debe suponer que dos escrituras independientes a claves o archivos distintos son una sola transacción.
07 · Duración y mantenimiento
Desgaste, lotes y rotación
La flash se programa y se borra con granularidades propias. Cambiar un valor pequeño puede provocar más trabajo físico que su tamaño lógico. El fabricante especifica la resistencia del componente; la vida de la aplicación depende además de la distribución de escrituras, los metadatos y la amplificación del sistema de archivos.
Con un registro de 24 bytes cada 10 s se generan 202,5 KiB lógicos al día. Eso no significa que se programen exactamente 202,5 KiB de flash ni permite calcular años de vida dividiendo la capacidad por ese número. Hay que conocer los ciclos de borrado admisibles y medir el patrón real de uso.
Agrupar sin ocultar la ventana de pérdida
Guardar cuatro registros por lote reduce la frecuencia de sincronizaciones respecto de guardar uno por vez, aunque los bytes de los registros sean los mismos. El precio es que hay registros pendientes en RAM. Justo antes de completar un lote de cuatro pueden quedar hasta tres registros todavía sin persistir, además de lo expuesto si el corte ocurre durante la operación en curso.
Usá dos límites: cantidad máxima de registros por lote y tiempo máximo desde el primer pendiente. Si solo sincronizás al juntar cuatro alarmas y el dispositivo genera una por semana, la primera podría permanecer en RAM varias semanas. Para un evento importante puede corresponder una sincronización inmediata.
Rotar segmentos
En lugar de reescribir un archivo enorme para quitar el primer registro confirmado, escribí segmentos acotados, por ejemplo de unos pocos KiB. Al llenar uno, cerralo y empezá otro. Cuando un segmento queda completamente confirmado o vence según la política del historial, se libera entero. El tamaño concreto se prueba con el medio y el ritmo de datos.
Antes de borrar un segmento confirmado, persistí el avance que permite demostrarlo. Dejá espacio para un segmento nuevo y para los metadatos de recuperación; llenar la partición hasta el último byte puede impedir las operaciones que necesitás para salir de esa situación.
La escritura también tiene latencia. Una sola tarea debe ser dueña de la cola y del sistema de archivos; otras tareas entregan solicitudes mediante una cola RAM acotada. No escribas archivos desde una interrupción del sensor ni mezcles dos tareas actualizando el mismo archivo y cursor sin coordinación.
08 · Firmware en ESP32
Escribir en LittleFS desde C++
Este fragmento para Arduino-ESP32 monta LittleFS sin formatear y agrega registros binarios de 24 bytes. Utiliza la ruta POSIX montada por la biblioteca para comprobar escritura, vaciado del buffer C, sincronización y cierre. La firma de montaje está documentada en el encabezado oficial LittleFS.h; el componente esp_littlefs integra las operaciones con VFS.
La placa necesita una tabla de particiones compatible y un sistema de archivos preparado. El montaje predeterminado usa /littlefs y la etiqueta de partición que espera esa versión de la biblioteca; comprobalo en tu entorno. Si usás otro punto de montaje, ajustá también la ruta de fopen.
#include <Arduino.h>
#include <LittleFS.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h> // fsync, fileno
bool almacenListo = false;
const size_t LIMITE_LOGICO = 192UL * 1024;
void poner16(uint8_t* p, uint16_t v) {
p[0] = uint8_t(v);
p[1] = uint8_t(v >> 8);
}
void poner32(uint8_t* p, uint32_t v) {
for (int i = 0; i < 4; i++) p[i] = uint8_t(v >> (8 * i));
}
uint32_t crc32(const uint8_t* p, size_t n) {
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < n; i++) {
crc ^= p[i];
for (int b = 0; b < 8; b++) {
crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320UL : 0);
}
}
return ~crc;
}
void crearRegistro(uint8_t* r, uint32_t arranque, uint32_t seq,
uint32_t ms, int16_t decimas) {
r[0] = 'I'; r[1] = '3'; r[2] = 1; r[3] = 1;
poner32(r + 4, arranque);
poner32(r + 8, seq);
poner32(r + 12, ms);
poner16(r + 16, uint16_t(decimas));
poner16(r + 18, 1); // lectura válida
poner32(r + 20, crc32(r, 20));
}
// Una única tarea usa esta función. El archivo ya debe estar recuperado.
bool agregarLectura(uint32_t arranque, uint32_t seq,
uint32_t ms, int16_t decimas) {
if (!almacenListo || decimas < -400 || decimas > 1250) return false;
uint8_t registro[24];
crearRegistro(registro, arranque, seq, ms, decimas);
FILE* f = fopen("/littlefs/lecturas.bin", "ab");
if (!f) {
almacenListo = false;
return false;
}
bool ok = fseek(f, 0, SEEK_END) == 0;
const long largo = ok ? ftell(f) : -1;
ok = ok && largo >= 0 && (largo % 24 == 0);
ok = ok && (size_t(largo) + sizeof(registro) <= LIMITE_LOGICO);
if (ok) ok = fwrite(registro, 1, sizeof(registro), f) == sizeof(registro);
if (ok) ok = fflush(f) == 0; // buffer de stdio al sistema de archivos
if (ok) ok = fsync(fileno(f)) == 0; // solicitar sincronización al controlador
if (fclose(f) != 0) ok = false;
if (!ok) almacenListo = false; // no seguir anexando sin diagnóstico
return ok;
}
void setup() {
Serial.begin(115200);
almacenListo = LittleFS.begin(false); // no formatear al fallar
if (!almacenListo) {
Serial.println("No se pudo montar LittleFS");
return;
}
Serial.println("Montado: validar segmentos y cursor antes de adquirir");
// En un archivo nuevo o después de completar la recuperación:
// agregarLectura(arranqueReservado, seq, uint32_t(millis()), 41);
}
void loop() {
// Adquisición, cola y envío se coordinan sin escribir desde una ISR.
}El ejemplo sincroniza cada registro para mostrar el punto de confirmación. La variante por lotes prepara varios registros y sincroniza al terminar el grupo. Si una operación falla, false significa que no podemos afirmar que ese agregado quedó persistido; no demuestra que no se escribió ningún byte. Antes de reintentar hay que inspeccionar el archivo y usar la misma identidad.
El chequeo largo % 24 detecta un final incompleto, pero no un CRC incorrecto en un registro completo. Por eso la recuperación al arrancar es un requisito previo, no algo reemplazado por ese chequeo. El siguiente ejemplo en Python muestra el recorrido de validación.
La sincronización es una solicitud a la pila de almacenamiento. Su garantía depende del controlador y del medio, y se verifica con pruebas de corte reales. El código evita tratar una escritura parcial o un error de sincronización como un éxito silencioso.
09 · Más capacidad
Cuándo conviene una tarjeta SD
Una SD es útil cuando necesitás guardar semanas de lecturas, archivos de diagnóstico grandes o datos que se van a retirar físicamente. En ESP32 puede conectarse por SPI o por una interfaz SDMMC compatible con la placa. Hay que elegir pines y modo según el hardware; un adaptador no convierte automáticamente todas las señales en compatibles.
Con Arduino-ESP32, las bibliotecas habituales son SD.h para SPI y SD_MMC.h para SDMMC. El sistema FAT permite intercambiar archivos con una PC. La documentación de FAT y tarjetas SD de ESP-IDF detalla el montaje, la sincronización y sus costos.
Lo que sigue igual
Identidad, formato, CRC, límites de espacio, política de descarte y confirmaciones. Una tarjeta más grande no reemplaza el diseño de la cola.
Lo que cambia
Hay un medio extraíble, un controlador interno y latencias variables. Se agregan estados de tarjeta ausente, retirada o con error de escritura.
No retires una tarjeta mientras se escribe. El producto necesita un procedimiento para detener nuevas escrituras, sincronizar, cerrar y desmontar antes de indicar que se puede extraer. Si se retira sin ese procedimiento, el firmware debe detectar la falla y cambiar de estado sin anunciar que los datos se guardaron.
Probá el consumo durante escritura y la estabilidad de alimentación del conjunto. Reservá un buffer RAM para absorber pausas del medio, pero acotalo: si la tarjeta deja de responder, la RAM también se llena. Las pruebas deben incluir la tarjeta concreta y no solo un modelo simulado de velocidad constante.
Elegir una tarjeta por capacidad no garantiza resistencia a escrituras ni tolerancia a cortes. Esas propiedades se comprueban con las especificaciones del fabricante y pruebas del dispositivo. El historial puede ser valioso incluso después de una falla: no formatees una tarjeta automáticamente para conseguir que el montaje vuelva a funcionar.
10 · Validar bytes antes de usarlos
Recuperar un registro en Python
Este programa genera el mismo formato binario del escritor C++ y simula archivos incompletos o dañados. Trabaja con bytes en memoria para ejecutarse también en el navegador: no mide la durabilidad de una flash real. Devuelve los registros válidos y el offset donde termina el prefijo aceptado.
Python en el navegador. La primera ejecución descarga Pyodide. Ambos bloques son independientes: podés ejecutarlos o copiarlos a archivos Python 3 sin conectar una placa.
import struct
import zlib
CUERPO = struct.Struct("<2sBBIIIhH") # 20 bytes; sin relleno implícito
CRC = struct.Struct("<I")
TAM = CUERPO.size + CRC.size # 24 bytes
def codificar(arranque, seq, ms, decimas, tipo=1):
if tipo not in (1, 2) or not -400 <= decimas <= 1250:
raise ValueError("Tipo o temperatura inválidos")
cuerpo = CUERPO.pack(b"I3", 1, tipo, arranque, seq, ms, decimas, 1)
return cuerpo + CRC.pack(zlib.crc32(cuerpo))
def recuperar(datos):
validos = []
offset = 0
while offset + TAM <= len(datos):
bloque = datos[offset:offset + TAM]
cuerpo, crc = bloque[:20], CRC.unpack(bloque[20:])[0]
if zlib.crc32(cuerpo) != crc:
break
marca, version, tipo, boot, seq, ms, c, flags = CUERPO.unpack(cuerpo)
if (marca != b"I3" or version != 1 or tipo not in (1, 2)
or flags != 1 or not -400 <= c <= 1250):
break
validos.append((boot, seq, ms, c, tipo))
offset += TAM
return validos, offset
registros = [codificar(42, i, i * 10000, 41 + i) for i in range(1, 5)]
archivo = b"".join(registros)
recuperados, fin = recuperar(archivo)
assert TAM == 24 and len(recuperados) == 4 and fin == 96
# Corte durante el cuarto registro: conservar tres completos.
cortado = archivo[:3 * TAM + 7]
recuperados, fin = recuperar(cortado)
assert len(recuperados) == 3 and fin == 72
print(f"Corte: {len(cortado)} bytes; {len(recuperados)} registros; prefijo {fin} bytes")
# Daño en el segundo registro: detenerse en el primero.
danado = bytearray(archivo)
danado[TAM + 16] ^= 1
assert recuperar(danado)[1] == 24
# Una temperatura negativa debe conservar su signo.
assert recuperar(codificar(42, 9, 90000, -35))[0][0][3] == -35
# CRC correcto con versión desconocida: también se rechaza.
desconocido = bytearray(registros[0])
desconocido[2] = 2
desconocido[20:] = CRC.pack(zlib.crc32(desconocido[:20]))
assert recuperar(desconocido) == ([], 0)
# Cada posible corte recupera solo el prefijo completo.
for corte in range(len(archivo) + 1):
filas, fin = recuperar(archivo[:corte])
assert len(filas) == corte // TAM and fin == (corte // TAM) * TAM
print("6 comprobaciones superadas (incluyen los 97 puntos de corte).")
print("No anexar detrás de una cola incompleta: reparar o abrir otro segmento.")
Resultado inicial: el archivo de 79 bytes conserva tres registros y un prefijo de 72 bytes. Un daño en el segundo registro detiene la recuperación a los 24 bytes. Las seis comprobaciones terminan sin errores, incluyendo todos los puntos de corte del archivo de 96 bytes.
La versión v1 de este ejemplo admite registros con calidad válida. Para almacenar una adquisición fallida se necesita definir la representación y ampliar el validador, en lugar de escribir una temperatura inventada. Lo mismo ocurre con nuevos tipos de evento: agregar campos requiere una nueva especificación de formato.
Un recuperador real recorre el archivo por bloques acotados, sin cargar todo el historial en RAM. Después verifica también secuencias, segmentos y cursor. El CRC valida bytes de un registro; no detecta por sí solo que desapareció un segmento completo ni que un archivo fue copiado desde otro dispositivo.
11 · Un modelo de la cola
Simular una cola en Python
La siguiente simulación reproduce el laboratorio. Hay veinte pasos y se genera un registro por paso del 1 al 12. La identidad es el número del registro de la traza; en el firmware correspondería a dispositivo, arranque y secuencia. El servidor conserva identidades y reconoce repeticiones.
En cada paso, el orden es: reiniciar si corresponde → intentar generar → persistir lote si corresponde → enviar si hay red. Se intentan hasta dos envíos por paso. Si se pierde un ACK, el registro queda en la cola y se espera al paso siguiente para reintentar.
def simular(medio="flash", lote=4, capacidad=8, politica="rechazar",
sin_red=8, reinicio=6, perder_ack=True):
guardados, ram, servidor = [], [], set()
perdidos = descartados = duplicados = lotes = 0
ack_pendiente = perder_ack
filas = []
for t in range(1, 21):
if t == reinicio:
afectados = guardados + ram if medio == "ram" else ram
perdidos += sum(i not in servidor for i in afectados)
if medio == "ram":
guardados = []
ram = []
if t <= 12:
aceptar = True
if len(guardados) + len(ram) >= capacidad:
if politica == "rechazar":
aceptar = False
descartados += 1
else:
viejo = guardados.pop(0) if guardados else ram.pop(0)
descartados += int(viejo not in servidor)
if aceptar:
ram.append(t)
online = t > sin_red
if medio == "flash" and ram and (len(ram) >= lote or online):
guardados.extend(ram)
ram = []
lotes += 1
if online:
cola = guardados if medio == "flash" else ram
for _ in range(2):
if not cola:
break
identidad = cola[0]
duplicados += int(identidad in servidor)
servidor.add(identidad)
if ack_pendiente:
ack_pendiente = False
break
cola.pop(0)
filas.append(dict(t=t, guardados=guardados.copy(), ram=ram.copy(),
recibidos=sorted(servidor), perdidos=perdidos,
descartados=descartados, duplicados=duplicados,
lotes=lotes))
return filas
def resumen(filas):
f = filas[-1]
return (len(f["recibidos"]), f["perdidos"], f["descartados"],
f["duplicados"], f["lotes"])
base = simular()
print(" t persistidos RAM únicos perdidos descartados")
for f in base:
print(f'{f["t"]:2} {str(f["guardados"]):20} {str(f["ram"]):14}'
f' {len(f["recibidos"]):6} {f["perdidos"]:9} {f["descartados"]:12}')
# Tupla: únicos, perdidos en reinicio, no entregados por capacidad,
# entregas duplicadas, lotes persistidos.
assert resumen(base) == (10, 1, 1, 1, 4)
assert resumen(simular(medio="ram")) == (7, 5, 0, 1, 0)
assert resumen(simular(lote=1)) == (10, 0, 2, 1, 10)
assert resumen(simular(lote=1, capacidad=16)) == (12, 0, 0, 1, 12)
assert resumen(simular(capacidad=16)) == (11, 1, 0, 1, 5)
assert resumen(simular(perder_ack=False)) == (11, 1, 0, 0, 5)
# Sin reinicio ni pérdida de ACK y con capacidad suficiente llega todo.
assert resumen(simular(capacidad=16, reinicio=0, perder_ack=False)) == (12, 0, 0, 0, 6)
print("\n7 escenarios comprobados.")
print("Base: 10 únicos; 1 perdido al reiniciar; 1 por capacidad; 1 duplicado.")
La configuración inicial usa flash, lotes de cuatro, capacidad de ocho, red desde el paso 9, reinicio al comenzar el paso 6 y pérdida del primer ACK. El registro 5 se pierde al reiniciar; el 10 se rechaza por capacidad. El servidor termina con diez registros únicos y reconoce una entrega repetida del registro 1.
El contador de lotes mide grupos de registros persistidos, no escrituras físicas ni ciclos de borrado. Además hay metadatos, avances de cursor y liberación de espacio que el modelo no cuenta. La versión simulada sincroniza cualquier remanente cuando hay red; un dispositivo real también necesita un límite de antigüedad para los pendientes cuando no hay conexión.
12 · Exploración interactiva
Desconexión, reinicio y confirmación perdida
Elegí las condiciones y presioná «Aplicar y preparar». Podés avanzar de a un paso o completar los veinte. El laboratorio no usa el almacenamiento del navegador: representa la RAM y el medio persistente con dos colas para poder ver exactamente qué se pierde.
Una cola de doce registros
La configuración aplicada aparecerá aquí.
- Persistidos pendientesVacía
- Pendientes en RAMVacía
- Identidades recibidas por el servidorVacía
Preparando recorrido.
| Paso | Red | Operaciones | Persistidos | RAM |
|---|
Recorrido guiado
- Ejecutá con los parámetros iniciales. Llegan 10 de 12 registros. Se pierde el 5, que estaba en RAM al reiniciar, y se rechaza el 10 porque la cola está llena. Hay una entrega repetida y cuatro lotes persistidos.
- Reiniciá el recorrido y avanzá hasta el paso 4: los registros 1 a 4 quedan persistidos. En el paso 5 hay un pendiente en RAM; al comenzar el 6, ese registro desaparece.
- En el paso 9 el servidor recibe el 1 pero se pierde el ACK. El 1 todavía aparece en la cola. En el paso 10 se vuelve a enviar con la misma identidad: el servidor lo reconoce y devuelve ACK.
- Cambiá a Solo RAM y aplicá. El reinicio pierde los registros 1 a 5. Al final llegan siete registros, no hay pérdida por capacidad y no se persistió ningún lote.
- Volvé a flash y elegí un registro por lote. No se pierde ninguno por el reinicio, pero se rechazan dos por capacidad: siguen llegando diez. Mejorar durabilidad no aumenta espacio.
- Con flash, lote de uno y capacidad de 16, llegan los doce. Hay una entrega repetida y doce lotes persistidos. Con lotes de cuatro y la misma capacidad llegan once: el 5 seguía expuesto al reinicio.
- Volvé a la configuración inicial y desmarcá Perder el primer ACK. Llegan once: desaparece la repetición y se libera espacio a tiempo para aceptar el registro 10.
- Probá Descartar el más antiguo con capacidad de cuatro y doce pasos sin red. Mirá cuáles identidades sobreviven. El total recibido no cuenta toda la historia: también importa si conservaste los datos más viejos o los más recientes.
Los contadores «no entregados» solo cuentan registros que el servidor no había recibido. Si una política retira por capacidad un registro cuyo ACK se perdió, no se lo cuenta como dato perdido: ya existe en el servidor. La cola local puede contener una identidad que también aparece en el servidor mientras su confirmación está pendiente.
Desafío: ¿por qué el ACK perdido puede causar además un descarte?
Porque el registro sigue ocupando lugar hasta confirmar el reintento. Con la configuración inicial, en el paso 9 queda una cola de ocho registros. Al comenzar el 10 se genera antes de enviar y no hay espacio, así que se rechaza el nuevo. El mismo problema puede ocurrir por respuestas lentas o por reintentos repetidos: dimensioná con margen y con la tasa efectiva de confirmaciones.
13 · Diseñar y comprobar
Método y errores frecuentes
- Separá los usos: configuración, historia y cola de entrega tienen retenciones diferentes.
- Definí qué pérdida aceptás: cantidad y antigüedad máximas de los datos todavía volátiles.
- Calculá espacio y drenaje: tamaño real de registros, tiempo sin red, crecimiento durante la recuperación y margen de operación.
- Especificá el formato: versión, identidad, unidades, orden de bytes, límites y comprobación de integridad.
- Definí el ACK: quién lo emite, qué garantiza y qué identidad o rango confirma.
- Acotá todas las colas: RAM, archivos y pendientes de red. Elegí qué hacer cuando se llenan.
- Implementá recuperación antes de anexar: no supongas que el último cierre fue limpio.
- Medí en hardware: latencias, consumo, ocupación real, errores y cortes durante escritura, sincronización y avance de cursor.
Qué diagnósticos conviene reportar
Además de temperatura y alarma, reportá cantidad de pendientes, antigüedad del más viejo, espacio disponible, registros descartados, causa del último error de escritura y cantidad de reenvíos. «Conectado» no significa «al día»: un dispositivo puede tener Wi-Fi y seguir acumulando deuda porque el servidor confirma demasiado lento.
«Si está en una cola RAM, ya no lo pierdo»
La RAM puede cubrir una desconexión, pero no un corte de energía o un reinicio normal. El lote se considera expuesto hasta completar el punto de sincronización definido y comprobar su resultado.
«LittleFS hace que cada línea de mi programa sea atómica»
Las garantías del sistema de archivos no agrupan automáticamente «escribir registro, avanzar cursor y borrar segmento». La aplicación define el orden y debe poder recuperarse de un corte entre operaciones.
«Agrupar cuatro registros divide el desgaste exactamente por cuatro»
Reduce sincronizaciones en ese patrón, pero el desgaste también depende de datos, metadatos, borrados y recolección del sistema de archivos. El contador de lotes del simulador no es un contador de ciclos físicos.
«Si publish() devuelve éxito, puedo borrar el archivo»
Depende de qué significa éxito en esa biblioteca. Puede indicar que el mensaje se aceptó para enviar. Retirá el registro solo al alcanzar el nivel de confirmación que definiste, y conservá su identidad para reintentos.
«Un CRC correcto demuestra que no falta nada»
Demuestra una comprobación del contenido de ese registro. Para detectar omisiones necesitás revisar secuencias, segmentos y metadatos. Tampoco demuestra autenticidad ni procedencia del dispositivo.
«Más capacidad resuelve cualquier desconexión»
Solo amplía la cobertura. Una cola finita termina llenándose y un enlace que confirma menos de lo que se genera nunca elimina la deuda. Capacidad, tasa de producción y tasa de entrega se diseñan juntas.
«Si no monta, formateo y sigo»
El formateo elimina los datos que intentabas preservar y oculta la causa original. En un equipo desplegado se informa la falla y se aplica un procedimiento de recuperación. Preparar un medio nuevo es una operación distinta.
14 · Práctica guiada
Predecí antes de comprobar
1. Tiempo de desconexión
Tenés 96 KiB de presupuesto lógico, registros de 24 bytes y una muestra cada 15 s. ¿Cuántos registros caben y cuánto tiempo cubren, antes de considerar la asignación real del sistema de archivos?
Ver solución
96 × 1024 / 24 = 4096 registros. Cubren 4096 × 15 = 61.440 s: 17 h 4 min. Hace falta validar el espacio físico y dejar margen adicional para operar.
2. Un día completo
Con el formato de 24 bytes y período de 10 s, ¿alcanza el presupuesto lógico de 192 KiB para un día sin red?
Ver solución
No. Se requieren 8640 registros y 207.360 bytes (202,5 KiB). Caben 8192: faltan 448 registros, equivalentes a 1 h 14 min 40 s. Podés aumentar espacio, resumir o cambiar el período si el requisito lo permite.
3. Última escritura incompleta
Un segmento mide 127 bytes después de un corte. Los primeros cinco registros tienen formato y CRC válidos. ¿Cuánto prefijo se puede aceptar? ¿Se puede anexar directamente?
Ver solución
Cinco registros ocupan 120 bytes; sobran siete de una escritura interrumpida. No se anexa detrás de esos siete: se aísla o repara el final y se continúa de manera controlada, por ejemplo en un segmento nuevo.
4. Lotes y pérdida
Se persiste cada cuatro registros. Después de un lote confirmado se generan tres más y se corta la energía antes de comenzar otra escritura. ¿Cuántos pueden perderse?
Ver solución
Los tres pendientes en RAM. El lote previo ya estaba confirmado según la garantía del medio. Si el corte ocurriera durante la sincronización siguiente, habría que recuperar y comprobar qué parte del nuevo trabajo quedó disponible.
5. ACK perdido
El servidor guardó el registro 42/17 y se perdió la respuesta. ¿Se crea una nueva secuencia para reenviar? ¿Qué hace el servidor si ya lo tiene?
Ver solución
Se conserva la identidad original, junto con el dispositivo al que pertenece. El servidor reconoce el mismo registro, evita repetir el efecto y puede confirmar otra vez. Una identidad nueva convertiría el reintento en otro evento lógico.
6. Vaciar el atraso
Hay 600 registros pendientes, entran 6 por minuto y se confirman 18 por minuto. ¿Cuánto tarda en quedar al día? ¿Qué pasa si solo se confirman 5?
Ver solución
El drenaje neto es 18 − 6 = 12 por minuto; tarda 50 minutos. Si solo se confirman 5, el atraso crece un registro por minuto y eventualmente se agota la capacidad.
7. Actualizar un cursor
Se recibió el ACK de un segmento. ¿Conviene borrarlo y después persistir el cursor, o persistir primero el cursor que demuestra su confirmación?
Ver solución
Persistir primero el cursor validado y luego liberar el segmento. Si el corte ocurre antes de guardar el cursor, todavía se puede reintentar con deduplicación. El arranque debe validar la relación entre cursor y segmentos presentes.
8. Qué guardar dónde
Un equipo tiene tres parámetros de calibración, una cola moderada de alarmas y un mes de muestras de alta frecuencia que se retiran físicamente. ¿Qué opciones considerarías?
Ver solución
NVS/Preferences para la calibración, LittleFS para la cola acotada y SD para el historial voluminoso y extraíble. La elección final depende de volumen medido, alimentación y requisitos de pérdida; cada caso mantiene su política de validación y recuperación.
15 · Síntesis
Ideas para recordar
- RAM, medio físico y sistema de archivos cumplen funciones distintas. Preferences sirve para valores pequeños; LittleFS y SD permiten organizar historiales y colas en archivos.
- La capacidad se expresa también en tiempo sin red. Con 192 KiB lógicos y 24 bytes por muestra caben 8192 registros: 22 h 45 min 20 s si se genera uno cada 10 s.
- La cola conserva identidad, versión, unidades y comprobaciones de integridad. Un formato explícito evita depender del relleno de una estructura C++.
- Escribir, sincronizar y confirmar entrega son pasos diferentes. Un reintento conserva identidad y el receptor deduplica.
- Después de un corte se valida antes de anexar. Un final incompleto no se trata como un registro ni se oculta con un formateo automático.
- Los lotes reducen sincronizaciones y exponen datos en RAM; se acotan por cantidad y antigüedad. Los ciclos físicos se comprueban con el medio real.
- La cola llena necesita una política explícita; la conexión debe confirmar más rápido que la generación para poder ponerse al día.
- En el laboratorio inicial llegan diez de doce registros: uno se pierde al reiniciar y otro por capacidad. La entrega repetida se reconoce y no crea otro registro lógico.
¿Listo para el tema 34?
Antes de avanzar, calculá la cobertura de tu dispositivo, definí el formato de un registro y escribí qué sucede si falla cada paso: generar, persistir, enviar, confirmar y liberar. Probá una cola llena y un corte en medio de una escritura antes de confiarle datos importantes.
En el próximo tema, Almacenamiento de datos en la nube, pasamos del dispositivo al servidor: bases de series temporales, retención y consultas para guardar y aprovechar lo que la cola entrega.