---
title: Android
slug: cp-sdconn-android
docTags: 
createdAt: 2026-03-11T16:00:58.147Z
---

## Integración

### Desde archivo .aar

1. Generar el .aar ejecutando el script de build:

:::BlockQuote
cd sdconn-android
./build-aar.sh
:::

1. Copiar dist/sdconn-release.aar al directorio libs/ del módulo de la aplicación.
2. Agregar la dependencia en build.gradle.kts:

:::BlockQuote
dependencies \{
&#x20;   implementation(files("libs/sdconn-release.aar"))

&#x20;   *// Dependencias transitivas (requeridas)*
&#x20;   implementation("com.github.mik3y\:usb-serial-for-android:3.8.1")
&#x20;   implementation("com.squareup.okhttp3\:okhttp:4.12.0")
&#x20;   implementation("org.jetbrains.kotlinx\:kotlinx-serialization-json:1.7.3")
&#x20;   implementation("org.jetbrains.kotlinx\:kotlinx-coroutines-android:1.9.0")
&#x20;   implementation("androidx.security\:security-crypto:1.1.0-alpha06")
}
:::

1. Agregar el repositorio de JitPack en settings.gradle.kts (necesario para usb-serial-for-android):

:::BlockQuote
dependencyResolutionManagement \{
&#x20;   repositories \{
&#x20;       google()
&#x20;       mavenCentral()
&#x20;       maven \{ url = uri("https\://jitpack.io") }
&#x20;   }
}
:::

### Desde módulo local

Si se prefiere incluir el SDK como módulo del proyecto:

1. Copiar la carpeta sdconn/ al proyecto.
2. Agregar en settings.gradle.kts:

:::BlockQuote
include("\:sdconn")
:::

1. Agregar la dependencia:

:::BlockQuote
dependencies \{
&#x20;   implementation(project("\:sdconn"))
}
:::

## Permisos

El SDK declara estos permisos en su manifiesto (se fusionan automáticamente):

:::BlockQuote
\<uses-permission android\:name="android.permission.INTERNET" />
\<uses-feature android\:name="android.hardware.usb.host" android\:required="false" />
:::

- **INTERNET** — Necesario para comunicarse con el dispositivo por HTTP y con la API de Mobbex.
- **USB Host** — Necesario solo para conexión serial. Declarado como required="false" para que la app funcione en dispositivos sin USB Host (usando solo HTTP).

### Permisos USB

Cuando se conecta un dispositivo USB, Android muestra un diálogo pidiendo permiso al usuario. Si se desea aceptar automáticamente, agregar un intent-filter en la Activity principal:

:::BlockQuote
\<activity android\:name=".MainActivity">
&#x20;   \<intent-filter>
&#x20;       \<action android\:name="android.hardware.usb.action.USB\_DEVICE\_ATTACHED" />
&#x20;   \</intent-filter>
&#x20;   \<meta-data
&#x20;       android\:name="android.hardware.usb.action.USB\_DEVICE\_ATTACHED"
&#x20;       android\:resource="@xml/device\_filter" />
\</activity>
:::

Con res/xml/device\_filter.xml:

:::BlockQuote
\<?xml version="1.0" encoding="utf-8"?>
\<resources>
&#x20;   \<usb-device />
\</resources>
:::

## Inicialización

:::BlockQuote
import com.mobbex.sdconn.SDConn
import com.mobbex.sdconn.model.SDConnConfig

*// Con valores por defecto*
val sdconn = SDConn(context)

*// Con configuración personalizada*
val sdconn = SDConn(context, SDConnConfig(
&#x20;   serialBaudRate = 115200,
&#x20;   mobbexApiBase = "https\://api.mobbex.com",
&#x20;   debug = true
))
:::

Se recomienda crear una única instancia de SDConn a nivel de Application o usando inyección de dependencias.

## Configurar credenciales

Las credenciales son la **Clave API** y el **Token de Acceso** de Mobbex. Son necesarias para que el SDK pueda crear operaciones de pago.

:::BlockQuote
sdconn.setCredentials(
&#x20;   apiKey = "clave\_api\_mobbex",
&#x20;   accessToken = "token\_de\_acceso\_mobbex"
)
:::

Las credenciales se almacenan cifradas con EncryptedSharedPreferences (AES-256). Persisten entre sesiones de la aplicación.

### Verificar si están configuradas

:::BlockQuote
if (sdconn.credentialsConfigured) \{
&#x20;   *// Listo para operar*
}
:::

### Borrar credenciales

:::BlockQuote
sdconn.clearCredentials()
:::

## Conectar un dispositivo

### Opción 1: Serial (USB)

:::BlockQuote
try \{
&#x20;   sdconn.connectSerial()
&#x20;   *// Conectado — el estado cambia a Connected(mode="serial", detail="portName")*
} catch (e: SDConnException) \{
&#x20;   *// No se encontró dispositivo USB serial*
}
:::

El SDK:

1. Busca todos los drivers USB serial disponibles.
2. Abre cada puerto y envía un mensaje HELLO.
3. El primer dispositivo que responda queda conectado.
4. Si ninguno responde, lanza SDConnException.

### Opción 2: Red (HTTP)

La conexión HTTP requiere un proceso de emparejamiento con PIN:

:::BlockQuote
*// 1. Solicitar emparejamiento — el dispositivo muestra un PIN de 6 dígitos*
sdconn.requestPairing(ip = "192.168.1.50", port = 5000)

*// 2. El usuario lee el PIN de la pantalla del dispositivo*

*// 3. Confirmar con el PIN*
val device = sdconn.confirmPairing(
&#x20;   ip = "192.168.1.50",
&#x20;   port = 5000,
&#x20;   pin = "847293"
)
*// Conectado — el dispositivo queda en modo pinpad*
*// Se guarda un token de confianza para futuras reconexiones*
:::

### Conexión HTTP directa (sin emparejamiento)

Si solo se necesita conectar sin el flujo de pairing (por ejemplo, para pruebas):

:::BlockQuote
sdconn.connectHttp(ip = "192.168.1.50", port = 5000)
:::

### Reconexión automática

Una vez emparejado un dispositivo, se puede reconectar sin PIN:

:::BlockQuote
if (sdconn.hasSavedDevice()) \{
&#x20;   try \{
&#x20;       val device = sdconn.reconnect()
&#x20;       *// Reconectado a $&#x7B;device.name} en $&#x7B;device.ip}:$&#x7B;device.port}*
&#x20;   } catch (e: SDConnException) \{
&#x20;       *// Dispositivo no alcanzable o token rechazado*
&#x20;   }
}
:::

### Observar el estado de conexión

:::BlockQuote
lifecycleScope.launch \{
&#x20;   sdconn.connectionState.collect \{ state ->
&#x20;       when (state) \{
&#x20;           is ConnectionState.Disconnected -> showDisconnected()
&#x20;           is ConnectionState.Searching    -> showSearching()
&#x20;           is ConnectionState.Connected    -> showConnected(state.mode, state.detail)
&#x20;           is ConnectionState.Error        -> showError(state.message)
&#x20;       }
&#x20;   }
}
:::

## Leer una tarjeta

La operación de lectura ejecuta internamente:

1. Obtiene información del dispositivo (serial number, modelo)
2. Obtiene la ubicación GPS del dispositivo
3. Crea una operación en la API de Mobbex (o usa un token externo si se proporciona)
4. Envía el token al dispositivo para que lea la tarjeta
5. Devuelve los datos de la tarjeta junto con el intentToken utilizado

### Flujo estándar (genera token contra API)

:::BlockQuote
import com.mobbex.sdconn.model.ReadRequest

val result = sdconn.read(ReadRequest(
&#x20;   total = 15.00,
&#x20;   currency = "ARS",
&#x20;   hideAmount = false,
&#x20;   test = false  *// true para operaciones de prueba*
))

if (result.result) \{
&#x20;   val sourceData = result.sourceData
&#x20;   val token = result.intentToken  *// Token generado por la API de Mobbex*
&#x20;   *// Datos de la tarjeta leída: pin, emvData, cryptogram, ksn, track2...*
} else \{
&#x20;   val error = result.error
&#x20;   *// "TIMEOUT", "CANCELLED", "NOT\_IN\_PINPAD\_MODE", etc.*
}
:::

### Flujo con token externo

Cuando ya se tiene un intentToken generado externamente (por ejemplo, desde un backend propio), se puede enviar directamente. En este caso el SDK **no contacta a la API de Mobbex** y además incluye la geolocalización del dispositivo en la respuesta:

:::BlockQuote
val result = sdconn.read(ReadRequest(
&#x20;   total = 15.00,
&#x20;   currency = "ARS",
&#x20;   intentToken = "eyJhbGciOiJIUzI1NiIs..."  *// Token externo*
))

if (result.result) \{
&#x20;   val sourceData = result.sourceData
&#x20;   val token = result.intentToken       *// Mismo token enviado*
&#x20;   val geo = result.geolocation         *// \[latitud, longitud] o null*
&#x20;   *// Procesar datos...*
}
:::

:::BlockQuote
**Nota:** Cuando se usa un token externo no es necesario tener configuradas las credenciales de Mobbex.
:::

### Flujo QR

Para pagos por QR, enviar el campo source con la referencia del medio de pago:

:::BlockQuote
val result = sdconn.read(ReadRequest(
&#x20;   total = 15.00,
&#x20;   currency = "ARS",
&#x20;   source = "arg.qr"  *// Activa el flujo QR*
))

if (result.result) \{
&#x20;   val operationId = result.operationId   *// ID de la operación*
&#x20;   val source = result.source             *// "arg.qr"*
&#x20;   val status = result.status             *// \{ code: "100", text: "Pendiente" }*
&#x20;   val token = result.intentToken         *// Token utilizado*

&#x20;   *// El QR se muestra en el dispositivo POS automáticamente.*
&#x20;   *// Consultar el estado de la operación externamente.*
&#x20;   *// Cuando el usuario escanee el QR y pague, el estado cambiará.*
} else \{
&#x20;   val error = result.error
}
:::

:::BlockQuote
**Nota:** El flujo QR siempre requiere credenciales configuradas. La respuesta no contiene sourceData ya que no se lee una tarjeta. Para cancelar la visualización del QR, llamar a cancelRead().
:::

### Cancelar lectura

:::BlockQuote
sdconn.cancelRead()
:::

Funciona tanto para cancelar una lectura de tarjeta como para quitar un QR de la pantalla del dispositivo.

## Mostrar resultado en pantalla

Después de procesar el pago en el servidor, mostrar el resultado en la pantalla del dispositivo:

:::BlockQuote
import com.mobbex.sdconn.model.DisplayRequest

*// Vista "result" — Pago aprobado (fire-and-forget)*
sdconn.display(DisplayRequest(
&#x20;   view = "result",       *// o simplemente omitir (es el defecto)*
&#x20;   status = "success",
&#x20;   title = "Pago Aprobado",
&#x20;   subtitle = "Visa \*\*\*\* 1234",
&#x20;   total = 15.00,
&#x20;   currency = "ARS",
&#x20;   text = "ID: 123456",
&#x20;   timeout = 10
))

*// Vista "result" — Pago rechazado*
sdconn.display(DisplayRequest(
&#x20;   status = "error",
&#x20;   title = "Rechazado",
&#x20;   timeout = 5
))

*// Vista "loading" — Procesando (fire-and-forget)*
sdconn.display(DisplayRequest(
&#x20;   view = "loading",
&#x20;   title = "Procesando pago...",
&#x20;   subtitle = "No retire la tarjeta",
&#x20;   timeout = 30
))

*// Vista "choose" — Selección de cuotas (bloqueante)*
val displayResult = sdconn.display(DisplayRequest(
&#x20;   view = "choose",
&#x20;   title = "Seleccione cuotas",
&#x20;   subtitle = "Visa \*\*\*\* 1234",
&#x20;   values = listOf(
&#x20;       DisplayOption(label = "1 Pago - $15.00", value = "1"),
&#x20;       DisplayOption(label = "3 Cuotas - $5.75 c/u", value = "3"),
&#x20;       DisplayOption(label = "6 Cuotas - $3.08 c/u", value = "6")
&#x20;   ),
&#x20;   timeout = 60
))

if (displayResult != null && displayResult.result) \{
&#x20;   val selected = displayResult.selected
&#x20;   *// selected?.label = "3 Cuotas - $5.75 c/u"*
&#x20;   *// selected?.value = "3"*
} else \{
&#x20;   *// Timeout o error*
}

*// Ocultar pantalla antes del timeout*
sdconn.dismissDisplay()
:::

### Vistas de display

| Vista     | Comportamiento                                              | Retorna       |
| --------- | ----------------------------------------------------------- | ------------- |
| "result"  | Muestra resultado con animación. Fire-and-forget.           | null          |
| "loading" | Muestra spinner de carga. Fire-and-forget.                  | null          |
| "choose"  | Muestra opciones. **Bloqueante** hasta selección o timeout. | DisplayResult |

### Valores de status (para vista result)

**Por nombre:**

| Valor     | Animación                   |
| --------- | --------------------------- |
| "success" | Círculo verde con check     |
| "error"   | Círculo rojo con X          |
| "neutral" | Círculo naranja con reloj   |
| "none"    | Sin animación, fondo blanco |

**Por código numérico:**

| Códigos                      | Se resuelve a |
| ---------------------------- | ------------- |
| 3, 4, 200, 300-302, 605, 800 | success       |
| 400-417, 500, 603, 604       | error         |
| 1, 2, 100, 210, 600-602, 610 | neutral       |
| Otros                        | none          |

## Desconectar

:::BlockQuote
*// Desconectar sin borrar datos guardados*
sdconn.disconnect()

*// Desconectar y borrar dispositivo + token de confianza*
sdconn.forgetDevice()
:::

## Cambiar modo del dispositivo

:::BlockQuote
sdconn.setMode("pinpad")
:::

| Modo    | Descripción                                             |
| ------- | ------------------------------------------------------- |
| normal  | Modo POS estándar con pantalla de ingreso de monto      |
| slave   | Modo cola. Recibe trabajos de pago desde una cola       |
| transit | Lectura continua de tarjetas para peajes/transporte     |
| pinpad  | Modo pinpad. Espera comandos de lectura del controlador |

## Información del dispositivo

:::BlockQuote
val info = sdconn.getDeviceInfo()
*// info.type, info.serialNumber, info.deviceExtras*

val location = sdconn.getDeviceLocation()
*// location.latitude, location.longitude, location.accuracy*
:::

La información del dispositivo se cachea tras la primera consulta. La ubicación se consulta en tiempo real.

## Flujo completo de ejemplo

:::BlockQuote
class PaymentActivity : AppCompatActivity() \{

&#x20;   private lateinit var sdconn: SDConn

&#x20;   override fun onCreate(savedInstanceState: Bundle?) \{
&#x20;       super.onCreate(savedInstanceState)

&#x20;       sdconn = SDConn(this, SDConnConfig(debug = true))
&#x20;       sdconn.setCredentials("mi\_api\_key", "mi\_access\_token")

&#x20;       lifecycleScope.launch \{
&#x20;           *// 1. Conectar por serial*
&#x20;           try \{
&#x20;               sdconn.connectSerial()
&#x20;           } catch (e: Exception) \{
&#x20;               showError("No se encontró dispositivo USB")
&#x20;               return\@launch
&#x20;           }

&#x20;           *// 2. Leer tarjeta*
&#x20;           val result = sdconn.read(ReadRequest(total = 100.00))

&#x20;           if (result.result) \{
&#x20;               *// 3. Enviar sourceData al servidor para procesar el pago*
&#x20;               val paymentOk = processPaymentOnServer(result.sourceData!!)

&#x20;               *// 4. Mostrar resultado en el dispositivo*
&#x20;               if (paymentOk) \{
&#x20;                   sdconn.display(DisplayRequest(
&#x20;                       status = "success",
&#x20;                       title = "Aprobado",
&#x20;                       total = 100.00
&#x20;                   ))
&#x20;               } else \{
&#x20;                   sdconn.display(DisplayRequest(
&#x20;                       status = "error",
&#x20;                       title = "Rechazado"
&#x20;                   ))
&#x20;               }
&#x20;           } else \{
&#x20;               showError(result.error ?: "Error desconocido")
&#x20;           }
&#x20;       }
&#x20;   }

&#x20;   override fun onDestroy() \{
&#x20;       super.onDestroy()
&#x20;       sdconn.disconnect()
&#x20;   }
}
:::

## Troubleshooting

### No se detecta el dispositivo USB

- Verificar que el dispositivo Android soporte **USB Host** (la mayoría de tablets y POS lo soportan).
- Verificar que el cable USB sea de datos (no solo de carga).
- Verificar que el usuario haya aceptado el diálogo de permisos USB.
- Activar debug = true en la configuración para ver logs detallados en logcat.

### Error "Device not reachable" en conexión HTTP

- Verificar que el dispositivo POS y el dispositivo Android estén en la **misma red Wi-Fi**.
- Verificar que el puerto (por defecto 5000) esté correcto.
- Probar la conexión con un navegador: http\://\<ip>:5000/device/hello.

### Error "Mobbex credentials not configured"

- Llamar a sdconn.setCredentials(apiKey, accessToken) antes de ejecutar read().
- Verificar que las credenciales sean válidas.

### Error "TIMEOUT" en lectura de tarjeta

- El dispositivo no detectó una tarjeta en 120 segundos.
- Verificar que el dispositivo esté en modo **pinpad**.
- Verificar que el lector de tarjetas del dispositivo funcione correctamente.



# Descarga

::File{src="https://api.archbee.com/api/presign/wYlzYU9oe8HZjh9BkqeFY/t48HPneaIgJJl8OFFXxKM-20260320-185754.aar" label="sdconn-release.20260320155729.aar"}

