본문 바로가기
Android

안드로이드 BLE 통신 예제 by Kotlin

by 일용직 코딩노동자 2023. 6. 16.
728x90
반응형

안녕하세요 이번에는 BLE 통신예제를 해보도록 할게요.

 

우선 메니페스트에 권한을 먼저 넣어주겠습니다.

 

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

그리고 권한 체크도 꼭 해주세요.

 

permissions = arrayOf(
    Manifest.permission.BLUETOOTH,
    Manifest.permission.BLUETOOTH_ADMIN,
    Manifest.permission.BLUETOOTH_SCAN,
    Manifest.permission.BLUETOOTH_CONNECT,
    Manifest.permission.BLUETOOTH_ADVERTISE,
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.POST_NOTIFICATIONS
)

퍼미션 리스트입니다.

이중에는 위험권한이 아닌것도 있는데 저는 귀찮아서 그냥 다 때려박았네용 ㅎㅎ

 

그리고 이제 주변 BLE 장비를 스캔합니다.

 

if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this@BlueToothListActivity,"BLE 미지원",Toast.LENGTH_SHORT).show()
    Log.d(TAG,"BLE 미지원")
}

해당 조건문을 통하여 기기가 BLE를 지원하는지 여부를 체크 할 수 있습니다.

 

그다음에 스캔을 진행 하기 전에 앞서 스캔 결과값을 받아올 콜백을 만들어둘게요.

 

val scanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { return }
        val device: BluetoothDevice? = result?.device
        val deviceName: String = device?.name ?: "not device name"
        val deviceAddress: String = device?.address ?: "not address"
        val rssi: Int = result?.rssi ?: 0

        
    }

    override fun onScanFailed(errorCode: Int) {
        // 스캔 실패 처리
    }
}

여기서 각종 디바이스 정보를 얻을 수 있습니다.

 

리스트뷰나 리사이클러뷰에 자유롭게 뿌려보시는 것도 추천합니다.

 

하지만 결정적으로 디바이스에 BLE 통신을 위한 정보는

device 변수입니다.

 

자 이제 스캔을 시작해볼게요.

private val bluetoothManager: BluetoothManager by lazy {
    getSystemService(BluetoothManager::class.java)
}
private val bluetoothAdapter: BluetoothAdapter? by lazy {
    bluetoothManager.adapter
}
lateinit var bluetoothLeScanner : BluetoothLeScanner
bluetoothLeScanner = bluetoothAdapter!!.bluetoothLeScanner
val scanSettings: ScanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .build()
bluetoothLeScanner?.startScan(null, scanSettings, scanCallback)
override fun onDestroy() {
    super.onDestroy()
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { }
    bluetoothLeScanner?.stopScan(scanCallback)
}

해당 구문들을 통하여 스캔을 시작하고 위에서 미리 작성해둔 콜백에서 정보를 받을 수 있습니다.

 

엑티비티가 종료될때 스캔도 같이 종료해줍니다.

 

이제 연결을 시도해보겠습니다.

lateinit var mGatt: BluetoothGatt
fun connect(){
    if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {  }
    mGatt = Global.bluetoothDevice?.connectGatt(this@MainActivity, false, gattCallback)!!
}

fun disConnect(){
    if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {  }
    mGatt?.disconnect()
}

우선 블루투스 관련 유틸 클래스 하나 만들어주겠습니다.

UUID는 해당 모듈에 맞는걸로 재작성 해주셔야 할 수도 있습니다.

class BluetoothUtils {
    companion object {
        val WRITE_UUID = "0000fff2-0000-1000-8000-00805f9b34fb"
        val NOTIFY_UUID = "0000fff1-0000-1000-8000-00805f9b34fb"
        val NA_UUID = "0000fff0-0000-1000-8000-00805f9b34fb"

        /**
         * 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, WRITE_UUID)
        }

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

        /**
         * 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, NA_UUID)
        }

        /**
         * 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,
                WRITE_UUID,
                NOTIFY_UUID
            )
        }

        /**
         * 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
        }
    }
}

 

여기서도 콜백을 만들어줘야해요.

 

var readMsg = ""
val gattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            Log.d(TAG,"연결성공")
            if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {  }
            mGatt?.discoverServices()
        }
        else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            Log.d(TAG,"연결해제")
        }
    }

    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
        when(status){
            BluetoothGatt.GATT_SUCCESS -> {
                Log.d(TAG,"블루투스 셋팅완료")
                if (ActivityCompat.checkSelfPermission(this@MainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {  }

                val respCharacteristic = gatt?.let {
                    findResponseCharacteristic(it)
                }
                if( respCharacteristic == null ) {
                    Log.e(TAG, "블루투스 커맨드를 찾지 못하였습니다.")
                    return
                }
                gatt.setCharacteristicNotification(respCharacteristic, true)
                val descriptor:BluetoothGattDescriptor = respCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
                descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                gatt.writeDescriptor(descriptor)
            }
            else -> {
                Log.e(TAG,"블루투스 셋팅실패")
            }
        }
    }

    override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
        super.onCharacteristicWrite(gatt, characteristic, status)
        when(status){
            BluetoothGatt.GATT_SUCCESS -> {
                Log.d(TAG,"데이터 보내기 성공")
            }
            else -> {
                Log.d(TAG,"데이터 보내기 실패")
            }
        }
    }

    //안드로이드 13이상 호출
    override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) {
        super.onCharacteristicChanged(gatt, characteristic, value)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU){
            Log.d(TAG,"블루투스 수신성공")
            readMsg = String(value)
        }
    }

    //안드로이드 12까지 호출
    override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
        super.onCharacteristicChanged(gatt, characteristic)
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
            Log.d(TAG,"블루투스 수신성공")
            readMsg = characteristic?.getStringValue(0).toString()
        }
    }
}

onServicesDiscovered 함수는 현재 사용 가능한 uuid를 검색하여 등록해주는 역할을 해줍니다.

 

이제 데이터 보내는 예제를 보도록 하겠습니다.

 

val SERVICE = "0000fff0-0000-1000-8000-00805f9b34fb"
val READ_UUID = "0000fff2-0000-1000-8000-00805f9b34fb"
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { }
val service = mGatt.getService(UUID.fromString(SERVICE))
val characteristic = service.getCharacteristic(UUID.fromString(READ_UUID))

val command = "testCommand"
writeByte = command.toByteArray()
characteristic.setValue(command.toByteArray())
mGatt.writeCharacteristic(characteristic)

이렇게 데이터를 전송 할 수 있습니다.

 

이렇게하면 데이터 보내고 받기가 완료됩니다.

 

데이터 전송 시 

onCharacteristicWrite

이녀석이 호출되구요.

 

데이터를 받을때는

 

onCharacteristicChanged

이녀석이 호출됩니다.

 

안드로이드 버전별로 호출되는 녀석이 상이하니 주의하세요 (주석참고)

 

엑티비티가 종료될때

 

override fun onDestroy() {
    super.onDestroy()
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {  }
    mGatt?.disconnect()
}

연결도 종료해줍니다.

 

이상입니다.

 

 

728x90
반응형

댓글