전체적으로 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에 필요한 Characteristic을 onServicesDiscovered에서 찾을 수 있습니다.
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")
}
'Android > 통신' 카테고리의 다른 글
안드로이드 RxBle 사용하기, 예제 (6) | 2021.06.25 |
---|---|
[안드로이드-아두이노] bluetooth classic 자동 페어링&연결 / 데이터 송,수신 (22) | 2020.12.21 |
[안드로이드 java] byte 배열 타입별로 변환하기 ― 수신 프로토콜 처리하기 (0) | 2020.06.05 |
[안드로이드] Wifi List (와이파이 목록) 띄우기 ― Popup Window에서 Recycler View 사용하기 (7) | 2020.06.03 |