====== Integración de Hardware en PAE ======
===== Visión general =====
Machine integra varios componentes de hardware para la captura y procesamiento de entregas.
┌─────────────────────────────┐
│ Machine App (UI) │
└────────────┬────────────────┘
│
┌─────▼─────┐
│ Hardware │
│ Interfaces│
└────┬─┬─┬──┘
│ │ │
┌───────┘ │ └───────┐
│ │ │
▼ ▼ ▼
Camera Scale Board LED
API (Bluetooth)
===== Camera2 API =====
==== Permisos ====
==== Captura de imagen ====
**Ubicación**: ''MachineDomain/src/.../hardware/Camera2Service.kt''
class Camera2Service(private val context: Context) {
private lateinit var cameraManager: CameraManager
private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
// Permisos en runtime
fun requestCameraPermission(): Boolean {
val permission = Manifest.permission.CAMERA
return if (ContextCompat.checkSelfPermission(context, permission)
!= PackageManager.PERMISSION_GRANTED) {
// Solicitar permisos
false
} else {
true
}
}
suspend fun takeSelfie(): Bitmap? = suspendCancellableCoroutine { continuation ->
try {
cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
// Enumerar cámaras disponibles
val cameraIds = cameraManager.cameraIdList
val frontCameraId = cameraIds.firstOrNull { id ->
val characteristics = cameraManager.getCameraCharacteristics(id)
characteristics.get(CameraCharacteristics.LENS_FACING) ==
CameraCharacteristics.LENS_FACING_FRONT
}
if (frontCameraId != null) {
cameraManager.openCamera(frontCameraId, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
startCapture(continuation)
}
override fun onDisconnected(camera: CameraDevice) {
camera.close()
continuation.resume(null)
}
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
continuation.resumeWithException(
Exception("Camera error: $error")
)
}
}, Handler(Looper.getMainLooper()))
} else {
continuation.resumeWithException(
Exception("No front camera found")
)
}
} catch (e: SecurityException) {
continuation.resumeWithException(e)
}
}
private fun startCapture(continuation: Continuation) {
val imageCaptureTarget = object : ImageReader.OnImageAvailableListener {
override fun onImageAvailable(reader: ImageReader?) {
reader?.acquireNextImage()?.use { image ->
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
// Convertir a Bitmap
buffer.rewind()
val data = ByteArray(buffer.remaining())
buffer.get(data)
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
continuation.resume(bitmap)
}
}
}
val imageReader = ImageReader.newInstance(1280, 720, ImageFormat.JPEG, 1)
imageReader.setOnImageAvailableListener(imageCaptureTarget, Handler())
val surface = imageReader.surface
val requestBuilder = cameraDevice?.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
)
requestBuilder?.addTarget(surface)
cameraDevice?.createCaptureSession(
listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
captureSession = session
try {
session.capture(
requestBuilder?.build()!!,
null,
Handler()
)
} catch (e: CameraAccessException) {
continuation.resumeWithException(e)
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
continuation.resumeWithException(
Exception("Camera capture session failed")
)
}
},
Handler()
)
}
fun close() {
captureSession?.close()
cameraDevice?.close()
}
}
==== Uso en StateManager ====
// CaptureFaceState.kt
override suspend fun run(next: () -> Unit, retry: (String) -> Unit) {
try {
val bitmap = camera2Service.takeSelfie()
if (bitmap != null) {
// Procesar imagen
stateData.facePhoto = bitmap
next()
} else {
retry("No se pudo capturar foto")
}
} catch (e: Exception) {
retry("Error capturando foto: ${e.message}")
}
}
===== Balanza (Scale) - Bluetooth =====
==== Permisos ====
==== Interfaz de balanza ====
**Ubicación**: ''MachineDomain/src/.../hardware/ScaleManager.kt''
interface Scale {
suspend fun getWeight(): Float?
suspend fun tare()
fun addEventListener(listener: ScaleEventListener)
fun close()
}
interface ScaleEventListener {
fun onWeightChanged(weight: Float)
fun onConnectionStatusChanged(connected: Boolean)
fun onError(error: String)
}
==== Implementación Bluetooth ====
class BluetoothScaleManager(
private val context: Context,
private val deviceMacAddress: String
) : Scale {
private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothSocket: BluetoothSocket? = null
private var listeners = mutableListOf()
suspend fun connect(): Boolean = suspendCancellableCoroutine { continuation ->
try {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
val device = bluetoothAdapter?.getRemoteDevice(deviceMacAddress)
?: return@suspendCancellableCoroutine continuation.resume(false)
bluetoothSocket = device.createRfcommSocketToServiceRecord(
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") // SPP UUID
)
bluetoothSocket?.connect()
// Iniciar lectura
launchWeightReader()
continuation.resume(true)
} catch (e: Exception) {
continuation.resume(false)
}
}
private fun launchWeightReader() {
val inputStream = bluetoothSocket?.inputStream ?: return
Thread {
val buffer = ByteArray(1024)
try {
while (true) {
val bytes = inputStream.read(buffer)
val data = String(buffer, 0, bytes)
// Parse: típicamente formato "12.5 kg\r\n"
val weight = parseWeight(data)
if (weight != null) {
listeners.forEach { it.onWeightChanged(weight) }
}
}
} catch (e: IOException) {
listeners.forEach { it.onError("Balanza desconectada") }
listeners.forEach { it.onConnectionStatusChanged(false) }
}
}.start()
}
private fun parseWeight(data: String): Float? {
return try {
// Ejemplo: "12.5 kg"
val parts = data.trim().split(" ")
parts[0].toFloat()
} catch (e: Exception) {
null
}
}
override suspend fun getWeight(): Float? = suspendCancellableCoroutine { continuation ->
// Retorna el peso actual
// (implementación depende del protocolo de la balanza)
}
override suspend fun tare() {
bluetoothSocket?.outputStream?.apply {
write("TARE\r\n".toByteArray())
flush()
}
}
override fun addEventListener(listener: ScaleEventListener) {
listeners.add(listener)
}
override fun close() {
bluetoothSocket?.close()
}
}
==== Uso en StateManager ====
// WaitingForWeightState.kt
override suspend fun run(next: () -> Unit, retry: (String) -> Unit) {
var weightReceived = false
scaleManager.addEventListener(object : ScaleEventListener {
override fun onWeightChanged(weight: Float) {
if (weight > 0.5f && !weightReceived) { // peso mínimo
weightReceived = true
stateData.weight = weight
next()
}
}
override fun onConnectionStatusChanged(connected: Boolean) {
if (!connected) {
retry("Balanza desconectada")
}
}
override fun onError(error: String) {
retry("Error balanza: $error")
}
})
}
===== Indicadores LED - GPIO =====
==== Interfaz ====
interface BoardLed {
fun turnOn(color: LedColor)
fun turnOff()
fun blink(color: LedColor, durationMs: Long)
fun stopBlinking()
}
enum class LedColor {
RED, GREEN, YELLOW
}
==== Implementación sencilla ====
class GpioLedManager : BoardLed {
// Típicamente se comunica vía USB o serie
override fun turnOn(color: LedColor) {
val command = when (color) {
LedColor.RED -> "LED_RED_ON"
LedColor.GREEN -> "LED_GREEN_ON"
LedColor.YELLOW -> "LED_YELLOW_ON"
}
sendCommand(command)
}
override fun turnOff() {
sendCommand("LED_OFF")
}
override fun blink(color: LedColor, durationMs: Long) {
val command = when (color) {
LedColor.RED -> "LED_RED_BLINK,$durationMs"
LedColor.GREEN -> "LED_GREEN_BLINK,$durationMs"
LedColor.YELLOW -> "LED_YELLOW_BLINK,$durationMs"
}
sendCommand(command)
}
private fun sendCommand(command: String) {
// Enviar vía puerto serie o USB
}
}
==== Uso en StateManager ====
// GenerateEmbeddingState.kt
override suspend fun run(next: () -> Unit, retry: (String) -> Unit) {
try {
boardLed.turnOn(LedColor.YELLOW) // procesando
val embedding = generateEmbedding(stateData.facePhoto)
stateData.embedding = embedding
boardLed.turnOn(LedColor.GREEN) // éxito
next()
} catch (e: Exception) {
boardLed.blink(LedColor.RED, 500)
retry("Error generando embedding: ${e.message}")
}
}
===== Almacenamiento de fotos =====
==== Ubicación segura ====
class PhotoStorage(private val context: Context) {
private val photosDir = File(context.filesDir, "photos")
init {
photosDir.mkdirs()
}
fun saveBeneficiaryPhoto(bitmap: Bitmap, beneficiaryId: Long): String {
val filename = "beneficiary_${beneficiaryId}_${System.currentTimeMillis()}.jpg"
val file = File(photosDir, filename)
FileOutputStream(file).use { fos ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, fos)
}
return file.absolutePath
}
fun saveDeliveryPhoto(bitmap: Bitmap, deliveryId: Long): String {
val filename = "delivery_${deliveryId}_${System.currentTimeMillis()}.jpg"
val file = File(photosDir, filename)
FileOutputStream(file).use { fos ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos)
}
return file.absolutePath
}
fun deletePhoto(path: String): Boolean {
return File(path).delete()
}
fun getPhotoSize(path: String): Long {
return File(path).length()
}
}
===== Inicialización de hardware =====
// Machine app initialization
class HardwareInitializer(private val context: Context) {
suspend fun initializeAll(): HardwareServices {
return try {
val camera2Service = Camera2Service(context)
val scaleManager = BluetoothScaleManager(
context,
MAC_ADDRESS_SCALE
).apply {
connect()
}
val boardLed = GpioLedManager()
val photoStorage = PhotoStorage(context)
HardwareServices(
camera = camera2Service,
scale = scaleManager,
led = boardLed,
photoStorage = photoStorage
)
} catch (e: Exception) {
Logger.e("HW_INIT", "Error inicializando hardware: ${e.message}")
throw e
}
}
}
data class HardwareServices(
val camera: Camera2Service,
val scale: Scale,
val led: BoardLed,
val photoStorage: PhotoStorage
)
===== Simulación para desarrollo =====
// Para testing sin hardware físico
class MockScale : Scale {
override suspend fun getWeight(): Float? = Random.nextFloat() * 50
override suspend fun tare() { /* no-op */ }
override fun addEventListener(listener: ScaleEventListener) {
// Simular cambio de peso
Thread {
Thread.sleep(2000)
listener.onWeightChanged(12.5f)
}.start()
}
override fun close() { /* no-op */ }
}
class MockCamera2Service : Camera2Service(context) {
override suspend fun takeSelfie(): Bitmap? {
// Retornar imagen de prueba
return BitmapFactory.decodeResource(
context.resources,
R.drawable.test_face
)
}
}
class MockBoardLed : BoardLed {
override fun turnOn(color: LedColor) {
println("LED: ${color.name} ON")
}
override fun turnOff() {
println("LED: OFF")
}
override fun blink(color: LedColor, durationMs: Long) {
println("LED: ${color.name} BLINK $durationMs ms")
}
override fun stopBlinking() {
println("LED: STOP BLINK")
}
}
===== Manejo de desconexiones =====
class HardwareMonitor(private val hardware: HardwareServices) {
fun monitorAll() {
// Monitorear balanza
hardware.scale.addEventListener(object : ScaleEventListener {
override fun onConnectionStatusChanged(connected: Boolean) {
if (!connected) {
handleScaleDisconnection()
}
}
override fun onWeightChanged(weight: Float) { }
override fun onError(error: String) {
handleScaleError(error)
}
})
}
private fun handleScaleDisconnection() {
Logger.w("MONITOR", "Balanza desconectada")
// Notificar a UI
// Retry conexión
}
private fun handleScaleError(error: String) {
Logger.e("MONITOR", "Error en balanza: $error")
}
}
===== Calibración de hardware =====
class HardwareCalibration(private val context: Context) {
suspend fun calibrateScale(scale: Scale) {
// 1. Tare (poner a cero)
scale.tare()
delay(1000)
// 2. Colocar peso conocido (ej: 1 kg)
// 3. Leer peso
val readWeight = scale.getWeight()
// 4. Calcular factor de calibración
val calibrationFactor = 1000.0f / (readWeight ?: 1f)
// 5. Guardar
saveSetting("scale_calibration_factor", calibrationFactor)
}
private fun saveSetting(key: String, value: Float) {
val prefs = context.getSharedPreferences("hardware", Context.MODE_PRIVATE)
prefs.edit().putFloat(key, value).apply()
}
}
===== Energía y battery saver =====
// Optimización para máquinas con batería limitada
class PowerManager(private val context: Context) {
fun enableBatterySaverMode() {
// Reducir frecuencia de polling
// Apagar componentes no usados
// Reducir resolución de cámara
// Aumentar timeout de pantalla
}
fun disableBatterySaverMode() {
// Restaurar configuración normal
}
fun getDeviceBatteryPercentage(): Int {
val batteryManager = context.getSystemService(BatteryManager::class.java)
return batteryManager?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER) ?: 0
}
}