본문 바로가기

Android/통신

[안드로이드 Kotlin] BLE(Bluetooth Low Energy) 통신 예제

728x90
반응형

 

전체적으로 BLE 기능 구현을 심플하게 작성하였습니다.

UI 업데이트 부분은, 데이터 바인딩을 사용하였습니다. BLE 기능구현 위주로 봐주세요.

코드 미리보기

 

  • Permission

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

BLE사용을 위해 위 세개의 퍼미션을 AndroidManifest에 추가해줍니다.

 

  • Ble 지원 확인

override fun onResume() {
   super.onResume()

   // finish app if the BLE is not supported
   if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            finish()
   }
}

ble를 지원하지 않으면 어플이 종료되도록 합니다.

 

  • Constants.kt

class Constants{
    companion object{
        // used to identify adding bluetooth names
        const val REQUEST_ENABLE_BT = 1
        // used to request fine location permission
        const val REQUEST_ALL_PERMISSION = 2
        val PERMISSIONS = arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION
        )
        
        //사용자 BLE UUID Service/Rx/Tx
        const val SERVICE_STRING = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
        const val CHARACTERISTIC_COMMAND_STRING = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
        const val CHARACTERISTIC_RESPONSE_STRING = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
        
        //BluetoothGattDescriptor 고정
        const val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"
    }
}

 

  • 변수 정의

    //scan results
    var scanResults: ArrayList<BluetoothDevice>? = ArrayList()

    //ble adapter
    private var bleAdapter: BluetoothAdapter? = repository.bleAdapter

    // BLE Gatt
    private var bleGatt: BluetoothGatt? = null

 

 

  • BLE Adapter 설정

fun setBLEAdapter(){
    // ble manager
    val bleManager: BluetoothManager? = MyApplication.applicationContext().getSystemService( BLUETOOTH_SERVICE ) as BluetoothManager
    // set ble adapter
    bleAdapter?= bleManager?.adapter
}

onCreate에서 BLE Adapter를 설정해줍니다.

 

  • 권한 확인 / 승인 요청

먼저, BLE 에 필요한 권한을 요청해줍니다.

// check if location permission
if (!hasPermissions(this, PERMISSIONS)) {
     requestPermissions(PERMISSIONS, REQUEST_ALL_PERMISSION)
}

위 구문을 onCreate에서 실행해 권한이 승인되었는지 확인해봅니다.

    /**
     * Request BLE enable
     */
    private fun requestEnableBLE() {
        val bleEnableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(bleEnableIntent, REQUEST_ENABLE_BT)
    }

    private fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null && permissions != null) {
            for (permission in permissions) {
                if (ActivityCompat.checkSelfPermission(context, permission)
                    != PackageManager.PERMISSION_GRANTED) {
                    return false
                }
            }
        }
        return true
    }
    // Permission check
    @RequiresApi(Build.VERSION_CODES.M)
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String?>,
        grantResults: IntArray
    ) {
        when (requestCode) {
            REQUEST_ALL_PERMISSION -> {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show()
                } else {
                    requestPermissions(permissions, REQUEST_ALL_PERMISSION)
                    Toast.makeText(this, "Permissions must be granted", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }


}

필요한 메서드 들입니다.

 

 

  • BLE Scan

scan 버튼을 눌러 스캔을 시작합니다.

먼저, 블루투스가 켜져 있는지 확인합니다.

   // check ble adapter and ble enabled
   if (bleAdapter == null || !bleAdapter?.isEnabled!!) {
         requestEnableBLE()
         statusTxt.set("Scanning Failed: ble not enabled")
         return
    }
        
        ...
    /**
     * Request BLE enable
     */
    private fun requestEnableBLE() {
        val bleEnableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(bleEnableIntent, REQUEST_ENABLE_BT)
    }

 

특정 장치만 스캔하도록 filter를 설정할 수 있습니다.

    //scan filter
    val filters: MutableList<ScanFilter> = ArrayList()
    val scanFilter: ScanFilter = ScanFilter.Builder()
            .setServiceUuid(ParcelUuid(UUID.fromString(SERVICE_STRING))) 
            .build()
    filters.add(scanFilter)

.setServiceUuid()대신 .setDeviceAddress(MAC_ADDR)를 사용해 Uuid 말고 특정 mac address만 스캔할 수 있습니다.

 

ble scan 설정을 low power로 설정합니다.

        // scan settings
        // set low power scan mode
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .build()

 

이제, 스캔을 시작합니다.

bleAdapter?.bluetoothLeScanner?.startScan(filters, settings, BLEScanCallback)

callback object를 통하여 스캔 성공/실패시를 다룰수 있고, scan result를 받아올수 있습니다.

/**
     * BLE Scan Callback
     */
    private val BLEScanCallback: ScanCallback = @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)
            Log.i(TAG, "Remote device name: " + result.device.name)
            addScanResult(result)
        }

        override fun onBatchScanResults(results: List<ScanResult>) {
            for (result in results) {
                addScanResult(result)
            }
        }

        override fun onScanFailed(_error: Int) {
            Log.e(TAG, "BLE scan failed with code $_error")
        }

        /**
         * Add scan result
         */
        private fun addScanResult(result: ScanResult) {
            // get scanned device
            val device = result.device
            // get scanned device MAC address
            val deviceAddress = device.address
            val deviceName = device.name
            
            // 중복 체크
            for (dev in scanResults!!) {
                if (dev.address == deviceAddress) return
            }
            // add arrayList
            scanResults?.add(result.device)
            // status text UI update
            statusTxt.set("add scanned device: $deviceAddress")
            // scanlist update 이벤트
            _listUpdate.value = Event(true)
        }
    }

 

저는 scanResult ArrayList를 만들어 리사이클러뷰에 띄우기 위해 list에 추가하였습니다.

var scanResults: ArrayList<BluetoothDevice>? = ArrayList()
...
scanResults?.add(result.device)

또한 반복해서 스캔하기 때문에 같은 어드레스가 이미 존재한다면, 더 추가하지 않도록 하였습니다.

for (dev in scanResults!!) {
     if (dev.address == deviceAddress) return
}

 

또는 다음을 통해 filter와 setting없이 모든 기기를 스캔할수도 있습니다.

bleAdapter?.bluetoothLeScanner?.startScan(BLEScanCallback)

 

또 한가지 필수적으로 해주어야하는 것은 스캔 중지입니다. 스캔을 계속하기 때문에 일정시간 후에 스캔을 중지하거나, 특정기기를 찾으면 스캔을 중지하도록 하여야합니다.

저는 스캔버튼을 눌러 스캔을 시작하고, 3초 후에 스캔을 중지하도록 하였습니다.

   @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    fun stopScan(){
        bleAdapter?.bluetoothLeScanner?.stopScan(BLEScanCallback)
        isScanning.set(false) //스캔 중지(버튼 활성화)
        btnScanTxt.set("Start Scan") //button text update
        scanResults = ArrayList() //list 초기화
        Log.d(TAG, "BLE Stop!")
    }
  // start scan
  bleAdapter?.bluetoothLeScanner?.startScan(filters, settings, BLEScanCallback)
  //bleAdapter?.bluetoothLeScanner?.startScan(BLEScanCallback)
  btnScanTxt.set("Scanning..")

  isScanning.set(true)
  //3초 후 스캔 중지
  //import kotlin.concurrent.schedule
  Timer("SettingUp", false).schedule(3000) { stopScan() }

 

  • Connect

// BLE Gatt
private var bleGatt: BluetoothGatt? = null

...

bleGatt = device?.connectGatt(context, false, gattClientCallback)

위와같이 특정 기기와 connect할 수 있습니다. 

device는 연결성공했을때의 addScanResult참고하세요. BluetoothDevice 입니다.

또한 gattClientCallback을 만들어주어야합니다. 

   /**
     * BLE gattClientCallback
     */
    private val gattClientCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)
            if( status == BluetoothGatt.GATT_FAILURE ) {
                disconnectGattServer()
                return
            } else if( status != BluetoothGatt.GATT_SUCCESS ) {
                disconnectGattServer()
                return
            }
            if( newState == BluetoothProfile.STATE_CONNECTED ) {
                // update the connection status message

                statusTxt.set("Connected")
                Log.d(TAG, "Connected to the GATT server")
                gatt.discoverServices()
            } else if ( newState == BluetoothProfile.STATE_DISCONNECTED ) {
                disconnectGattServer()
            }
        }
        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            super.onServicesDiscovered(gatt, status)


            // check if the discovery failed
            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "Device service discovery failed, status: $status")
                return
            }

            // log for successful discovery
            Log.d(TAG, "Services discovery is successful")

          
        }

        override fun onCharacteristicChanged(
            gatt: BluetoothGatt?,
            characteristic: BluetoothGattCharacteristic
        ) {
            super.onCharacteristicChanged(gatt, characteristic)
           //Log.d(TAG, "characteristic changed: " + characteristic.uuid.toString())
            readCharacteristic(characteristic)
        }

        override fun onCharacteristicWrite(
            gatt: BluetoothGatt?,
            characteristic: BluetoothGattCharacteristic?,
            status: Int
        ) {
            super.onCharacteristicWrite(gatt, characteristic, status)
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.d(TAG, "Characteristic written successfully")
            } else {
                Log.e(TAG, "Characteristic write unsuccessful, status: $status")
                disconnectGattServer()
            }
        }

        override fun onCharacteristicRead(
            gatt: BluetoothGatt?,
            characteristic: BluetoothGattCharacteristic,
            status: Int
        ) {
            super.onCharacteristicRead(gatt, characteristic, status)
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.d(TAG, "Characteristic read successfully")
                readCharacteristic(characteristic)
            } else {
                Log.e(TAG, "Characteristic read unsuccessful, status: $status")
                // Trying to read from the Time Characteristic? It doesnt have the property or permissions
                // set to allow this. Normally this would be an error and you would want to:
                // disconnectGattServer();
            }
        }

        /**
         * Log the value of the characteristic
         * @param characteristic
         */
        private fun readCharacteristic(characteristic: BluetoothGattCharacteristic) {

            val msg = characteristic.getStringValue(0)
            _txtRead += msg
            txtRead.set(_txtRead)
            Log.d(TAG, "read: $msg")
        }


    }

    /**
     * Disconnect Gatt Server
     */
    fun disconnectGattServer() {
        Log.d(TAG, "Closing Gatt connection")
        // disconnect and close the gatt
        if (bleGatt != null) {
            bleGatt!!.disconnect()
            bleGatt!!.close()
        }
    }

연결이 완료되면, onServicesDiscovered에서 찍히는 로그를 확인할 수 있습니다.

또한 여기서 read/write도 볼 수 있습니다. 이어서 데이터를 수신해보겠습니다.

  • Disconnect

   /**
     * Disconnect Gatt Server
     */
    fun disconnectGattServer() {
        Log.d(TAG, "Closing Gatt connection")
        // disconnect and close the gatt
        if (bleGatt != null) {
            bleGatt!!.disconnect()
            bleGatt!!.close()
        }
    }

 

  • BLE Read

제 BLE는 연결에 성공하고 tx characteristic 에 대해 notification enable 하면 데이터를 읽어올 수 있습니다. 

먼저, BluetoothUtils class를 추가해줍니다.

class BluetoothUtils {
    companion object {
        /**
         * Find characteristics of BLE
         * @param gatt gatt instance
         * @return list of found gatt characteristics
         */
        fun findBLECharacteristics(gatt: BluetoothGatt): List<BluetoothGattCharacteristic> {
            val matchingCharacteristics: MutableList<BluetoothGattCharacteristic> = ArrayList()
            val serviceList = gatt.services
            val service = findGattService(serviceList) ?: return matchingCharacteristics
            val characteristicList = service.characteristics
            for (characteristic in characteristicList) {
                if (isMatchingCharacteristic(characteristic)) {
                    matchingCharacteristics.add(characteristic)
                }
            }
            return matchingCharacteristics
        }

        /**
         * Find command characteristic of the peripheral device
         * @param gatt gatt instance
         * @return found characteristic
         */
        fun findCommandCharacteristic(gatt: BluetoothGatt): BluetoothGattCharacteristic? {
            return findCharacteristic(gatt, CHARACTERISTIC_COMMAND_STRING)
        }

        /**
         * Find response characteristic of the peripheral device
         * @param gatt gatt instance
         * @return found characteristic
         */
        fun findResponseCharacteristic(gatt: BluetoothGatt): BluetoothGattCharacteristic? {
            return findCharacteristic(gatt, CHARACTERISTIC_RESPONSE_STRING)
        }

        /**
         * Find the given uuid characteristic
         * @param gatt gatt instance
         * @param uuidString uuid to query as string
         */
        private fun findCharacteristic(
            gatt: BluetoothGatt,
            uuidString: String
        ): BluetoothGattCharacteristic? {
            val serviceList = gatt.services
            val service = findGattService(serviceList) ?: return null
            val characteristicList = service.characteristics
            for (characteristic in characteristicList) {
                if (matchCharacteristic(characteristic, uuidString)) {
                    return characteristic
                }
            }
            return null
        }

        /**
         * Match the given characteristic and a uuid string
         * @param characteristic one of found characteristic provided by the server
         * @param uuidString uuid as string to match
         * @return true if matched
         */
        private fun matchCharacteristic(
            characteristic: BluetoothGattCharacteristic?,
            uuidString: String
        ): Boolean {
            if (characteristic == null) {
                return false
            }
            val uuid: UUID = characteristic.uuid
            return matchUUIDs(uuid.toString(), uuidString)
        }

        /**
         * Find Gatt service that matches with the server's service
         * @param serviceList list of services
         * @return matched service if found
         */
        private fun findGattService(serviceList: List<BluetoothGattService>): BluetoothGattService? {
            for (service in serviceList) {
                val serviceUuidString = service.uuid.toString()
                if (matchServiceUUIDString(serviceUuidString)) {
                    return service
                }
            }
            return null
        }

        /**
         * Try to match the given uuid with the service uuid
         * @param serviceUuidString service UUID as string
         * @return true if service uuid is matched
         */
        private fun matchServiceUUIDString(serviceUuidString: String): Boolean {
            return matchUUIDs(serviceUuidString, SERVICE_STRING)
        }

        /**
         * Check if there is any matching characteristic
         * @param characteristic query characteristic
         */
        private fun isMatchingCharacteristic(characteristic: BluetoothGattCharacteristic?): Boolean {
            if (characteristic == null) {
                return false
            }
            val uuid: UUID = characteristic.uuid
            return matchCharacteristicUUID(uuid.toString())
        }

        /**
         * Query the given uuid as string to the provided characteristics by the server
         * @param characteristicUuidString query uuid as string
         * @return true if the matched is found
         */
        private fun matchCharacteristicUUID(characteristicUuidString: String): Boolean {
            return matchUUIDs(
                characteristicUuidString,
                CHARACTERISTIC_COMMAND_STRING,
                CHARACTERISTIC_RESPONSE_STRING
            )
        }

        /**
         * Try to match a uuid with the given set of uuid
         * @param uuidString uuid to query
         * @param matches a set of uuid
         * @return true if matched
         */
        private fun matchUUIDs(uuidString: String, vararg matches: String): Boolean {
            for (match in matches) {
                if (uuidString.equals(match, ignoreCase = true)) {
                    return true
                }
            }
            return false
        }
    }
}

이를 통해 Characteristic을 받아올 것입니다.

연결이완료되면, onServicesDiscovered로  Callback되는데요, 여기서 바로 read 설정( Tx Characteristic notify) 할것입니다.

        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            super.onServicesDiscovered(gatt, status)
            
            // check if the discovery failed
            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "Device service discovery failed, status: $status")
                return
            }
            // log for successful discovery
            Log.d(TAG, "Services discovery is successful")

            // find command characteristics from the GATT server
            val respCharacteristic = gatt?.let { findResponseCharacteristic(it) }
            // disconnect if the characteristic is not found
            if( respCharacteristic == null ) {
                Log.e(TAG, "Unable to find cmd characteristic")
                disconnectGattServer()
                return
            }
            gatt.setCharacteristicNotification(respCharacteristic, true)
            // UUID for notification
            val descriptor:BluetoothGattDescriptor = respCharacteristic.getDescriptor(
                UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG)
            )
            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            gatt.writeDescriptor(descriptor)
        }

Read이기때문에 ResponseCharacteristic을 받아왔습니다.

그럼 이제 onCharacteristicChanged로 Callback되어 데이터가 들어오는것을 볼 수 있습니다.

       override fun onCharacteristicChanged(
            gatt: BluetoothGatt?,
            characteristic: BluetoothGattCharacteristic
        ) {
            super.onCharacteristicChanged(gatt, characteristic)
           //Log.d(TAG, "characteristic changed: " + characteristic.uuid.toString())
            readCharacteristic(characteristic)
        }
        
        ...
        
        /**
         * Log the value of the characteristic
         * @param characteristic
         */
        private fun readCharacteristic(characteristic: BluetoothGattCharacteristic) {

            val msg = characteristic.getStringValue(0)
            _txtRead += msg
            txtRead.set(_txtRead)
            Log.d(TAG, "read: $msg")
        }

 

  • BLE Write

    fun write(){
        val cmdCharacteristic = BluetoothUtils.findCommandCharacteristic(bleGatt!!)
        // disconnect if the characteristic is not found
        if (cmdCharacteristic == null) {
            Log.e(TAG, "Unable to find cmd characteristic")
            disconnectGattServer()
            return
        }
        val cmdBytes = ByteArray(2)
        cmdBytes[0] = 1
        cmdBytes[1] = 2
        cmdCharacteristic.value = cmdBytes
        val success: Boolean = bleGatt!!.writeCharacteristic(cmdCharacteristic)
        // check the result
        if( !success ) {
             Log.e(TAG, "Failed to write command")
        }
    }

byte 0x12 를 송신 할수 있습니다.

 

  • UUID, Characteristic 찾기

* 만약 UUID를 모르는 특정 bluetooth device에 연결하여 read, wrtie해야할 경우,

read,wrtie에 필요한 CharacteristiconServicesDiscovered에서 찾을 수 있습니다.

service에서 포함하고 있는 characteristic을 검색하여 propertiy가 WRITE or WRITE_NO_RESPONSE 의 경우 write,

NOTIFY일 경우 read할 수 있는 characteristic입니다.

override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
   super.onServicesDiscovered(gatt, status)


    // check if the discovery failed
    if (status != BluetoothGatt.GATT_SUCCESS) {
         statusTxt.postValue(Event("Device service discovery failed, status: $status"))
         return
    }
    
    val device = gatt?.device
    val address = device?.address
    
    var respCharacteristic: BluetoothGattCharacteristic? = null
    var cmdCharacteristic: BluetoothGattCharacteristic? = null
    val services = gatt?.services
    if (services != null) {
         for (service in services) {
            val characteristics = service?.characteristics

            if (characteristics != null) {
                  for ( characteristic in characteristics) {

                     if(characteristic.properties == BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)
                            cmdCharacteristic = characteristic
                     else if(characteristic.properties == BluetoothGattCharacteristic.PROPERTY_NOTIFY)
                            respCharacteristic = characteristic
                     }
                  }
             }
         }
            
            // log for successful discovery
            Log.d(TAG, "Services discovery is successful")

}

 

 

 

728x90
반응형