- Generar el .aar ejecutando el script de build:
cd sdconn-android
./build-aar.sh
- Copiar dist/sdconn-release.aar al directorio libs/ del módulo de la aplicación.
- Agregar la dependencia en build.gradle.kts:
dependencies {
implementation(files("libs/sdconn-release.aar"))
// Dependencias transitivas (requeridas)
implementation("com.github.mik3y:usb-serial-for-android:3.8.1")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
}
- Agregar el repositorio de JitPack en settings.gradle.kts (necesario para usb-serial-for-android):
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
Si se prefiere incluir el SDK como módulo del proyecto:
- Copiar la carpeta sdconn/ al proyecto.
- Agregar en settings.gradle.kts:
dependencies {
implementation(project(":sdconn"))
}
El SDK declara estos permisos en su manifiesto (se fusionan automáticamente):
<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).
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:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
Con res/xml/device_filter.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device />
</resources>
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(
serialBaudRate = 115200,
mobbexApiBase = "https://api.mobbex.com",
debug = true
))
Se recomienda crear una única instancia de SDConn a nivel de Application o usando inyección de dependencias.
Las credenciales son la Clave API y el Token de Acceso de Mobbex. Son necesarias para que el SDK pueda crear operaciones de pago.
sdconn.setCredentials(
apiKey = "clave_api_mobbex",
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
if (sdconn.credentialsConfigured) {
// Listo para operar
}
sdconn.clearCredentials()
try {
sdconn.connectSerial()
// Conectado — el estado cambia a Connected(mode="serial", detail="portName")
} catch (e: SDConnException) {
// No se encontró dispositivo USB serial
}
- Busca todos los drivers USB serial disponibles.
- Abre cada puerto y envía un mensaje HELLO.
- El primer dispositivo que responda queda conectado.
- Si ninguno responde, lanza SDConnException.
La conexión HTTP requiere un proceso de emparejamiento con PIN:
// 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(
ip = "192.168.1.50",
port = 5000,
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):
sdconn.connectHttp(ip = "192.168.1.50", port = 5000)
Una vez emparejado un dispositivo, se puede reconectar sin PIN:
if (sdconn.hasSavedDevice()) {
try {
val device = sdconn.reconnect()
// Reconectado a ${device.name} en ${device.ip}:${device.port}
} catch (e: SDConnException) {
// Dispositivo no alcanzable o token rechazado
}
}
Observar el estado de conexión
lifecycleScope.launch {
sdconn.connectionState.collect { state ->
when (state) {
is ConnectionState.Disconnected -> showDisconnected()
is ConnectionState.Searching -> showSearching()
is ConnectionState.Connected -> showConnected(state.mode, state.detail)
is ConnectionState.Error -> showError(state.message)
}
}
}
La operación de lectura ejecuta internamente:
- Obtiene información del dispositivo (serial number, modelo)
- Obtiene la ubicación GPS del dispositivo
- Crea una operación en la API de Mobbex (o usa un token externo si se proporciona)
- Envía el token al dispositivo para que lea la tarjeta
- Devuelve los datos de la tarjeta junto con el intentToken utilizado
Flujo estándar (genera token contra API)
import com.mobbex.sdconn.model.ReadRequest
val result = sdconn.read(ReadRequest(
total = 15.00,
currency = "ARS",
hideAmount = false,
test = false // true para operaciones de prueba
))
if (result.result) {
val sourceData = result.sourceData
val token = result.intentToken // Token generado por la API de Mobbex
// Datos de la tarjeta leída: pin, emvData, cryptogram, ksn, track2...
} else {
val error = result.error
// "TIMEOUT", "CANCELLED", "NOT_IN_PINPAD_MODE", etc.
}
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:
val result = sdconn.read(ReadRequest(
total = 15.00,
currency = "ARS",
intentToken = "eyJhbGciOiJIUzI1NiIs..." // Token externo
))
if (result.result) {
val sourceData = result.sourceData
val token = result.intentToken // Mismo token enviado
val geo = result.geolocation // [latitud, longitud] o null
// Procesar datos...
}
Nota: Cuando se usa un token externo no es necesario tener configuradas las credenciales de Mobbex.
Para pagos por QR, enviar el campo source con la referencia del medio de pago:
val result = sdconn.read(ReadRequest(
total = 15.00,
currency = "ARS",
source = "arg.qr" // Activa el flujo QR
))
if (result.result) {
val operationId = result.operationId // ID de la operación
val source = result.source // "arg.qr"
val status = result.status // { code: "100", text: "Pendiente" }
val token = result.intentToken // Token utilizado
// El QR se muestra en el dispositivo POS automáticamente.
// Consultar el estado de la operación externamente.
// Cuando el usuario escanee el QR y pague, el estado cambiará.
} else {
val error = result.error
}
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().
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:
import com.mobbex.sdconn.model.DisplayRequest
// Vista "result" — Pago aprobado (fire-and-forget)
sdconn.display(DisplayRequest(
view = "result", // o simplemente omitir (es el defecto)
status = "success",
title = "Pago Aprobado",
subtitle = "Visa **** 1234",
total = 15.00,
currency = "ARS",
text = "ID: 123456",
timeout = 10
))
// Vista "result" — Pago rechazado
sdconn.display(DisplayRequest(
status = "error",
title = "Rechazado",
timeout = 5
))
// Vista "loading" — Procesando (fire-and-forget)
sdconn.display(DisplayRequest(
view = "loading",
title = "Procesando pago...",
subtitle = "No retire la tarjeta",
timeout = 30
))
// Vista "choose" — Selección de cuotas (bloqueante)
val displayResult = sdconn.display(DisplayRequest(
view = "choose",
title = "Seleccione cuotas",
subtitle = "Visa **** 1234",
values = listOf(
DisplayOption(label = "1 Pago - $15.00", value = "1"),
DisplayOption(label = "3 Cuotas - $5.75 c/u", value = "3"),
DisplayOption(label = "6 Cuotas - $3.08 c/u", value = "6")
),
timeout = 60
))
if (displayResult != null && displayResult.result) {
val selected = displayResult.selected
// selected?.label = "3 Cuotas - $5.75 c/u"
// selected?.value = "3"
} else {
// Timeout o error
}
// Ocultar pantalla antes del timeout
sdconn.dismissDisplay()
Valores de status (para vista result)
// Desconectar sin borrar datos guardados
sdconn.disconnect()
// Desconectar y borrar dispositivo + token de confianza
sdconn.forgetDevice()
Cambiar modo del dispositivo
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
class PaymentActivity : AppCompatActivity() {
private lateinit var sdconn: SDConn
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sdconn = SDConn(this, SDConnConfig(debug = true))
sdconn.setCredentials("mi_api_key", "mi_access_token")
lifecycleScope.launch {
// 1. Conectar por serial
try {
sdconn.connectSerial()
} catch (e: Exception) {
showError("No se encontró dispositivo USB")
return@launch
}
// 2. Leer tarjeta
val result = sdconn.read(ReadRequest(total = 100.00))
if (result.result) {
// 3. Enviar sourceData al servidor para procesar el pago
val paymentOk = processPaymentOnServer(result.sourceData!!)
// 4. Mostrar resultado en el dispositivo
if (paymentOk) {
sdconn.display(DisplayRequest(
status = "success",
title = "Aprobado",
total = 100.00
))
} else {
sdconn.display(DisplayRequest(
status = "error",
title = "Rechazado"
))
}
} else {
showError(result.error ?: "Error desconocido")
}
}
}
override fun onDestroy() {
super.onDestroy()
sdconn.disconnect()
}
}
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.
- 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.