13 KiB
Packet - Especificación Técnica Completa
1. Visión General
Packet permite:
- Capturar contenido multimedia (fotos, audio, video, archivos)
- Etiquetarlo con referencias de bibliotecas externas
- Empaquetarlo en contenedores con hash único
- Enviarlo a backends configurables
Principios
| Principio | Descripción |
|---|---|
| Zero-retention | Los archivos nunca se almacenan en disco, solo en RAM hasta envío |
| Hash-first | Todo contenedor tiene hash único generado localmente |
| Contenedor cerrado | Una vez montado, no se inspecciona. Solo enviar o desmontar |
| App tonta | No valida coherencia de etiquetas, solo formato. El backend decide |
| Offline-capable | Captura sin conexión, cola de reintentos cuando hay red |
2. Stack Tecnológico
| Componente | Tecnología |
|---|---|
| Framework | Flutter 3.x |
| Lenguaje | Dart |
| Estado | flutter_bloc |
| DB Local | sqflite |
| HTTP | dio |
| Crypto | crypto (SHA-256) |
| Media | image_picker, record |
| Location | geolocator |
| Permisos | permission_handler |
3. Estructura de Carpetas
packet/
├── android/
├── ios/
├── lib/
│ ├── core/
│ │ ├── constants/
│ │ │ └── app_constants.dart
│ │ ├── errors/
│ │ │ └── exceptions.dart
│ │ ├── utils/
│ │ │ ├── hash_utils.dart
│ │ │ └── retry_utils.dart
│ │ └── theme/
│ │ └── app_theme.dart
│ │
│ ├── data/
│ │ ├── datasources/
│ │ │ ├── local_database.dart
│ │ │ ├── backend_api.dart
│ │ │ └── biblioteca_api.dart
│ │ ├── models/
│ │ │ ├── destino_model.dart
│ │ │ ├── contenedor_model.dart
│ │ │ ├── etiqueta_model.dart
│ │ │ └── pack_model.dart
│ │ └── repositories/
│ │ ├── contenedor_repository.dart
│ │ ├── etiqueta_repository.dart
│ │ └── config_repository.dart
│ │
│ ├── domain/
│ │ └── entities/
│ │ ├── contenedor.dart
│ │ ├── etiqueta.dart
│ │ ├── pack.dart
│ │ ├── destino.dart
│ │ └── archivo_adjunto.dart
│ │
│ ├── presentation/
│ │ ├── bloc/
│ │ │ ├── captura/
│ │ │ ├── etiquetas/
│ │ │ ├── packs/
│ │ │ ├── pendientes/
│ │ │ └── config/
│ │ ├── pages/
│ │ │ ├── captura_page.dart
│ │ │ ├── etiquetas_page.dart
│ │ │ ├── packs_page.dart
│ │ │ ├── pendientes_page.dart
│ │ │ └── config_page.dart
│ │ ├── widgets/
│ │ │ ├── audio_recorder.dart
│ │ │ ├── destino_selector.dart
│ │ │ ├── etiqueta_grid.dart
│ │ │ └── contenedor_card.dart
│ │ └── app.dart
│ │
│ └── main.dart
│
├── test/
├── pubspec.yaml
└── README.md
4. Pantallas
Navegación
┌─────────────────────────────────────────┐
│ [Mi DECK ▼] [CORP] [+Ext] │ ← Destinos (superior)
├─────────────────────────────────────────┤
│ │
│ CONTENIDO PANTALLA │
│ │
├─────────────────────────────────────────┤
│ 📷 🏷️ 📦 ⏳ ⚙️ │ ← Navegación (inferior)
│ Captura Tags Packs Pend. Config │
└─────────────────────────────────────────┘
4.1 Captura
Montar contenedor con contenido multimedia.
Campos:
- Título (string, opcional)
- Descripción (string, opcional, se procesa en backend)
- Identificador (string 64 chars, auto-generado SHA-256)
- Archivos (array, opcional)
- GPS (lat/long, opcional)
- Etiquetas (array de hashes, seleccionadas en pestaña Etiquetas)
Botones de captura:
- 📷 Foto (cámara)
- 🎤 Audio (con contador de tiempo)
- 🎥 Video
- 📎 Archivo
- 📍 GPS
El hash se genera al abrir la app o tras cada envío. Se puede enviar contenedor vacío (solo hash). Botones de copiar y regenerar hash visibles.
4.2 Etiquetas
Seleccionar etiquetas de bibliotecas y añadir hashes externos.
Secciones:
- Grid HST - Iconos de biblioteca HST (se oscurecen al seleccionar)
- Grid DECK - Iconos de biblioteca DECK
- Campo hash - Pegar URL o hash externo
- Seleccionadas - Lista de etiquetas actuales con botón eliminar
Validación de hash externo:
- Usuario pega texto
- App busca patrón: [a-f0-9]{64}
- Por cada match → check verde inmediato
- Background: consulta /api/tags para resolver nombre
- Si encuentra → muestra nombre
- Si no → muestra hash truncado
4.3 Packs
Conjuntos predefinidos de etiquetas creados localmente.
- 1 tap → aplica todas las etiquetas del pack
- Creación: nombre + etiquetas actualmente seleccionadas
- Almacenados en SQLite local
4.4 Pendientes
Cola de contenedores fallidos. Límite: 20 contenedores máximo. App se bloquea si se llena.
Por cada contenedor muestra:
- Hash (truncado)
- Título (o "sin título")
- Número de archivos
- Intentos realizados (máx 20)
- Último intento (hora)
- Estado (error / reintentando)
Acciones:
- Reintentar → Intenta enviar de nuevo
- Recuperar → Extrae archivos al almacenamiento del dispositivo y elimina contenedor
4.5 Config
URLs, llaves y bibliotecas.
Por cada destino:
- Nombre (identificador visual)
- URL Backend (endpoint de envío)
- Llave (hash de 64 chars para autenticación)
Bibliotecas de iconos:
- URL (dominio)
- Endpoint (ruta del índice, ej: /api/tags)
- Nombre (identificador visual)
5. Modelos de Datos
Contenedor
class Contenedor {
String hash; // 64 chars, generado localmente
String? titulo;
String? descripcion;
List<ArchivoAdjunto> archivos;
GpsLocation? gps;
List<String> etiquetas; // Lista de hashes
DateTime createdAt;
}
Etiqueta
class Etiqueta {
String hMaestro; // Hash único (64 chars)
String hGlobal; // biblioteca + etiqueta (128 chars)
String? mrf; // Media reference para imagen
String? ref; // Código corto
String? nombreEs;
String? nombreEn;
String grupo;
bool activo;
}
Destino
class Destino {
int id;
String nombre;
String url;
String hash; // Llave de autenticación
bool activo;
}
Pack
class Pack {
int id;
String nombre;
String icono;
List<String> tags; // Lista de hashes
}
ArchivoAdjunto
class ArchivoAdjunto {
String nombre;
String mimeType;
Uint8List bytes; // En memoria, nunca en disco
FileType tipo; // image, audio, video, document
String? hash;
}
6. Base de Datos SQLite
CREATE TABLE registro (
hash TEXT PRIMARY KEY,
titulo TEXT,
hora_envio TEXT NOT NULL,
primera_conf TEXT,
ultima_conf TEXT,
destino_id INTEGER
);
CREATE TABLE destinos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
url TEXT NOT NULL,
hash TEXT NOT NULL,
activo INTEGER DEFAULT 1
);
CREATE TABLE bibliotecas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
url TEXT NOT NULL,
endpoint TEXT NOT NULL,
h_biblioteca TEXT
);
CREATE TABLE etiquetas_cache (
h_maestro TEXT PRIMARY KEY,
h_global TEXT,
mrf TEXT,
ref TEXT,
nombre_es TEXT,
nombre_en TEXT,
grupo TEXT,
biblioteca_id INTEGER
);
CREATE TABLE packs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
icono TEXT DEFAULT 📦,
tags TEXT NOT NULL
);
CREATE TABLE pendientes (
hash TEXT PRIMARY KEY,
titulo TEXT,
contenido BLOB NOT NULL,
intentos INTEGER DEFAULT 0,
ultimo_intento TEXT,
proximo_intento TEXT,
destino_id INTEGER
);
CREATE TABLE app_state (
key TEXT PRIMARY KEY,
value TEXT
);
7. APIs Externas
Backend (envío)
POST {destino.url}/ingest
X-Auth-Key: {destino.hash}
Content-Type: application/json
{
"hash": "fc2fae65158ca7c1880dd3ff9b8ed9081107b50f9cdce08df30234fe166777ce",
"titulo": "ferr-23",
"descripcion": "Compra en ferretería",
"etiquetas": ["a1b2c3d4...fc2fae65...", "a1b2c3d4...b2c3d4e5..."],
"gps": {"lat": 43.3623, "long": -8.4115},
"archivos": [{"nombre": "foto.jpg", "tipo": "image/jpeg", "contenido": "base64..."}]
}
Response 200: { "ok": true, "received_at": "2024-12-20T10:00:00Z" }
Response 409: { "error": "hash_exists" }
Envío por chunks (archivos grandes)
POST {destino.url}/upload/init
{ "hash": "...", "total_chunks": 50, "file_name": "video.mp4" }
→ { "upload_id": "..." }
POST {destino.url}/upload/chunk/{upload_id}/{chunk_number}
Body: bytes (512KB - 1MB por chunk)
→ { "ok": true }
POST {destino.url}/upload/complete/{upload_id}
→ { "ok": true, "file_hash": "..." }
Bibliotecas (índice de etiquetas)
GET {biblioteca.url}/api/tags
Response:
{
"count": 973,
"biblioteca": {
"h_biblioteca": "b7149f9e2106c566032aeb29a26e4c6cdd5f5c16b4421025c58166ee345740d1",
"nombre": "HST",
"publica": true
},
"results": [
{
"h_maestro": "fc2fae65...77ce",
"h_global": "b7149f9e...fc2fae65...77ce",
"mrf": "502dc114...",
"ref": "inv",
"nombre_es": "factura",
"nombre_en": "invoice",
"grupo": "hst",
"imagen_url": "https://tzrtech.org/502dc...png",
"activo": true
}
]
}
Imágenes: GET {biblioteca.url}/{mrf}.png
8. Formato de Referencias
Hash simple (etiqueta): 64 caracteres hex
fc2fae65158ca7c1880dd3ff9b8ed9081107b50f9cdce08df30234fe166777ce
Referencia global (biblioteca + etiqueta): 128 caracteres
[h_biblioteca 64 chars][h_etiqueta 64 chars]
Cadena jerárquica: múltiplo de 64, sin separador
[biblioteca][nivel1][nivel2][nivel3]...
Múltiples cadenas: separadas por \n
9. Lógica de Reintentos
20 intentos en 72 horas con backoff exponencial:
| Intento | Espera | Acumulado |
|---|---|---|
| 1 | 1 min | 0 |
| 2 | 2 min | 1 min |
| 3 | 5 min | 3 min |
| 4 | 10 min | 8 min |
| 5 | 20 min | 18 min |
| 6 | 30 min | 38 min |
| 7 | 1 h | 1 h |
| 8 | 2 h | 2 h |
| 9 | 3 h | 4 h |
| 10 | 4 h | 7 h |
| 11 | 5 h | 11 h |
| 12 | 6 h | 16 h |
| 13 | 6 h | 22 h |
| 14 | 6 h | 28 h |
| 15 | 8 h | 34 h |
| 16 | 8 h | 42 h |
| 17 | 8 h | 50 h |
| 18 | 8 h | 58 h |
| 19 | 6 h | 66 h |
| 20 | STOP | 72 h |
10. Persistencia de Estado
La app restaura al abrir exactamente como se cerró:
- Destino seleccionado
- Pantalla activa
- Contenedor en progreso
- Etiquetas seleccionadas
- Cola de pendientes
- Configuración completa
11. Permisos
Android:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
iOS:
<key>NSCameraUsageDescription</key>
<string>Para capturar fotos y videos</string>
<key>NSMicrophoneUsageDescription</key>
<string>Para grabar notas de voz</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Para añadir ubicación a los contenedores</string>
12. CI/CD
name: Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- run: flutter pub get
- run: flutter test
build-android:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- run: flutter build apk --release
build-ios:
needs: test
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- run: flutter build ios --release --no-codesign
13. Roadmap
v1.0 (MVP)
- 5 pantallas básicas
- Captura foto/audio/video
- Envío a backend único
- Cola de pendientes
- Packs locales
v1.1
- Múltiples destinos
- Envío por chunks
- Cache de bibliotecas
v1.2
- Notificaciones push
- Widgets de acceso rápido
v2.0
- Credenciales para bibliotecas privadas
- Cadenas jerárquicas visuales
- Sincronización entre dispositivos
14. Referencias
- Biblioteca HST: https://tzrtech.org
- API HST: https://tzrtech.org/api/tags
- h_biblioteca HST: b7149f9e2106c566032aeb29a26e4c6cdd5f5c16b4421025c58166ee345740d1
Versión: 1.0 Fecha: 2025-12-20