bytes.cat

La wiki d'FP d'informàtica

Eines de l'usuari

Eines del lloc


Barra lateral

ASIX Administració de Sistemes Informàtics i Xarxes
Tots els mòduls del cicle
MP01 Implantació de sistemes operatius
Totes les UFs del modul
MP02 Gestió de bases de dades
Totes les UFs del modul
MP03 Programació bàsica
Totes les UFs del modul
MP04 Llenguatges de marques i sistemes de gestió d'informació
Totes les UFs del modul
MP05 Fonaments de maquinari
Totes les UFs del modul
MP06 Administració de sistemes operatius
Totes les UFs del modul
MP07 Planificació i administració de xarxes
Totes les UFs del modul
MP08 Serveis de xarxa i Internet
Totes les UFs del modul
MP09 Implantació d'aplicacions web
Totes les UFs del modul
MP10 Administració de sistemes gestors de bases de dades
Totes les UFs del modul
MP11 Seguretat i alta disponibilitat
Totes les UFs del modul
MP12 Formació i orientació laboral
Totes les UFs del modul
MP13 Empresa i iniciativa emprenedora
Totes les UFs del modul
MP14 Projecte
Totes les UFs del modul
DAM Desenvolupament d’aplicacions multiplataforma
Tots els mòduls del cicle
MP01 Sistemes informàtics
Totes les UFs del modul
MP02 Bases de dades
Totes les UFs del modul
MP03 Programació bàsica
Totes les UFs del modul
MP04 Llenguatges de marques i sistemes de gestió d'informació
Totes les UFs del modul
MP05 Entorns de desenvolupament
Totes les UFs del modul
MP06 Accés a dades
Totes les UFs del modul
MP07 Desenvolupament d’interfícies
Totes les UFs del modul
MP08 Programació multimèdia i dispositius mòbils
Totes les UFs del modul
MP09 Programació de serveis i processos
Totes les UFs del modul
MP10 Sistemes de gestió empresarial
Totes les UFs del modul
MP11 Formació i orientació laboral
Totes les UFs del modul
MP12 Empresa i iniciativa emprenedora
Totes les UFs del modul
MP13 Projecte de síntesi
Totes les UFs del modul
MPDual Mòdul Dual / Projecte
DAW Desenvolupament d’aplicacions web
Tots els mòduls del cicle
MP01 Sistemes informàtics
Totes les UFs del modul
MP02 Bases de dades
Totes les UFs del modul
MP03 Programació
Totes les UFs del modul
MP04 Llenguatge de marques i sistemes de gestió d’informació
Totes les UFs del modul
MP05 Entorns de desenvolupament
Totes les UFs del modul
MP06 Desenvolupament web en entorn client
Totes les UFs del modul
MP07 Desenvolupament web en entorn servidor
Totes les UFs del modul
MP08 Desplegament d'aplicacions web
Totes les UFs del modul
MP09 Disseny d'interfícies web
Totes les UFs del modul
MP10 Formació i Orientació Laboral
Totes les UFs del modul
MP11 Empresa i iniciativa emprenedora
Totes les UFs del modul
MP12 Projecte de síntesi
Totes les UFs del modul
SMX Sistemes Microinformàtics i Xarxes
Tots els mòduls del cicle
MP01 Muntatge i manteniment d’equips
Totes les UFs del modul
MP02 Sistemes Operatius Monolloc
Totes les UFs del modul
MP03 Aplicacions ofimàtiques
Totes les UFs del modul
MP04 Sistemes operatius en xarxa
Totes les UFs del modul
MP05 Xarxes locals
Totes les UFs del modul
MP06 Seguretat informàtica
Totes les UFs del modul
MP07 Serveis de xarxa
Totes les UFs del modul
MP08 Aplicacions Web
Totes les UFs del modul
MP09 Formació i Orientació Laboral
Totes les UFs del modul
MP10 Empresa i iniciativa emprenedora
Totes les UFs del modul
MP11 Anglès
Totes les UFs del modul
MP12 Síntesi
Totes les UFs del modul
CETI Ciberseguretat en Entorns de les Tecnologies de la Informació
Tots els mòduls del cicle
CiberOT Ciberseguretat en Entorns d'Operació
Tots els mòduls del cicle
android_bluetooth

Android i Bluetooth

El tema de Bluetooth és ample i complex. En aquest article abordarem el codi necessari per:

  • Gestionar els permisos per a comunicació Bluetooth.
  • Llistar i triar entre els dispositius aparellats al sistema operatiu.
  • Utilitzar GATT per a recepció de dades entre dispositius BLE o Bluetooth Low Emission.

android-bluetooth.jpg

Altres temes pendents d'abordar aquí:

  • Escaneig de dispositius.
  • GATT per a la transmissió de dades.

La pràctica que s'inclou aquí està pensada per a rebre dades d'un dispositiu ESP32. Podeu trobar aquí la pràctica de captura amb càmera i transmissió de dades per BLE.


Llistar els dispositius emparellats

Obtenir aquesta llista és senzill accedint BluetoothAdapter.bondedDevices. En aquest exemple, a més, filtrem els dispositius que poden ser del tipus BLE (Bluetooth Low Emission), però es pot treure el filtre si cal:

@SuppressLint("MissingPermission")
fun updatePairedDevices() {
    // empty list
    dataset.clear()
 
    // update list
    val bluetoothManager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
    val bluetoothAdapter = bluetoothManager.adapter
    for( elem in bluetoothAdapter.bondedDevices.filter { device ->
        // Filtrar per dispositius BLE
        device.type == BluetoothDevice.DEVICE_TYPE_LE ||
                device.type == BluetoothDevice.DEVICE_TYPE_DUAL ||
                device.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN
    } ) {
        // afegim element al dataset
        dataset.add( elem )
    }
}


Gestió de permisos

La gestió de permisos del Bluetooth és una part més engorrosa.

Primer de tot, declarar els permisos estàtics a AndroidManifest.xml:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 
<!-- Per a Android 12 o superior -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

Seguidament, necessitarem al nostre codi demanar els permisos dinàmics. Abans de cridar a la funció updatePairedDevices() ja vista abans, haurem de fer una funció «wrapper» que abans s'asseguri que els permisos estan ben gestionats.

private val REQUEST_CODE_BLUETOOTH = 100 // es pot posar un nombre aleatori no emprat en cap altre lloc
 
private fun requestBluetoothPermissionAndUpdate() {
    val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        // Android 12+ requereix BLUETOOTH_CONNECT
        Manifest.permission.BLUETOOTH_CONNECT
    } else {
        // Versions anteriors
        Manifest.permission.BLUETOOTH
    }
 
    if (ContextCompat.checkSelfPermission(this, permission) !=
        PackageManager.PERMISSION_GRANTED) {
 
        // Demanar el permís
        ActivityCompat.requestPermissions(
            this,
            arrayOf(permission),
            REQUEST_CODE_BLUETOOTH
        )
    } else {
        // Permís ja concedit - llegir dispositius
        updatePairedDevices()
    }
}
 
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
 
    if (requestCode == REQUEST_CODE_BLUETOOTH) {
        if (grantResults.isNotEmpty() && grantResults[0] ==
            PackageManager.PERMISSION_GRANTED) {
            // Permís concedit - llegir dispositius
            updatePairedDevices()
        } else {
            // Permís denegat
            Toast.makeText(this, "Permís necessari per a llegir Bluetooth",
                Toast.LENGTH_SHORT).show()
        }
    }
}


Rebre fotos amb BLE GATT : BLEconnDialog

Per la comunicació de dades (sèrie) es sol utilitzar el Bluetooth GATT o Generic Attribute Profile. GATT és un protocol de serveis i característiques que defineix com s'intercanvien dades entre dispositius Bluetooth Low Energy (BLE). És la base de tota comunicació en dispositius BLE com ara wearables, sensors mèdics, beacons, etc. GATT és essencial per a l'ecosistema IoT i dispositius wearables, permetent una comunicació estructurada i eficient entre dispositius.

Conceptes claus:

  • Profile: Especificació estàndard (ex: Perfil de freqüència cardíaca)
  • Service: Col·lecció de característiques relacionades (ex: «Servei de freqüència cardíaca»)
  • Characteristic: Conté una dada específica (ex: «Mesura de freqüència cardíaca»)
  • Descriptor: Metadades sobre les característiques

Us presento un codi per recepció de dades BLE encapsulat dins un Dialog, per tal d'efectuar la recepció de les dades i tancar la comunicació, i tot dins d'un arxiu separat del codi de l'Activity o Fragment.

El BLEconnDialog no gestiona els permisos, assumim que ja s'ha fet abans en el codi per accedir al llistat de dispositius emparellats o fent un scan.


Layout Dialog

El layout conté diversos TextView per a l'estat de la comunicació, una ProgressBar pel progrés de la transmissió i una ImageView per mostrar la imatge rebuda (que també s'enregistrarà al sistema d'arxius del mòbil).

dialog_ble_conn.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="24dp">
 
    <TextView
        android:id="@+id/tvDeviceName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"
        android:gravity="center"
        android:layout_marginBottom="16dp"/>
 
    <TextView
        android:id="@+id/tvDeviceAddress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:gravity="center"
        android:layout_marginBottom="24dp"/>
 
    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:layout_marginBottom="16dp"/>
 
    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:gravity="center"
        android:layout_marginBottom="24dp"/>
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="end"
        android:orientation="horizontal">
 
        <Button
            android:id="@+id/btnCancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:text="@android:string/cancel" />
 
        <Button
            android:id="@+id/btnConnect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Connectar" />
 
    </LinearLayout>
 
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:srcCompat="@tools:sample/avatars" />
 
</LinearLayout>


BLEconnDialog

BLEconnDialog.kt
class BLEconnDialog(
    context: Context,
    private val device: BluetoothDevice,
    private val connectionCallback: BLEConnectionCallback
) : Dialog(context) {
 
    interface BLEConnectionCallback {
        fun onConnectionSuccess(gatt: BluetoothGatt)
        fun onConnectionFailed(error: String)
        fun onConnectionCancelled()
        fun onReceivedImage(file: File)
    }
 
    // Views
    private lateinit var tvDeviceName: TextView
    private lateinit var tvDeviceAddress: TextView
    private lateinit var tvStatus: TextView
    private lateinit var tvImage: ImageView
    private lateinit var progressBar: ProgressBar
    private lateinit var btnConnect: Button
    private lateinit var btnCancel: Button
 
    // BLE
    // UUIDs
    private val SERVICE_UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
    private val CHARACTERISTIC_UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361b26a8")
    private var bluetoothGatt: BluetoothGatt? = null
    private val handler = Handler(Looper.getMainLooper())
    private var isConnecting = false
    private val CONNECTION_TIMEOUT = 10000L // 10 segons
    private val SCAN_PERIOD: Long = 10000
    private val RECEIVE_TIMEOUT: Long = 30000  // 30 segons timeout
 
 
    // Variables per foto
    private val receivedData = ByteArrayOutputStream()
    lateinit private var receivedFile : File
    private var totalSize = 0
    private var isReceiving = false
    private var received = false
    private var lastPacketTime = 0L
    private var packetCount = 0
 
    // Callback de timeout
    @SuppressLint("MissingPermission")
    private val connectionTimeoutRunnable = Runnable {
        if (isConnecting) {
            disconnect()
            connectionCallback.onConnectionFailed("Timeout de connexió")
            dismiss()
        }
    }
 
    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.dialog_ble_conn)
 
        // Inicialitzar views
        tvDeviceName = findViewById(R.id.tvDeviceName)
        tvDeviceAddress = findViewById(R.id.tvDeviceAddress)
        tvStatus = findViewById(R.id.tvStatus)
        tvImage = findViewById(R.id.imageView)
        progressBar = findViewById(R.id.progressBar)
        btnConnect = findViewById(R.id.btnConnect)
        btnCancel = findViewById(R.id.btnCancel)
 
        // Configurar dades del dispositiu
        val deviceName = device.name ?: "Dispositiu desconegut"
        tvDeviceName.text = deviceName
        tvDeviceAddress.text = device.address
 
        // Configurar botons
        btnConnect.setOnClickListener {
            if( received ) {
                connectionCallback.onReceivedImage(receivedFile)
                disconnect()
                dismiss()
            }
            if (!isConnecting) {
                connectToDevice()
            }
        }
 
        btnCancel.setOnClickListener {
            cancelConnection()
        }
 
        // Connectar automàticament al mostrar el dialog
        connectToDevice()
    }
 
    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    private fun connectToDevice() {
        if (isConnecting) return
 
        isConnecting = true
        updateUIForConnecting()
 
        // Iniciar timeout
        handler.postDelayed(connectionTimeoutRunnable, CONNECTION_TIMEOUT)
 
        // Connectar al dispositiu BLE
        // Nota: Necessites tenir el context de l'activitat o un context vàlid
        val context = context.applicationContext ?: context
        bluetoothGatt = device.connectGatt(context, false, gattCallback)
 
        // En alguns dispositius, cal fer un connect explícit
        // bluetoothGatt?.connect()
    }
 
    private fun updateUIForConnecting() {
        tvStatus.text = "Connectant..."
        btnConnect.isEnabled = false
        btnConnect.text = "Connectant..."
        progressBar.isIndeterminate = true
    }
 
    private fun updateUIForConnected() {
        tvStatus.text = "Connectat"
        btnConnect.isEnabled = false
        btnConnect.text = "Connectat"
        progressBar.isIndeterminate = false
        progressBar.progress = 100
    }
 
    private fun updateUIForDisconnected() {
        tvStatus.text = "Desconnectat"
        btnConnect.isEnabled = true
        btnConnect.text = "Connectar"
        progressBar.isIndeterminate = false
        progressBar.progress = 0
    }
 
    private fun updateUIForReceived() {
        //tvStatus.text = "Desconnectat"
        btnConnect.isEnabled = true
        btnConnect.text = "Fet!"
        //progressBar.isIndeterminate = false
        //progressBar.progress = 0
    }
 
 
    @SuppressLint("MissingPermission")
    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            handler.post {
                when (newState) {
                    BluetoothProfile.STATE_CONNECTED -> {
                        // Cancel·lar timeout
                        handler.removeCallbacks(connectionTimeoutRunnable)
                        isConnecting = false
 
                        // Actualitzar UI
                        updateUIForConnected()
 
                        // Sol·licitar MTU més gran
                        gatt.requestMtu(517)
 
                        // Descobrir serveis
                        //gatt.discoverServices()
                        // Descobrir serveis després d'un breu retard
                        handler.postDelayed({
                            gatt.discoverServices()
                        }, 500)
                    }
 
                    BluetoothProfile.STATE_DISCONNECTED -> {
                        handler.removeCallbacks(connectionTimeoutRunnable)
                        isConnecting = false
 
                        if (status == BluetoothGatt.GATT_SUCCESS) {
                            updateUIForDisconnected()
                        } else {
                            // Error de connexió
                            val errorMsg = when (status) {
                                0x08 -> "Timeout"
                                0x13 -> "Terminat per host local"
                                0x16 -> "Terminat per host remot"
                                0x3E -> "No connectat"
                                else -> "Error desconegut: $status"
                            }
 
                            tvStatus.text = "Error: $errorMsg"
                            connectionCallback.onConnectionFailed(errorMsg)
                        }
                    }
                }
            }
        }
 
        override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
            super.onMtuChanged(gatt, mtu, status)
            handler.post {
                Log.d("BLE", "MTU canviat a: $mtu")
                tvStatus.text = "MTU: $mtu"
            }
        }
 
        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            super.onServicesDiscovered(gatt, status)
 
            if (status == BluetoothGatt.GATT_SUCCESS) {
                handler.post {
                    //binding.statusText.text = "Serveis descoberts"
                }
 
                val service = gatt.getService(SERVICE_UUID)
                service?.let {
                    val photoCharacteristic = it.getCharacteristic(CHARACTERISTIC_UUID)
                    photoCharacteristic?.let { characteristic ->
                        // Habilitar notificacions
                        gatt.setCharacteristicNotification(characteristic, true)
 
                        val descriptor = characteristic.getDescriptor(
                            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
                        )
                        descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                        gatt.writeDescriptor(descriptor)
 
                        /*handler.post {
                            binding.statusText.text = "Llest per rebre fotos!"
                            binding.photoStatus.text = "Esperant foto..."
                            Toast.makeText(this@MainActivity,
                                "Connectat! Prem BOOT a l'ESP32",
                                Toast.LENGTH_SHORT).show()
                        }*/
                    } ?: run {
                        handler.post {
                            Log.d("BT","Error: Característica no trobada")
                            //binding.statusText.text = "Error: Característica no trobada"
                        }
                    }
                } ?: run {
                    handler.post {
                        Log.d("BT","Error: Servei no trobat")
                        //binding.statusText.text = "Error: Servei no trobat"
                    }
                }
            } else {
                handler.post {
                    Log.d("BT","Error descobrint serveis: $status")
                    //binding.statusText.text = "Error descobrint serveis: $status"
                }
            }
        }
 
        override fun onCharacteristicChanged(gatt: BluetoothGatt,
                                             characteristic: BluetoothGattCharacteristic) {
            super.onCharacteristicChanged(gatt, characteristic)
 
            if (characteristic.uuid == CHARACTERISTIC_UUID) {
                handleIncomingData(characteristic.value)
            }
        }
 
        override fun onCharacteristicRead(gatt: BluetoothGatt,
                                          characteristic: BluetoothGattCharacteristic,
                                          status: Int) {
            super.onCharacteristicRead(gatt, characteristic, status)
            Log.d("BLE", "Característica llegida: ${characteristic.uuid}")
        }
 
        override fun onCharacteristicWrite(gatt: BluetoothGatt,
                                           characteristic: BluetoothGattCharacteristic,
                                           status: Int) {
            super.onCharacteristicWrite(gatt, characteristic, status)
            Log.d("BLE", "Característica escrita: $status")
        }
 
 
 
    }
 
    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    private fun disconnect() {
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
        isConnecting = false
    }
 
    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
    private fun cancelConnection() {
        handler.removeCallbacks(connectionTimeoutRunnable)
        disconnect()
        connectionCallback.onConnectionCancelled()
        dismiss()
    }
 
    override fun dismiss() {
        handler.removeCallbacks(connectionTimeoutRunnable)
        super.dismiss()
    }
 
    override fun onDetachedFromWindow() {
        handler.removeCallbacks(connectionTimeoutRunnable)
        super.onDetachedFromWindow()
    }
 
    companion object {
        const val TAG = "BLEconnDialog"
    }
 
 
    // BLE data receive
    ///////////////////////
    private fun handleIncomingData(data: ByteArray) {
        handler.post {
            packetCount++
            lastPacketTime = System.currentTimeMillis()
 
            Log.d("BLE", "Paquet $packetCount rebut: ${data.size} bytes")
            Log.d("BLE", "Primers bytes: ${data.take(4).joinToString("") { "%02X".format(it) }}")
 
            // Verificar si és paquet de finalització
            if (data.size == 4 && data.contentEquals(byteArrayOf(0xFF.toByte(),
                    0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()
                ))) {
                if (isReceiving && receivedData.size() > 0) {
                    completePhotoTransfer()
                } else {
                    tvStatus.text = "Finalització rebuda sense dades"
                }
                return@post
            }
 
            // Primer paquet (pot ser la mida)
            if (!isReceiving && data.size == 4) {
                // Intentar interpretar com a mida de 32 bits
                try {
                    totalSize = (data[0].toInt() and 0xFF) +
                            ((data[1].toInt() and 0xFF) shl 8) +
                            ((data[2].toInt() and 0xFF) shl 16) +
                            ((data[3].toInt() and 0xFF) shl 24)
 
                    isReceiving = true
                    receivedData.reset()
 
                    tvStatus.text = "Rebent foto ($totalSize bytes)..."
                    progressBar.max = totalSize
                    progressBar.progress = 0
 
                    Log.d("BLE", "Mida anunciada: $totalSize bytes")
 
                    // Iniciar timeout
                    startReceiveTimeout()
 
                } catch (e: Exception) {
                    Log.e("BLE", "Error interpretant mida: ${e.message}")
                }
                return@post
            }
 
            // Si estem rebent, afegir dades
            if (isReceiving) {
                receivedData.write(data)
 
                val currentSize = receivedData.size()
                progressBar.progress = currentSize
 
                // Actualitzar estat cada certs paquets
                if (packetCount % 10 == 0 || currentSize == totalSize) {
                    val percent = if (totalSize > 0)
                        (currentSize * 100) / totalSize else 0
 
                    tvStatus.text =
                        "Rebent: $currentSize/$totalSize bytes ($percent%)"
 
                    Log.d("BLE", "Progrés: $currentSize/$totalSize ($percent%)")
                }
 
                // Reiniciar timeout amb cada paquet
                resetReceiveTimeout()
 
                // Si hem arribat a la mida esperada, completar
                if (totalSize > 0 && currentSize >= totalSize) {
                    completePhotoTransfer()
                }
            }
        }
    }
 
    private fun startReceiveTimeout() {
        handler.removeCallbacks(receiveTimeoutRunnable)
        handler.postDelayed(receiveTimeoutRunnable, RECEIVE_TIMEOUT)
    }
 
    private fun resetReceiveTimeout() {
        handler.removeCallbacks(receiveTimeoutRunnable)
        handler.postDelayed(receiveTimeoutRunnable, RECEIVE_TIMEOUT)
    }
 
    private val receiveTimeoutRunnable = Runnable {
        handler.post {
            if (isReceiving) {
                tvStatus.text = "⏰ Timeout! Transferència incompleta"
                Log.e("BLE", "Timeout en recepció de foto")
                resetPhotoTransfer()
            }
        }
    }
 
    private fun completePhotoTransfer() {
        val finalSize = receivedData.size()
 
        handler.post {
            tvStatus.text = "✅ Foto rebuda: $finalSize bytes"
            progressBar.progress = finalSize
 
            val dataStr = receivedData.toString().trim()
            Log.v("FOTO",dataStr)
            // Guardar foto
            try {
                val decodedData = Base64.decode(dataStr,Base64.DEFAULT)
                savePhoto(decodedData)
                // Notificació
                //Toast.makeText(this, "Foto rebuda: $finalSize bytes",
                //    Toast.LENGTH_LONG).show()
                Log.v("BT","Foto rebuda: $finalSize bytes")
            } catch (e : Exception) {
                Log.v("ERROR","Error en descodificació base64")
                e.printStackTrace()
            }
 
            // Reset
            resetPhotoTransfer()
        }
    }
 
    private fun resetPhotoTransfer() {
        isReceiving = false
        totalSize = 0
        receivedData.reset()
        packetCount = 0
        handler.removeCallbacks(receiveTimeoutRunnable)
    }
 
    private fun savePhoto(imageData: ByteArray) {
        try {
            val timestamp = System.currentTimeMillis()
            val filename = "ESP32_${timestamp}.jpg"
 
            // Guardar al directori de Pictures
            val picturesDir = Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_PICTURES)
            val fri3dDir = File(picturesDir, "ESP32_Camera")
 
            if (!fri3dDir.exists()) {
                fri3dDir.mkdirs()
            }
 
            receivedFile = File(fri3dDir, filename)
            FileOutputStream(receivedFile).use { fos ->
                fos.write(imageData)
                fos.flush()
            }
 
            Log.d("Photo", "Foto guardada: ${receivedFile.absolutePath}")
 
            // preview image a l'app
            tvImage.setImageURI(receivedFile.toUri())
 
            // Notificar galeria
            val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
            mediaScanIntent.data = android.net.Uri.fromFile(receivedFile)
            context.sendBroadcast(mediaScanIntent)
 
            tvStatus.text = "💾 Guardat: $filename"
            received = true
            updateUIForReceived()
            //dismiss()
 
        } catch (e: Exception) {
            Log.e("Photo", "Error guardant foto: ${e.message}")
            tvStatus.text = "❌ Error guardant foto"
        }
    }
 
}


Us del Dialog a Activity o Fragment

Per utilitzar el BLEconnDialog afegirem la seva interfície a l'Activity:

class MainActivity : AppCompatActivity(), BLEconnDialog.BLEConnectionCallback {
    // El codi de MainActivity aquí...
}

I hi implementarem les callback necessàries.

Quan volguem mostrar el BLEconnDialog cridarem showBLEDialog(device) amb el BluetoothDevice que haguem sel·leccionat prèviament (fent un scan o bé, en aquest cas, triant el device d'una llista de dispositius emparellats).

// DIALOG : cridar aquesta funció per mostrar-lo
////////////////////////////////////////////////
private fun showBLEDialog(device: BluetoothDevice) {
    bleDialog = BLEconnDialog(this, device, this)
    bleDialog?.apply {
        setCancelable(false)
        setOnCancelListener {
            onConnectionCancelled()
        }
        show()
    }
}
 
// DIALOG CALLBACKS
///////////////////////////////
override fun onConnectionSuccess(gatt: BluetoothGatt) {
    runOnUiThread {
        Toast.makeText(this, "Connectat amb èxit!", Toast.LENGTH_SHORT).show()
        // Aquí pots fer operacions amb el gatt connectat
        // Per exemple: llegir/escribre característiques
    }
}
 
override fun onConnectionFailed(error: String) {
    runOnUiThread {
        Toast.makeText(this, "Error de connexió: $error", Toast.LENGTH_LONG).show()
    }
}
 
override fun onConnectionCancelled() {
    runOnUiThread {
        Toast.makeText(this, "Connexió cancel·lada", Toast.LENGTH_SHORT).show()
    }
}
 
override fun onReceivedImage(file: File) {
    runOnUiThread {
        val filename = file.name
        Toast.makeText(this, "Imatge rebuda: $filename", Toast.LENGTH_SHORT).show()
    }
}
 
// Aquesta callback és de l'Activity
override fun onDestroy() {
    super.onDestroy()
    bleDialog?.dismiss()
}


Transmissió de dades per BLE

La pràctica que s'inclou aquí està pensada per a rebre dades d'un dispositiu ESP32. Podeu trobar aquí la pràctica de captura amb càmera i transmissió de dades per BLE.


android_bluetooth.txt · Darrera modificació: 2026/01/25 19:33 per enric_mieza_sanchez