====== 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