저전력 블루투스 개요

Android 4.3(API 레벨 18)에서는 저전력 블루투스(BLE)에 대한 플랫폼 내 지원을 핵심적 역할로 도입하고 앱이 기기를 검색하고, 서비스를 쿼리하고, 정보를 전송하는 데 사용할 수 있는 API를 제공합니다.

일반적인 사용 사례의 예시는 다음과 같습니다.

  • 근처 기기 사이에서 소량의 데이터를 전송합니다.
  • Google 비콘과 같은 근접 센서와 상호작용하여 사용자에게 현재 위치에 기반한 맞춤 설정된 환경을 제공합니다.

기존 블루투스와 달리 저전력 블루투스(BLE)는 상당히 전력을 적게 소모하도록 설계되었습니다. 따라서 Android 앱은 전력 요구 사항이 엄격한 저전력 블루투스 기기와 통신할 수 있게 됩니다. 예를 들어, 근접 센서, 심장 박동 모니터, 피트니스 기기 등이 있습니다.

주의: 사용자가 저전력 블루투스를 사용하여 자신의 기기를 다른 기기와 페어링하면 두 기기 간에 교환된 데이터가 사용자 기기에 있는 모든 앱에 액세스할 수 있습니다.

그러므로 민감한 데이터를 수집하는 앱일 경우, 앱 레이어 보안을 구현하여 해당 데이터의 개인 정보를 보호해야 합니다.

주요 용어 및 개념

저전력 블루투스에 대한 주요 용어와 개념은 아래와 같이 요약할 수 있습니다.

  • 포괄적 특성 프로필(GATT)—GATT 프로필은 저전력 블루투스 링크를 통해 "속성"이라고 알려진 짧은 데이터를 주고받기 위한 일반적인 사양입니다. 현재 모든 저전력 애플리케이션 프로필은 GATT에 기초합니다.
    • 블루투스 SIG는 저전력 기기를 위해 여러 가지 프로필을 정의합니다. 프로필은 기기가 특정 애플리케이션에서 작동하는 방식을 지정한 사양입니다. 참고로 기기는 두 개 이상의 프로필을 구현할 수 있습니다. 예를 들어 기기에 심장 박동 모니터와 배터리 수준 탐지기가 포함될 수 있습니다.
  • 속성 프로토콜(ATT)—GATT는 속성 프로토콜(ATT) 위에 구축됩니다. 이를 일컬어 GATT/ATT라고도 합니다. ATT는 저전력 블루투스 기기에서 실행되도록 최적화됩니다. 이를 위해서 최대한 적은 양의 데이터를 사용합니다. 각 속성은 UUID(Universally Unique Identifier)로 고유하게 식별되는데, UUID는 고유 식별 정보에 사용하는 문자열 ID의 표준화된 128비트 형식을 나타냅니다. ATT가 전송하는 속성특성서비스로 구성됩니다.
  • 특성—특성에는 하나의 값과 특성의 값을 설명하는 0-n 설명자가 포함됩니다. 특성은 일종의 유형으로, 클래스와 유사하다고 생각하면 됩니다. 
  • 설명자—설명자는 특성 값을 설명하도록 정의된 속성입니다. 예를 들어 설명자는 인간이 읽을 수 있는 설명, 특성 값의 허용 가능한 범위 또는 특성 값에 적용되는 측정 단위를 지정할 수 있습니다.
  • 서비스—서비스는 특성의 모음입니다. 예를 들어 "심장 박동 모니터"라는 서비스에는 "심장 박동 측정값"과 같은 특성이 포함됩니다. 기존의 GATT 기반 프로필과 서비스 목록은 bluetooth.org를 참조하세요.

역할과 책임

Android 기기가 저전력 블루투스 기기와 상호작용할 때 적용되는 역할과 책임은 다음과 같습니다.

  • 중앙 vs. 주변. 이는 저전력 블루투스 자체에 적용됩니다. 중앙 역할을 맡은 기기는 스캔하며 광고를 찾고, 주변 역할을 맡은 기기는 광고를 게재합니다.
  • GATT 서버 vs. GATT 클라이언트. 두 기기 사이에 연결이 설정되었을 때 서로 통신하는 방법을 결정합니다.

어떤 차이가 있는지 이해하려면 Android 전화기와 저전력 블루투스 기기인 Activity 트래커가 있다고 생각하면 됩니다. 전화기는 중앙 역할을 지원하고 Activity 트래커는 주변 역할을 지원합니다(저전력 블루투스 연결을 설정하려면 중앙 역할과 주변 역할 기기가 각각 필요합니다. 주변 역할만 지원하는 기기나 중앙 역할만 지원하는 기기가 두 개 있으면 서로 통신이 불가능합니다).

전화기와 Activity 트래커 사이에 연결이 설정되면 서로 GATT 메타데이터를 전송하기 시작합니다. 전송하는 데이터 유형에 따라 둘 중 하나가 서버 역할을 할 수도 있습니다. 예를 들어 Activity 트래커가 전화기에 센서 데이터를 보고하고자 할 경우, Activity 트래커가 서버 역할을 하는 것이 적절할 수 있습니다. Activity 트래커가 전화기에서 업데이트를 받고자 할 경우에는 전화기가 서버 역할을 하는 것이 적절할 수 있습니다.

이 문서의 예시에서는 Android 앱(Android 기기에서 실행)이 GATT 클라이언트에 해당합니다. 앱이 GATT 서버에서 데이터를 가져오는데, 여기에서 GATT 서버란 심장 박동 프로필을 지원하는 저전력 블루투스 심장 박동 모니터입니다. 그러나 Android 앱이 GATT 서버 역할을 하도록 설계할 수도 있습니다. 자세한 내용은 BluetoothGattServer를 참조하세요.

저전력 블루투스 권한

애플리케이션에서 블루투스 기능을 사용하려면 블루투스 권한인 BLUETOOTH를 선언해야 합니다. 이 권한은 연결 요청, 연결 수락 및 데이터 전송과 같은 블루투스 통신을 수행하는 데 필요합니다.

저전력 비콘은 위치와 연결되는 경우가 많기 때문에 ACCESS_FINE_LOCATION 권한도 선언해야 합니다. 이 권한이 없으면 스캔 시 아무런 결과가 반환되지 않습니다.

참고: 앱이 Android 9(API 레벨 28) 이하를 대상으로 하는 경우 ACCESS_FINE_LOCATION 권한 대신 ACCESS_COARSE_LOCATION 권한을 선언할 수 있습니다.

앱이 기기 검색을 시작하거나 블루투스 설정을 조작하려면 BLUETOOTH_ADMIN 권한도 선언해야 합니다. 참고: BLUETOOTH_ADMIN 권한을 사용하는 경우 BLUETOOTH 권한도 있어야 합니다.

애플리케이션 매니페스트 파일에 이 권한들을 선언합니다. 예를 들면 다음과 같습니다.

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

<!-- If your app targets Android 9 or lower, you can declare
     ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

앱이 저전력 블루투스 지원 기기에만 제공된다고 선언하고 싶다면 앱 매니페스트에 다음 항목을 포함하세요.

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

그러나 저전력 블루투스를 지원하지 않는 기기에도 앱을 제공하고 싶다면 앱 매니페스트에 이 요소를 포함하되, required="false"로 설정해야 합니다. 그런 다음, 런타임에서 PackageManager.hasSystemFeature()를 사용하여 저전력 블루투스의 가용성을 지정할 수 있습니다.

Kotlin

private fun PackageManager.missingSystemFeature(name: String): Boolean = !hasSystemFeature(name)
...

packageManager.takeIf { it.missingSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) }?.also {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show()
    finish()
}

Java

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish();
}

저전력 블루투스 설정

애플리케이션이 저전력 블루투스를 사용하여 통신하려면 저전력 블루투스가 기기에서 지원되는지 확인하고, 지원되는 경우 활성화해야 합니다. 단, <uses-feature.../>가 false로 설정된 경우에만 이 검사가 필요합니다.

저전력 블루투스가 지원되지 않는다면 모든 저전력 블루투스 기능을 적절히 비활성화해야 합니다. 저전력 블루투스가 지원되지만 비활성화된 경우, 개발자는 사용자가 애플리케이션을 떠나지 않은 상태에서 블루투스를 활성화하도록 요청할 수 있습니다. 이 설정은 BluetoothAdapter를 사용하여 2단계로 수행됩니다.

  1. BluetoothAdapter 가져오기

    모든 블루투스 Activity에는 BluetoothAdapter가 필요합니다. BluetoothAdapter는 기기 자체의 블루투스 어댑터(블루투스 송수신 장치)를 나타냅니다. 전체 시스템에 대한 단일 블루투스 어댑터가 있고 애플리케이션이 해당 객체를 사용하여 상호작용할 수 있습니다. 아래의 스니펫은 어댑터를 가져오는 방법을 보여줍니다. 이 방법은 getSystemService()를 사용하여 BluetoothManager의 인스턴스를 반환한 다음, 어댑터를 가져옵니다. Android 4.3(API 레벨 18)에는 BluetoothManager가 도입됩니다.

    Kotlin

    private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothManager.adapter
    }
    

    Java

    private BluetoothAdapter bluetoothAdapter;
    ...
    // Initializes Bluetooth adapter.
    final BluetoothManager bluetoothManager =
            (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothAdapter = bluetoothManager.getAdapter();
    
  2. 블루투스 활성화

    이제 블루투스를 활성화해야 합니다. isEnabled()를 호출하여 현재 블루투스가 활성화되었는지 확인합니다. 이 메서드가 false를 반환하는 경우 블루투스가 비활성화된 것입니다. 다음 스니펫은 블루투스가 활성화되었는지 검사합니다. 블루투스가 활성화되어 있지 않다면 오류를 표시하고 사용자에게 설정으로 가서 블루투스를 활성화하라는 메시지를 보여줍니다.

    Kotlin

    private val BluetoothAdapter.isDisabled: Boolean
        get() = !isEnabled
    ...
    
    // Ensures Bluetooth is available on the device and it is enabled. If not,
    // displays a dialog requesting user permission to enable Bluetooth.
    bluetoothAdapter?.takeIf { it.isDisabled }?.apply {
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
    }
    

    Java

    // Ensures Bluetooth is available on the device and it is enabled. If not,
    // displays a dialog requesting user permission to enable Bluetooth.
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    

    참고: startActivityForResult(android.content.Intent, int)에 전달된 REQUEST_ENABLE_BT 상수는 지역적으로 정의된 정수(0보다 커야 함)이며, 시스템이 onActivityResult(int, int, android.content.Intent) 구현에서 개발자에게 requestCode 매개변수로 다시 전달합니다.

저전력 블루투스 기기 찾기

저전력 블루투스 기기를 찾으려면 startLeScan() 메서드를 사용합니다. 이 메서드는 BluetoothAdapter.LeScanCallback을 매개변수로 받습니다. 이 콜백을 통해 스캔 결과가 반환되므로 반드시 구현해야 합니다. 스캔 작업은 배터리를 많이 소모하므로 다음의 가이드라인을 준수해야 합니다.

  • 원하는 기기를 찾는 즉시 스캔을 중단합니다.
  • 절대로 반복해서 스캔해서는 안 되고, 스캔에 시간 제한을 설정해야 합니다. 이전에 사용 가능했던 기기가 범위 밖으로 이동했을 수도 있고 이를 계속 스캔하면 배터리가 소모됩니다.

다음 스니펫은 스캔을 시작하고 중지하는 방법을 보여줍니다.

Kotlin

private const val SCAN_PERIOD: Long = 10000

/**
 * Activity for scanning and displaying available BLE devices.
 */
class DeviceScanActivity(
        private val bluetoothAdapter: BluetoothAdapter,
        private val handler: Handler
) : ListActivity() {

    private var mScanning: Boolean = false

    private fun scanLeDevice(enable: Boolean) {
        when (enable) {
            true -> {
                // Stops scanning after a pre-defined scan period.
                handler.postDelayed({
                    mScanning = false
                    bluetoothAdapter.stopLeScan(leScanCallback)
                }, SCAN_PERIOD)
                mScanning = true
                bluetoothAdapter.startLeScan(leScanCallback)
            }
            else -> {
                mScanning = false
                bluetoothAdapter.stopLeScan(leScanCallback)
            }
        }
    }
}

Java

/**
 * Activity for scanning and displaying available BLE devices.
 */
public class DeviceScanActivity extends ListActivity {

    private BluetoothAdapter bluetoothAdapter;
    private boolean mScanning;
    private Handler handler;

    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ...
    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    bluetoothAdapter.stopLeScan(leScanCallback);
                }
            }, SCAN_PERIOD);

            mScanning = true;
            bluetoothAdapter.startLeScan(leScanCallback);
        } else {
            mScanning = false;
            bluetoothAdapter.stopLeScan(leScanCallback);
        }
        ...
    }
...
}

특정 유형의 주변 장치만 스캔하고 싶다면 startLeScan(UUID[], BluetoothAdapter.LeScanCallback)를 호출하여 앱이 지원하는 GATT 서비스를 지정하는 UUID 객체의 배열을 제공해야 합니다.

다음은 BluetoothAdapter.LeScanCallback의 구현인데, 저전력 블루투스 스캔 결과를 제공하는 데 사용하는 인터페이스입니다.

Kotlin

val leDeviceListAdapter: LeDeviceListAdapter = ...

private val leScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
    runOnUiThread {
        leDeviceListAdapter.addDevice(device)
        leDeviceListAdapter.notifyDataSetChanged()
    }
}

Java

private LeDeviceListAdapter leDeviceListAdapter;
...
// Device scan callback.
private BluetoothAdapter.LeScanCallback leScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        runOnUiThread(new Runnable() {
           @Override
           public void run() {
               leDeviceListAdapter.addDevice(device);
               leDeviceListAdapter.notifyDataSetChanged();
           }
       });
   }
};

참고: 저전력 블루투스 기기만 스캔하거나, 또는 기존 블루투스 기기만 스캔할 수 있습니다. 이 내용은 블루투스에 설명되어 있습니다. 저전력 블루투스 기기와 기존 블루투스 기기를 동시에 스캔할 수 없습니다.

GATT 서버에 연결

저전력 블루투스 기기와 상호작용하는 첫 번째 단계는 해당 기기에 연결하는 것입니다. 좀 더 구체적으로 말하면, 기기의 GATT 서버에 연결해야 합니다. 블루투스 기기의 GATT 서버에 연결할 때는 connectGatt() 메서드를 사용합니다. 이 메서드는 세 가지 매개변수를 받습니다(Context 객체, autoConnect(이용 가능한 즉시 블루투스 기기에 자동 연결할지 나타내는 부울), BluetoothGattCallback에 대한 참조).

Kotlin

var bluetoothGatt: BluetoothGatt? = null
...

bluetoothGatt = device.connectGatt(this, false, gattCallback)

Java

bluetoothGatt = device.connectGatt(this, false, gattCallback);

이렇게 하면 블루투스 기기에서 호스팅되는 GATT 서버에 연결되고 BluetoothGatt 인스턴스가 반환됩니다. 그런 다음, 이 인스턴스를 사용하여 GATT 클라이언트 작업을 수행할 수 있습니다. 호출자(Android 앱)가 GATT 클라이언트가 됩니다. BluetoothGattCallback은 클라이언트에 결과(예: 연결 상태)와 추가적인 GATT 클라이언트 작업을 전달하는 데 사용합니다.

이 예시에서 블루투스 앱이 데이터를 연결, 표시하고 기기에서 지원하는 GATT 서비스와 특성을 표시하기 위한 Activity(DeviceControlActivity)를 제공합니다. 이 Activity는 사용자 입력에 기초하여 BluetoothLeService라는 Service와 통신하고, 이는 Android BLE API를 통해 블루투스 기기와 상호작용합니다.

Kotlin

private val TAG = BluetoothLeService::class.java.simpleName
private const val STATE_DISCONNECTED = 0
private const val STATE_CONNECTING = 1
private const val STATE_CONNECTED = 2
const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"
const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"
const val ACTION_GATT_SERVICES_DISCOVERED =
        "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED"
const val ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE"
const val EXTRA_DATA = "com.example.bluetooth.le.EXTRA_DATA"
val UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT)

// A service that interacts with the BLE device via the Android BLE API.
class BluetoothLeService(private var bluetoothGatt: BluetoothGatt?) : Service() {

    private var connectionState = STATE_DISCONNECTED

    // Various callback methods defined by the BLE API.
    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(
                gatt: BluetoothGatt,
                status: Int,
                newState: Int
        ) {
            val intentAction: String
            when (newState) {
                BluetoothProfile.STATE_CONNECTED -> {
                    intentAction = ACTION_GATT_CONNECTED
                    connectionState = STATE_CONNECTED
                    broadcastUpdate(intentAction)
                    Log.i(TAG, "Connected to GATT server.")
                    Log.i(TAG, "Attempting to start service discovery: " +
                            bluetoothGatt?.discoverServices())
                }
                BluetoothProfile.STATE_DISCONNECTED -> {
                    intentAction = ACTION_GATT_DISCONNECTED
                    connectionState = STATE_DISCONNECTED
                    Log.i(TAG, "Disconnected from GATT server.")
                    broadcastUpdate(intentAction)
                }
            }
        }

        // New services discovered
        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            when (status) {
                BluetoothGatt.GATT_SUCCESS -> broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
                else -> Log.w(TAG, "onServicesDiscovered received: $status")
            }
        }

        // Result of a characteristic read operation
        override fun onCharacteristicRead(
                gatt: BluetoothGatt,
                characteristic: BluetoothGattCharacteristic,
                status: Int
        ) {
            when (status) {
                    BluetoothGatt.GATT_SUCCESS -> {
                        broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
                    }
            }
        }
    }
}

Java

// A service that interacts with the BLE device via the Android BLE API.
public class BluetoothLeService extends Service {
    private final static String TAG = BluetoothLeService.class.getSimpleName();

    private BluetoothManager bluetoothManager;
    private BluetoothAdapter bluetoothAdapter;
    private String bluetoothDeviceAddress;
    private BluetoothGatt bluetoothGatt;
    private int connectionState = STATE_DISCONNECTED;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

    // Various callback methods defined by the BLE API.
    private final BluetoothGattCallback gattCallback =
            new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                connectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                Log.i(TAG, "Attempting to start service discovery:" +
                        bluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                connectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }

        @Override
        // New services discovered
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        // Result of a characteristic read operation
        public void onCharacteristicRead(BluetoothGatt gatt,
                BluetoothGattCharacteristic characteristic,
                int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }
     ...
    };
...
}

특정 콜백이 트리거되면 적절한 broadcastUpdate() 도우미 메서드를 호출하고 작업에 전달합니다. 이 섹션에서 데이터 파싱은 블루투스 심장 박동 측정 프로필 사양에 따라 실행됩니다.

Kotlin

private fun broadcastUpdate(action: String) {
    val intent = Intent(action)
    sendBroadcast(intent)
}

private fun broadcastUpdate(action: String, characteristic: BluetoothGattCharacteristic) {
    val intent = Intent(action)

    // This is special handling for the Heart Rate Measurement profile. Data
    // parsing is carried out as per profile specifications.
    when (characteristic.uuid) {
        UUID_HEART_RATE_MEASUREMENT -> {
            val flag = characteristic.properties
            val format = when (flag and 0x01) {
                0x01 -> {
                    Log.d(TAG, "Heart rate format UINT16.")
                    BluetoothGattCharacteristic.FORMAT_UINT16
                }
                else -> {
                    Log.d(TAG, "Heart rate format UINT8.")
                    BluetoothGattCharacteristic.FORMAT_UINT8
                }
            }
            val heartRate = characteristic.getIntValue(format, 1)
            Log.d(TAG, String.format("Received heart rate: %d", heartRate))
            intent.putExtra(EXTRA_DATA, (heartRate).toString())
        }
        else -> {
            // For all other profiles, writes the data formatted in HEX.
            val data: ByteArray? = characteristic.value
            if (data?.isNotEmpty() == true) {
                val hexString: String = data.joinToString(separator = " ") {
                    String.format("%02X", it)
                }
                intent.putExtra(EXTRA_DATA, "$data\n$hexString")
            }
        }

    }
    sendBroadcast(intent)
}

Java

private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
                             final BluetoothGattCharacteristic characteristic) {
    final Intent intent = new Intent(action);

    // This is special handling for the Heart Rate Measurement profile. Data
    // parsing is carried out as per profile specifications.
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
        int flag = characteristic.getProperties();
        int format = -1;
        if ((flag & 0x01) != 0) {
            format = BluetoothGattCharacteristic.FORMAT_UINT16;
            Log.d(TAG, "Heart rate format UINT16.");
        } else {
            format = BluetoothGattCharacteristic.FORMAT_UINT8;
            Log.d(TAG, "Heart rate format UINT8.");
        }
        final int heartRate = characteristic.getIntValue(format, 1);
        Log.d(TAG, String.format("Received heart rate: %d", heartRate));
        intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
    } else {
        // For all other profiles, writes the data formatted in HEX.
        final byte[] data = characteristic.getValue();
        if (data != null && data.length > 0) {
            final StringBuilder stringBuilder = new StringBuilder(data.length);
            for(byte byteChar : data)
                stringBuilder.append(String.format("%02X ", byteChar));
            intent.putExtra(EXTRA_DATA, new String(data) + "\n" +
                    stringBuilder.toString());
        }
    }
    sendBroadcast(intent);
}

DeviceControlActivity에서 이런 이벤트는 BroadcastReceiver가 처리합니다.

Kotlin

// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a
// result of read or notification operations.
private val gattUpdateReceiver = object : BroadcastReceiver() {

    private lateinit var bluetoothLeService: BluetoothLeService

    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action
        when (action){
            ACTION_GATT_CONNECTED -> {
                connected = true
                updateConnectionState(R.string.connected)
                (context as? Activity)?.invalidateOptionsMenu()
            }
            ACTION_GATT_DISCONNECTED -> {
                connected = false
                updateConnectionState(R.string.disconnected)
                (context as? Activity)?.invalidateOptionsMenu()
                clearUI()
            }
            ACTION_GATT_SERVICES_DISCOVERED -> {
                // Show all the supported services and characteristics on the
                // user interface.
                displayGattServices(bluetoothLeService.getSupportedGattServices())
            }
            ACTION_DATA_AVAILABLE -> {
                displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA))
            }
        }
    }
}

Java

// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a
// result of read or notification operations.
private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
            connected = true;
            updateConnectionState(R.string.connected);
            invalidateOptionsMenu();
        } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
            connected = false;
            updateConnectionState(R.string.disconnected);
            invalidateOptionsMenu();
            clearUI();
        } else if (BluetoothLeService.
                ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
            // Show all the supported services and characteristics on the
            // user interface.
            displayGattServices(bluetoothLeService.getSupportedGattServices());
        } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
            displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
        }
    }
};

블루투스 속성 읽기

Android 앱이 GATT 서버에 연결되어 서비스를 발견하고 나면 속성을 읽고 쓸 수 있습니다(지원되는 경우). 예를 들어 이 스니펫은 서버 서비스와 특성에 대한 확인 절차를 반복하고 이들을 UI에 표시합니다.

Kotlin

class DeviceControlActivity : Activity() {

    // Demonstrates how to iterate through the supported GATT
    // Services/Characteristics.
    // In this sample, we populate the data structure that is bound to the
    // ExpandableListView on the UI.
    private fun displayGattServices(gattServices: List<BluetoothGattService>?) {
        if (gattServices == null) return
        var uuid: String?
        val unknownServiceString: String = resources.getString(R.string.unknown_service)
        val unknownCharaString: String = resources.getString(R.string.unknown_characteristic)
        val gattServiceData: MutableList<HashMap<String, String>> = mutableListOf()
        val gattCharacteristicData: MutableList<ArrayList<HashMap<String, String>>> =
                mutableListOf()
        mGattCharacteristics = mutableListOf()

        // Loops through available GATT Services.
        gattServices.forEach { gattService ->
            val currentServiceData = HashMap<String, String>()
            uuid = gattService.uuid.toString()
            currentServiceData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownServiceString)
            currentServiceData[LIST_UUID] = uuid
            gattServiceData += currentServiceData

            val gattCharacteristicGroupData: ArrayList<HashMap<String, String>> = arrayListOf()
            val gattCharacteristics = gattService.characteristics
            val charas: MutableList<BluetoothGattCharacteristic> = mutableListOf()

            // Loops through available Characteristics.
            gattCharacteristics.forEach { gattCharacteristic ->
                charas += gattCharacteristic
                val currentCharaData: HashMap<String, String> = hashMapOf()
                uuid = gattCharacteristic.uuid.toString()
                currentCharaData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownCharaString)
                currentCharaData[LIST_UUID] = uuid
                gattCharacteristicGroupData += currentCharaData
            }
            mGattCharacteristics += charas
            gattCharacteristicData += gattCharacteristicGroupData
        }
    }
}

Java


public class DeviceControlActivity extends Activity {
    ...
    // Demonstrates how to iterate through the supported GATT
    // Services/Characteristics.
    // In this sample, we populate the data structure that is bound to the
    // ExpandableListView on the UI.
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null) return;
        String uuid = null;
        String unknownServiceString = getResources().
                getString(R.string.unknown_service);
        String unknownCharaString = getResources().
                getString(R.string.unknown_characteristic);
        ArrayList<HashMap<String, String>> gattServiceData =
                new ArrayList<HashMap<String, String>>();
        ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                = new ArrayList<ArrayList<HashMap<String, String>>>();
        mGattCharacteristics =
                new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

        // Loops through available GATT Services.
        for (BluetoothGattService gattService : gattServices) {
            HashMap<String, String> currentServiceData =
                    new HashMap<String, String>();
            uuid = gattService.getUuid().toString();
            currentServiceData.put(
                    LIST_NAME, SampleGattAttributes.
                            lookup(uuid, unknownServiceString));
            currentServiceData.put(LIST_UUID, uuid);
            gattServiceData.add(currentServiceData);

            ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                    new ArrayList<HashMap<String, String>>();
            List<BluetoothGattCharacteristic> gattCharacteristics =
                    gattService.getCharacteristics();
            ArrayList<BluetoothGattCharacteristic> charas =
                    new ArrayList<BluetoothGattCharacteristic>();
           // Loops through available Characteristics.
            for (BluetoothGattCharacteristic gattCharacteristic :
                    gattCharacteristics) {
                charas.add(gattCharacteristic);
                HashMap<String, String> currentCharaData =
                        new HashMap<String, String>();
                uuid = gattCharacteristic.getUuid().toString();
                currentCharaData.put(
                        LIST_NAME, SampleGattAttributes.lookup(uuid,
                                unknownCharaString));
                currentCharaData.put(LIST_UUID, uuid);
                gattCharacteristicGroupData.add(currentCharaData);
            }
            mGattCharacteristics.add(charas);
            gattCharacteristicData.add(gattCharacteristicGroupData);
         }
    ...
    }
...
}

GATT 알림 수신

일반적으로 블루투스 앱은 기기에서 특정 특성이 변경되면 알림을 받도록 되어 있습니다. 이 스니펫은 setCharacteristicNotification() 메서드를 사용하여 어떤 특성에 대한 알림을 설정하는 방법을 보여줍니다.

Kotlin

lateinit var bluetoothGatt: BluetoothGatt
lateinit var characteristic: BluetoothGattCharacteristic
var enabled: Boolean = true
...
bluetoothGatt.setCharacteristicNotification(characteristic, enabled)
val uuid: UUID = UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)
val descriptor = characteristic.getDescriptor(uuid).apply {
    value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
}
bluetoothGatt.writeDescriptor(descriptor)

Java

private BluetoothGatt bluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);

특성에 대한 알림이 활성화되었을 때 원격 기기에서 특성이 변경되면 onCharacteristicChanged() 콜백이 트리거됩니다.

Kotlin

// Characteristic notification
override fun onCharacteristicChanged(
        gatt: BluetoothGatt,
        characteristic: BluetoothGattCharacteristic
) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
}

Java

@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}

클라이언트 앱 닫기

앱에서 블루투스 기기의 사용이 끝나면 close()를 호출해 시스템이 적절하게 리소스를 해제하도록 해야 합니다.

Kotlin

fun close() {
    bluetoothGatt?.close()
    bluetoothGatt = null
}

Java

public void close() {
    if (bluetoothGatt == null) {
        return;
    }
    bluetoothGatt.close();
    bluetoothGatt = null;
}