핀수로그
  • [Android] BLE 통신 구현하기
    2023년 05월 29일 21시 31분 35초에 업로드 된 글입니다.
    작성자: 핀수
    728x90
    반응형

    BLE

    다들 알고 있겠지만 우리가 아는 블루투스와 BLE는 약간 다르다.

    블루투스 통신 프로토콜이라는 큰 카테고리안에 들어가긴 하지만

    안드로이드에서 통신하기 위한 코드와 로직이 다르다는 의미다.

    BLE는 블루투스 4.0에서부터 채택되었으며, 기존 블루투스 기술보다 전력을 적게 소모한다.

    사용하기

    기존 블루투스와 거의 비슷하다.

    다만 기존 블루투스에서 자주 보던 '페어링' 하는 과정이 빠져있으며,

    유의해야할 점은 BLE 장치를 블루투스 설정창에서 검색하고 연결하려고 하면

    버전 9이하 버전에서는 연결을 거부한다는 메세지를 볼 수 있고,

    그 위로는 그냥 페어링이 되지 않는다.

    1. 권한 선언

    위에서 말했듯 같은 블루투스 프로토콜이기 때문에 권한 선언은 기존 블루투스를 사용하기 위해

    선언해야하는 권한과 같다. 그래도 한번 더 짚고 넘어가자.

    Android 12 이상

    12부터 블루투스 권한이 바뀌었기 때문에 이를 타겟팅 하는 경우 유의깊게 살펴봐야 한다.

    12에서부터 필요한 권한은 다음과 같다.

    • BLUETOOTH_SCAN
      • 해당 앱이 블루투스 (BLE 포함) 주변기기를 찾는 경우
      • 사용자의 위치를 사용하지 않는 경우 플래그를 통해 선언해주어야 한다.
    android:usesPermissionFlags="neverForLocation"
    •  BLUETOOTH_CONNECT
      • 해당 앱이 이미 페어링된 블루투스 기기와 통신하는 경우
    • BLUETOOTH_ADVERTISE
      • 해당 앱에서 현재 기기를 다른 블루투스 기기에서 검색할 수 있게 만드는 경우

    Android 9 이상

    • BLUETOOTH
    • BLUETOOTH_ADMIN
    • ACCESS_FINE_LOCATION
      • Android 9 이하를 타겟팅하는 경우 ACCESS_COARSE_LOCATION 를 선언해도 무방하다.
       /**
         * 권한을 체크합니다.
         */
        @SuppressLint("InlinedApi")
        private fun checkPermission() {
            val requiredPermissionS = arrayOf(BLUETOOTH_SCAN, BLUETOOTH_CONNECT)
            val requiredPermissionQ = arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)
            val rejectPermissionList = ArrayList<String>()
            val requiredPermissionArrays = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                requiredPermissionS
            } else {
                requiredPermissionQ
            }
    
            requestPermissionLauncher.launch(requiredPermissionArrays)
    
            for (permission in requiredPermissionArrays) {
                if (ActivityCompat.checkSelfPermission(
                        mContext,
                        permission
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    isAllowed = false
                    rejectPermissionList.add(permission)
                }
            }
    
            if (rejectPermissionList.isNotEmpty()) {
                val array = arrayOfNulls<String>(rejectPermissionList.size)
                ActivityCompat.requestPermissions(
                    this@BleActivity,
                    rejectPermissionList.toArray(array),
                    REQUEST_CODE
                )
            }
        }
    
        private val requestPermissionLauncher =
            registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
                for (result in results.values) {
                    isAllowed = result
                }
                if (isAllowed) {
                    settingBluetooth()
                } else {
                    showWarning()
                }
            }

    2. 블루투스 활성화 확인

    /**
     * 블루투스 어댑터를 초기화합니다.
     */
    private fun settingBluetooth() {
        if (isAllowed) {
            bluetoothManager = mContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
            bluetoothAdapter = bluetoothManager.adapter
            bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
    
            if (!bluetoothAdapter.isEnabled) {
                // 블루투스가 비활성화 되어 있거나, 사용할 수 없는 경우
                Toast.makeText(mContext, "블루투스가 비활성화 상태입니다", Toast.LENGTH_SHORT).show()
            } else {
                scanLeDevice(true)
            }
        } else {
            showWarning()
        }
    }

    3. 기기 검색

    스캔은 반드시 시간을 지정해두고 진행해야한다.

    BluetoothAdapter에도 BLE 스캔 메소드가 있지만

    BluetoothLeScanner 의 scanStart, scanStop 메소드를 사용하기를 권장하고 있다.

    @SuppressLint("MissingPermission")
    private fun scanLeDevice(enable: Boolean){
        when (enable) {
            true -> {
                handler.postDelayed({
                    isScanning = false
                    bluetoothLeScanner.stopScan(mScanCallback)
                    handler.post {
                        // 스캔중..
                    }
                }, SCAN_PERIOD)
                isScanning = true
                bluetoothLeScanner.startScan(mScanCallback)
            }
            else -> {
                isScanning = false
                bluetoothLeScanner.stopScan(mScanCallback)
            }
        }
    }
    
    /**
     * 스캔 결과를 받아온다.
     */
    private val mScanCallback = object: ScanCallback() {
        @SuppressLint("MissingPermission")
        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            Log.d(TAG, "onScanResult result: $result")
        }
    
        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            super.onBatchScanResults(results)
            Log.d(TAG, "onBatchScanResults: $results")
        }
    
        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            Log.d(TAG, "onScanResult errorCode: $errorCode")
        }
    }

    4. 원하는 장치를 연결하기

    보통 BluetoothAdapter에 기기를 추가하고 원하는 장치를 선택해 연결을 시도하겠지만

    여기서는 원하는 장치가 스캔될 경우 자동으로 연결하도록 했다.

    connectGatt 메소드의 두번째 인자인 autoConnect의 여부는 이용 가능한 즉시 블루투스 기기에 자동 연결할지 나타내는 부울이며 true로 설정할 경우 자동 연결되지만 우선순위가 false일 때보다는 뒤로 밀린다고 어느 블로그에서 본 것 같다..

    // 장치 연결 (GATT 서버에 연결)
    bluetoothGatt = device.connectGatt(this@BleActivity, false, mBluetoothGattCallback)
    
    /**
     * BluetoothGatt callback
     */
    private val mBluetoothGattCallback = object : BluetoothGattCallback() {
        /*
         * GATT 클라이언트가 원격 GATT 서버에 연결/연결 해제된 시기를 나타내는 콜백입니다.
         */
        @SuppressLint("MissingPermission")
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)
            Log.d(TAG, "onConnectionStateChange gatt: $gatt")
            Log.d(TAG, "onConnectionStateChange status: $status")
            Log.d(TAG, "onConnectionStateChange newState: $newState")
        }
    }

    중요하게 봐야할 것은 newState 이다.

    newState가 0이면 Disconncect, 2이면 connect 를 의미한다.


    공부하며 작성된 글이라 잘못된 정보가 있을 수 있습니다.

    말씀해주시면 수정하겠습니다. 감사합니다.

    References

    아래 글을 참고하여 작성 되었습니다.

    Bluetooth permissions  |  Android Developers

    저전력 블루투스 개요  |  Android 개발자  |  Android Developers

    BluetoothGattCallback  |  Android Developers

    728x90
    반응형
    댓글