====== Referencia de APIs Principales ======
===== StateManager =====
**Ubicación**: ''MachineDomain/src/.../state/StateManager.kt''
==== Métodos públicos ====
class StateManager(
private val repository: Repository,
private val hardware: Hardware
) {
/**
* Inicializa la máquina de estados.
* @param context contexto Android
* @return true si inicialización exitosa
*/
fun init(context: Context): Boolean
/**
* Ejecuta un ciclo de la máquina de estados.
* @return resultado del estado actual
*/
suspend fun cycle(): StateResult
/**
* Obtiene nombre del estado actual.
* @return nombre string del estado (ej: "CaptureFace")
*/
fun getCurrentState(): String
/**
* Retrocede a estado anterior.
* @param reason razón del retroceso (mostrado en logs)
*/
fun goBack(reason: String): Boolean
/**
* Para la máquina de estados.
*/
fun stop()
// State data access
/**
* Acceso a datos del estado válidos solo durante ejecución.
*/
val stateData: StateData
}
data class StateResult(
val success: Boolean,
val message: String,
val nextState: String?
)
data class StateData(
var weight: Float = 0f,
var beneficiaryPhoto: Bitmap? = null,
var faceEmbedding: FloatArray = FloatArray(512),
var similarity: Float = 0f
)
==== Uso típico ====
// En Machine app
val stateManager = StateManager(repository, hardware)
// Iniciar flujo
viewModelScope.launch {
if (stateManager.init(context)) {
while (stateManager.getCurrentState() != "Done") {
stateManager.cycle()
StateNameEmitter.emit(stateManager.getCurrentState())
}
}
}
----
===== P2PManager =====
==== RutaPAE ====
**Ubicación**: ''RutaPAEDomain/src/.../P2PManager.kt''
interface P2PManager {
/**
* Lista IDs de máquinas que se pueden descubrir.
* @return Set de string IDs de máquinas
*/
suspend fun discoverableMachineIds(): Set
/**
* Obtiene candidatos de máquinas descubiertas.
* @return Set de peers disponibles
*/
suspend fun discoveredMachineCandidates(): Set
/**
* Conecta a máquina por ID.
* @param machineId identificador de la máquina
* @return P2PGestor si conexión exitosa, null otherwise
*/
suspend fun connectMachine(machineId: String): P2PGestor?
/**
* Conecta a máquina vía hotspot.
* @param ssid nombre de la red hotspot
* @return P2PGestor si conexión exitosa, null otherwise
*/
suspend fun connectMachineHotspot(ssid: String): P2PGestor?
/**
* Sincroniza entregas de máquina remota.
* @param machineId ID de máquina
* @param machineDatabaseId ID en BD local
* @return resultado de sincronización
*/
suspend fun syncDeliveriesFromMachine(
machineId: String,
machineDatabaseId: Long
): SyncDeliveriesResult
}
data class P2PPeer(
val peerId: String,
val name: String,
val status: String // "Connected", "Available", "Unavailable"
)
data class SyncDeliveriesResult(
val success: Boolean,
val message: String,
val fetched: Int = 0,
val failed: Int = 0
)
==== Uso típico ====
// En RutaPAE
val p2pManager = DomainManager.p2pManager ?: return
// Descubrir máquinas
val machines = p2pManager.discoverableMachineIds()
// Conectar
val gestor = p2pManager.connectMachine(machineId) ?: return
// Sincronizar
val result = p2pManager.syncDeliveriesFromMachine(machineId, dbId)
if (result.success) {
DeliverySyncUiEmitter.update(result)
}
----
===== Repository =====
==== Interfaz común ====
interface Repository {
/**
* Acceso a tabla de entregas.
*/
val deliveries: Table
/**
* Acceso a tabla de beneficiarios.
*/
val beneficiaries: Table
/**
* Servicio CRUD para entregas.
*/
val deliveryService: DeliveryService
/**
* Servicio CRUD para beneficiarios.
*/
val beneficiaryService: BeneficiaryService
}
/**
* Interfaz genérica para tablas.
*/
interface Table {
/**
* Obtiene todos los registros.
*/
suspend fun getAll(): List
/**
* Obtiene por ID primario.
*/
suspend fun getById(id: Long): T?
/**
* Crea nuevo registro.
*/
suspend fun create(entity: T): T
/**
* Actualiza registro existente.
*/
suspend fun update(entity: T): Boolean
/**
* Elimina registro.
*/
suspend fun delete(id: Long): Boolean
/**
* Consulta personalizada.
*/
suspend fun where(predicate: (T) -> Boolean): List
}
==== Búsqueda vectorial (Beneficiarios) ====
/**
* Interfaz de búsqueda vectorial en VectorDB.
*/
interface VectorSearchable : Table {
/**
* Búsqueda por similitud de vectores.
* @param query vector de búsqueda (512 dims)
* @param k número de resultados
* @return lista ordenada por similitud descendente
*/
suspend fun nearestNeighbors(
query: FloatArray,
k: Int = 5
): List
/**
* Búsqueda por rango de distancia.
* @param query vector de búsqueda
* @param maxDistance distancia máxima permitida
* @return resultados dentro del rango
*/
suspend fun rangeSearch(
query: FloatArray,
maxDistance: Float
): List
}
----
===== Services (Data Layer) =====
==== BeneficiaryService ====
object BeneficiaryService {
/**
* Busca beneficiarios similares al embedding.
* @param embedding vector 512d
* @param similarity threshold mínimo (0-1)
* @param limit cantidad máxima de resultados
* @return lista ordenada por similitud
*/
suspend fun findSimilar(
embedding: FloatArray,
similarity: Float = 0.7f,
limit: Int = 10
): List
/**
* Crea nuevo beneficiario.
*/
suspend fun create(beneficiary: Beneficiary): Beneficiary
/**
* Actualiza beneficiario.
*/
suspend fun update(beneficiary: Beneficiary): Boolean
/**
* Obtiene by ID remoto (para matching).
*/
suspend fun getByRemoteId(remoteId: String): Beneficiary?
/**
* Obtiene todos no sincronizados.
*/
suspend fun getLocalOnly(): List
}
==== DeliveryService ====
object DeliveryService {
/**
* Obtiene todas las entregas.
*/
suspend fun getAll(): List
/**
* Obtiene entregas sin sincronizar.
*/
suspend fun getUnsynced(): List
/**
* Crea nueva entrega.
*/
suspend fun create(delivery: Delivery): Delivery
/**
* Actualiza entrega.
*/
suspend fun update(delivery: Delivery): Boolean
/**
* Marca as synced en servidor.
*/
suspend fun markSynced(deliveryId: Long, serverDeliveryId: String)
/**
* Obtiene por rango de fecha.
*/
suspend fun getByDateRange(
startMs: Long,
endMs: Long
): List
/**
* Obtiene entregas por máquina.
*/
suspend fun getByMachineId(machineId: Long): List
}
----
===== Emitters - Observable Streams =====
==== StateNameEmitter ====
/**
* Emite nombre del estado actual.
*/
object StateNameEmitter {
/**
* Flow observable.
*/
val flow: Flow
/**
* Emitir cambio de estado.
* @param stateName nombre del nuevo estado
*/
suspend fun emit(stateName: String)
/**
* Obtener estado actual.
*/
fun current(): String?
/**
* Escuchar cambios (helper).
*/
suspend inline fun collect(action: (String) -> Unit) {
flow.collect(action)
}
}
==== DeliverySyncUiEmitter ====
object DeliverySyncUiEmitter {
/**
* Flow de resultados de sincronización.
*/
val flow: Flow
/**
* Actualizar progreso de sincronización.
*/
suspend fun update(progress: SyncProgress)
}
data class SyncProgress(
val state: SyncState, // Syncing, Success, Failed
val current: Int, // entregas procesadas
val total: Int, // entregas totales
val message: String,
val startTimeMs: Long,
val estimatedRemaining: Long? = null
)
enum class SyncState { Syncing, Success, Failed, Cancelled }
----
===== Hardware Interfaces =====
==== Camera2Service ====
class Camera2Service(context: Context) {
/**
* Verifica si se requieren permisos.
*/
fun requiresCameraPermission(): Boolean
/**
* Solicita permiso de cámara.
*/
fun requestCameraPermission(): Boolean
/**
* Captura foto de rostro (selfie).
* @return Bitmap de rostro capturado
* @throws Exception si hay error de hardware
*/
suspend fun takeSelfie(): Bitmap?
/**
* Captura foto de documento/producto.
*/
suspend fun captureDocument(): Bitmap?
/**
* Cierra recurso de cámara.
*/
fun close()
}
==== Scale (Balanza) ====
interface Scale {
/**
* Obtiene peso actual.
* @return peso en kg, atau null si no disponible
*/
suspend fun getWeight(): Float?
/**
* Vuelve escala a cero (tare).
*/
suspend fun tare()
/**
* Agrega listener para cambios.
*/
fun addEventListener(listener: ScaleEventListener)
/**
* Cierra conexión.
*/
fun close()
}
interface ScaleEventListener {
/**
* Peso cambió.
*/
fun onWeightChanged(weight: Float)
/**
* Estado de conexión cambió.
*/
fun onConnectionStatusChanged(connected: Boolean)
/**
* Error en balanza.
*/
fun onError(error: String)
}
----
===== Models (Data Classes) =====
==== Delivery ====
data class Delivery(
@PrimaryKey
val id: Long = 0,
// Datos de captura
val weight: Float = 0f,
val beneficiaryPhotoPath: String? = null, // ruta local
val alimentPhotoPath: String? = null, // ruta local
val similarity: Float = 0f, // 0-1
val processTimeMs: Long = 0L,
// Sincronización
val serverDeliveryId: String? = null,
val synchronized: Boolean = false,
val syncAttempts: Int = 0,
val lastSyncAttemptMs: Long? = null,
// Metadata
val createdAt: Long = System.currentTimeMillis(),
val beneficiaryId: Long = 0,
val enrollmentShiftId: Long = 0
)
==== Beneficiary ====
data class Beneficiary(
@PrimaryKey
val id: Long = 0,
@VectorColumn(dimensions = 512, distanceMetric = DistanceMetric.COSINE)
val embedding: FloatArray, // 512 dimensiones
val name: String,
val remoteBeneficiaryId: String? = null,
val enrollmentId: String? = null,
val lastRecognitionAtMs: Long? = null,
val lastRecognitionSimilarity: Float? = null,
val createdAt: Long = System.currentTimeMillis()
)
==== P2PMachineState ====
data class P2PMachineState(
val id: String = "",
val name: String = "",
val status: String = "Inactive", // Active, Inactive, Error
val unsyncedDeliveries: Long = 0,
val serverSyncedDeliveries: Long = 0,
val configuration: P2PMachineConfiguration? = null,
val lastSync: Long? = null
)
data class P2PMachineConfiguration(
val campusId: String? = null,
val modalityId: String? = null,
val shiftId: String? = null,
val gradeId: String? = null,
val operatorName: String? = null
)
----
===== Enums =====
enum class P2PMachineStatus {
Active, // funcionando
Inactive, // sin usar
Error // fallo
}
enum class DeliveryStatus {
WaitingForWeight,
CaptureImages,
CaptureFace,
ComparingWeights,
GenerateEmbedding,
VerifyInDatabase,
SaveDelivery,
WaitForWeightRemoved
}
enum class SyncState {
Syncing, // en progreso
Success, // completado exitosamente
Failed, // error
Cancelled // cancelado por usuario
}
enum class DistanceMetric {
COSINE, // similitud coseno (recomendado)
EUCLIDEAN, // distancia euclidiana
MANHATTAN // distancia manhattan
}
----
===== Excepciones Personalizadas =====
/**
* Excepción de conectividad P2P.
*/
class DirectLinkException(message: String, cause: Throwable? = null)
: Exception(message, cause)
/**
* Excepción de operación de hardware.
*/
class HardwareException(message: String, cause: Throwable? = null)
: Exception(message, cause)
/**
* Excepción de validación de datos.
*/
class ValidationException(message: String, cause: Throwable? = null)
: Exception(message, cause)
/**
* Excepción de acceso a base de datos.
*/
class DatabaseException(message: String, cause: Throwable? = null)
: Exception(message, cause)
----
===== Extension Functions =====
// En Core module
/**
* Convertir Bitmap a ByteArray JPG.
*/
fun Bitmap.toJpgBytes(quality: Int = 90): ByteArray {
val stream = ByteArrayOutputStream()
compress(Bitmap.CompressFormat.JPEG, quality, stream)
return stream.toByteArray()
}
/**
* Calcular similitud coseno entre embeddings.
*/
fun FloatArray.cosineSimilarity(other: FloatArray): Float {
require(size == 512 && other.size == 512)
// implementación
}
/**
* Validar peso válido.
*/
fun Float.isValidWeight(): Boolean = this > 0.5f && this < 100f
/**
* Formatear timestamp a string legible.
*/
fun Long.toReadableDate(): String {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.default)
return sdf.format(Date(this))
}
----
===== Constants =====
// MachineDomain
const val MIN_VALID_WEIGHT = 0.5f
const val MAX_VALID_WEIGHT = 100f
const val SIMILARITY_THRESHOLD = 0.7f
// Timeouts
const val API_TIMEOUT_MS = 30000L
const val P2P_TIMEOUT_MS = 20000L
const val CAMERA_TIMEOUT_MS = 10000L
// Configuración
const val EMBEDDING_DIMENSIONS = 512
const val MAX_PHOTO_QUALITY = 95
const val PHOTO_COMPRESSION_QUALITY = 80
// Paths
const val PHOTOS_DIR = "photos"
const val DB_NAME = "pae_database.db"
const val VECTOR_INDEX_NAME = "beneficiary_embedding_index"
----
===== Kotlin DSL Help =====
==== Configurar módulo en settings.gradle.kts ====
include(":NombreModulo")
project(":NombreModulo").projectDir = file("NombreModulo")
==== Dependencias comunes en build.gradle.kts ====
dependencies {
// Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
// Compose
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
// Coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
// Inyección
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
// Testing
testImplementation(libs.junit)
testImplementation(libs.mockito.kotlin)
}