====== Interacción de módulos y flujos de integración ======
===== Grafo de dependencias =====
┌─────────────────────────────────────────────────────────────────┐
│ Capa de Presentación (UI) │
│ Machine (app) | RutaPAE (app) │
└────────────┬──────────────────────────┬──────────────────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────┐
│ MachineDomain (Logic) │ │ RutaPAEDomain (Logic) │
│ - StateManager │ │ - P2PManager │
│ - P2PManager │ │ - DomainManager │
│ - SyncServices │ │ - SyncServices │
└────────┬─────────────────┘ └────────┬────────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────┐
│ MachineData (DB) │ │ RutaPAEData (DB) │
│ - Repository │ │ - Repository │
│ - Services │ │ - Services │
└────────┬─────────────────┘ └────────┬────────────────┘
│ │
└──────────────┬──────────────┘
▼
┌────────────────────────────────────┐
│ VectorialDB (ORM) │
│ - Table │
│ - Índices vectoriales │
│ - SQLite backend │
└────────────────────────────────────┘
│
▼
┌────────────┐
│ DB.sqlite │
└────────────┘
Comunicación inter-app
┌──────────────────────────────────┐ ┌──────────────────────────┐
│ Machine (REST Server) │ │ RutaPAE (REST Client) │
│ directlink:8000/p2p/... │ │ Conecta a Machine P2P │
└────────────┬─────────────────────┘ └──────────┬───────────────┘
│ │
└────────────┬───────────────────────┘
▼
DirectLink (Wi-Fi Direct)
Hotspot (tethering)
┌──────────────────────────────────────────────────────────────┐
│ Servicios compartidos │
│ Contract - modelos P2P │
│ Core - logging e interfaces │
│ ComputerVision - modelos IA │
└──────────────────────────────────────────────────────────────┘
===== Ciclo de vida del estado de entrega =====
==== Máquina (Machine) ====
Usuario inicia StateManager crea Ejecuta transiciones
captura de entrega StateWorkflow
│ │ │
▼ ▼ ▼
LaboratoryScreen StateManager.init() WaitingForWeight
│ │ │
└──────────┬───────────────┘ ├─→ CaptureImages
UI espera ├─→ CaptureFace
cambios de estado ├─→ ComparingWeights
│ ├─→ GenerateEmbedding
▼ ├─→ VerifyInDatabase
StateNameEmitter ├─→ SaveDelivery
emite evento └─→ WaitForWeightRemoved
│
▼
Flow
│
▼
UI Composable
actualiza según nombre
===== Sincronización P2P: RutaPAE → Machine =====
==== Fase 1: Descubrimiento ====
RutaPAE DirectLink Machine
(cliente) (P2P) (servidor)
│ │ │
├─ discoverableMachineIds() ├─────────────────────────┤
│ │ broadcast peers │
│◄──────────────────────────┤◄────────────────────────┤
│ SET │
│
├─ discoveredMachineCandidates()
│ (devuelve peers activos)
│
==== Fase 2: Conexión ====
RutaPAE DirectLink Machine
(cliente) (P2P) (servidor)
│ │ │
├─ connectMachine(id) ├─────────────────────────┤
│ │ negotiate connection │
│ ├─────────────────────────►│
│ │ │
│ ◄─────────────────────────┤
│ │ accept / reject │
│◄───────────────────────────┤ │
│ connection ready │ │
│
==== Fase 3: Sincronización de entregas ====
RutaPAE DirectLink Machine
(cliente) (HTTP) (servidor)
│ │ │
├─ syncDeliveries ├─────────────────────┤
│ │ │
│ GET /p2p/ │ │
│ machine/ │ │
│ deliveries/1/1 ├──────────────────►│
│ │ │
│ │ query db.sqlite │
│ │ Delivery.all() │
│ │ │
│ ◄──────────────────┤
│ │ │
│ LIST[ │ P2PDeliveriesPageResponse
│ P2PDelivery │ - results: LIST[P2PDelivery]
│ ] │ - totalCount
│◄───────────────────┤ - pageIndex
│ │
├─ DeliveryService │
│ create(list) │
│ │
├─ persist in DB │
│ RutaPAEData │
│
└─ UI updates
===== Flujo completo de entrega: Machine =====
STEP 1: Presentación STEP 2: Dominio STEP 3: Datos
(Machine UI) (MachineDomain) (MachineData)
User clicks "Capture"
│
▼
LaboratoryScreenUI
├─ VM.startCapture()
│ │
│ ▼
│ StateManager.init()
│ │
│ ▼
│ Loop: for state in workflow
│ │
│ ├─→ WaitingForWeight.run()
│ │ │
│ │ ├─ await scale input
│ │ │ [Hardware interface]
│ │ │
│ │ ├─ next() ──────────┐
│ │ │ ▼
│ │ │ CaptureImages.run()
│ │ │ │
│ │ │ ├─ Camera2 capture
│ │ │ │ [Hardware interface]
│ │ │ │
│ │ │ ├─ next()
│ │ │ │ ▼
│ │ │ CaptureFace.run()
│ │ │ │
│ │ │ └─ next() ─────┐
│ │ │ ▼
│ │ │ GenerateEmbedding.run()
│ │ │ │
│ │ │ ├─ ComputerVision
│ │ │ │ .generateEmbedding()
│ │ │ │
│ │ │ ├─ next()
│ │ │ │ ▼
│ │ │ VerifyInDatabase.run()
│ │ │ │
│ │ │ ┌────────────────────┘
│ │ │ │
│ │ │ ▼ (acceso a DB)
│ │ │ BeneficiaryService.findSimilar()
│ │ │ │
│ │ │ ├─ VectorDB nearest neighbors
│ │ │ │ query: embedding, k=5
│ │ │ │
│ │ │ ▼
│ │ │ Beneficiary[]
│ │ │ │
│ │ │ ▼ (regresa a estado)
│ │ │ state.next() ────┐
│ │ │ ▼
│ │ │ SaveDelivery.run()
│ │ │ │
│ │ │ ├─ DeliveryService.save()
│ │ │ │ │
│ │ │ │ ├─ Repository.create()
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ DB INSERT
│ │ │ │ │
│ │ │ └──┬─┘
│ │ │
│ │ ├─ StateNameEmitter.emit() ────┐
│ │ │ │
│ ◄─────────┤ │
│ StateNameEmitter │
│ observable │
│ │ │
│ └──────────────────────────────────────┐│
│ ││
▼ ▼▼
LaunchedEffect in Composable
.collect { stateName: String ->
// updateUI(stateName)
}
===== Manejo de errores y reintentos =====
State.run(next, retry)
│
├─ try {
│ action()
│ next() ──────► continuar a siguiente estado
│ }
│
└─ catch (e) {
retry(reason) ──────► retroceder a estado anterior
}
Ejemplo: CaptureFace falla
│
▼
retry("Camera error")
│
▼
estado anterior reactiva
│
▼
usuario intenta nuevamente
===== Inyección de dependencias =====
==== Machine (Dagger/Hilt) ====
Activity
├─ ViewModelFactory (Hilt)
│ ├─ ViewModel
│ │ ├─ StateManager
│ │ ├─ Repository
│ │ └─ Hardware
│ │
│ ├─ StateManager
│ │ ├─ Repository
│ │ ├─ Camera2Service
│ │ ├─ ScaleManager
│ │ └─ ComputerVision
│ │
│ └─ Repository
│ ├─ DeliveryService
│ ├─ BeneficiaryService
│ └─ VectorDB
==== RutaPAE (inyección manual) ====
Worker (enqueue)
├─ DomainManager.init()
│ ├─ P2PManager
│ │ ├─ DirectLink
│ │ └─ Contract models
│ │
│ └─ Repository
│ ├─ DeliveryService
│ └─ MachineService
│
└─ DomainManager.p2pManager ────► syncDeliveriesFromMachine()
===== Comunicación de eventos =====
==== Machine: StateFlow ====
StateManager
│
├─ StateNameEmitter ──► Flow
│ │
│ └─ emit("CapturingFace")
│
├─ StateMessageEmitter ──► Flow
│ │
│ └─ emit("Por favor espere...")
│
└─ SyncRunningEmitter ──► Flow
│
└─ emit(true) cuando sincronización activa
Observadores:
│
├─ LaboratoryScreenUI.stateCollector()
├─ ProgressBarUI.messageCollector()
└─ StatusIndicatorUI.syncCollector()
==== RutaPAE: Emisores ====
DeliverySyncWorker
│
├─ DeliverySyncUiEmitter
│ │
│ └─ emit(SyncDeliveriesResult)
│
└─ SyncRunningEmitter
│
└─ emit(true) durante sync
Observadores:
│
├─ MachineListScreenUI
└─ SyncProgressIndicatorUI
===== Casos de uso críticos =====
==== Caso 1: Máquina recibe beneficiario nuevo (HTTP) ====
Backend notifica Machine descarga Machine guarda
nueva beneficiaria
│ │ │
▼ ▼ ▼
Backend MachineEndpoints.getBeneficiaries() BeneficiaryService.create()
│ │
├─ FuelHttpClient.get() ├─ Repository.create()
│ │
├─ parse JSON ├─ VectorDB add()
│ │
└─ MachineData.Repository └─ DB.sqlite INSERT
==== Caso 2: Máquina y RutaPAE se sincronización (P2P) ====
RutaPAE inicia Machine responde RutaPAE persiste
sincronización con entregas
│ │ │
▼ ▼ ▼
RutaPAEUI Machine P2P server RutaPAEData.Repository
├─ enqueue Worker │ │
│ ├─ query Delivery DB ├─ DeliveryService.create()
│ │ │
└──► Worker starts ├─ serialize └─ persist
│ │ │
├─ init Domain └─ HTTP response ▼
│ │ DB.sqlite
├─ P2PManager.connect() │
│ │
├─ P2PManager.sync()────────┤
│
└─ Worker.done()
==== Caso 3: Error durante captura (Machine) ====
CaptureFace.run()
│
├─ try {
│ Camera.takePhoto()
│ } ──── EXCEPCIÓN
│
└─ catch {
retry("Camera no disponible")
}
│
▼
retrocede a CaptureImages
│
▼
estado anterior = true
│
▼
UI muestra botón "Reintentar"
│
▼
usuario presiona "Reintentar"
│
▼
vuelve a intentar CaptureFace
===== Optimizaciones de concurrencia =====
==== Coroutines en Machine ====
StateManager.cycle()
│
└─ suspend fun (en LaunchedEffect)
│
├─ state.run() ──► suspender si no hay input
│
└─ observable emitters ──► no bloquea UI
==== Coroutines en RutaPAE ====
DeliverySyncWorker
│
└─ suspend fun syncDeliveriesFromMachine()
│
├─ delay espera entre reintentos
│
└─ parallel requests via coroutines
===== Versioning y compatibilidad =====
Machine v0.9.0 RutaPAE v0.4.0
│ │
├─ uses Contract v... ├─ uses Contract v...
│ │
├─ uses DirectLink v... ├─ uses DirectLink v...
│ │
└─ API mínima: SDK 27 └─ API mínima: SDK 27
API objetivo: SDK 36 API objetivo: SDK 36