기본 콘텐츠로 건너뛰기

Android에서 BLE 통신을 사용하는 방법



Android에서 BLE 통신을 사용하는 방법

BLE(Bluetooth Low Energy)는 이름 그대로 전력 사용량을 낮춘 Bluetooth 통신 방식입니다. 센서, 비콘, 건강 측정기, 소형 IoT 기기처럼 작은 데이터를 주기적으로 주고받는 환경에 특히 잘 맞습니다. Classic Bluetooth와 달리 소켓으로 자유롭게 데이터를 주고받기보다, 정해진 GATT 구조 안에서 데이터를 읽고 쓰는 방식으로 동작합니다.

목차

  1. BLE를 사용하기 전 알아둘 점
  2. BLE의 핵심 구조: Central, Peripheral, GATT
  3. Service와 Characteristic 이해하기
  4. Android 버전별 BLE 권한
  5. BLE 통신 흐름
  6. 스캔 및 연결 샘플 코드

1. BLE를 사용하기 전 알아둘 점

BLE는 배터리 효율이 좋지만, 많은 양의 데이터를 빠르게 전송하는 용도에는 적합하지 않을 수 있습니다. 연결하려는 기기가 제공하는 Service UUID와 Characteristic UUID, 데이터 형식은 제조사 문서나 기기 사양으로 반드시 확인해야 합니다.

  • BLE는 작은 데이터를 주기적으로 전송하는 데 적합합니다.
  • 스캔은 배터리 사용량이 크므로 시간 제한을 두고, 기기를 찾으면 바로 멈춥니다.
  • 기기별 UUID와 데이터 형식은 다르므로 공통값으로 가정하면 안 됩니다.
  • 연결, 서비스 탐색, 데이터 수신은 모두 비동기 콜백으로 처리됩니다.
  • 연결이 끝나면 disconnect()와 close()로 리소스를 해제합니다.

2. BLE의 핵심 구조: Central, Peripheral, GATT

BLE에서는 스마트폰이 보통 Central 역할을 맡습니다. 주변 BLE 기기를 스캔하고 연결을 요청하는 쪽입니다. 센서나 측정 장치는 Peripheral 역할을 하며, 자신의 데이터와 기능을 외부에 제공합니다.

이때 데이터의 구조와 접근 방식은 GATT(Generic Attribute Profile)가 정의합니다. 스마트폰은 GATT Client, BLE 기기는 GATT Server가 되어 서로 통신합니다.

3. Service와 Characteristic 이해하기

BLE 기기의 기능은 Service로 묶이고, 실제 데이터는 그 안의 Characteristic에 들어 있습니다. 쉽게 말하면 Service는 기능별 폴더, Characteristic은 실제 값을 담은 파일이라고 생각하면 됩니다.

구성 요소 역할 예시
Service 관련 기능을 하나로 묶는 단위 배터리 정보 서비스, 심박수 서비스
Characteristic 실제 데이터를 읽거나 쓰는 단위 현재 배터리 잔량, 현재 심박수
Descriptor Characteristic의 동작 방식을 보조 변경 알림(Notification) 활성화

Characteristic은 읽기(Read), 쓰기(Write), 알림(Notification) 같은 속성을 가질 수 있습니다. 센서 값처럼 계속 바뀌는 데이터는 Notification을 활성화해 두면, 값이 바뀔 때마다 기기가 스마트폰에 알려주는 방식으로 받을 수 있습니다.

4. Android 버전별 BLE 권한

BLE 권한은 Android 12(API 31)부터 주변 기기 권한으로 분리되었습니다. Android 12 이상에서는 BLUETOOTH_SCAN과 BLUETOOTH_CONNECT 권한을 실행 중에 요청해야 합니다.

<!-- Android 11(API 30) 이하 -->
<uses-permission
    android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />

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

<!-- Android 11 이하에서 BLE 스캔 시 필요할 수 있음 -->
<uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION"
    android:maxSdkVersion="30" />

<!-- Android 12(API 31) 이상 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- BLE가 필수 기능이 아니라면 required를 false로 설정 -->
<uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="false" />

5. BLE 통신 흐름

  1. BLE 지원 여부와 Bluetooth 활성화 상태를 확인합니다.
  2. 권한을 확인한 뒤 주변 BLE 기기를 스캔합니다.
  3. 원하는 기기를 찾으면 즉시 스캔을 중지합니다.
  4. connectGatt()로 기기의 GATT Server에 연결합니다.
  5. 연결되면 Service를 탐색합니다.
  6. 필요한 Characteristic을 찾아 읽기·쓰기·알림 수신을 처리합니다.
  7. 통신이 끝나면 GATT 연결을 해제합니다.

6. 스캔 및 연결 샘플 코드

아래 코드는 BLE 기기를 스캔하고, 원하는 이름의 기기를 찾으면 GATT 연결 후 Service를 탐색하는 간단한 예제입니다. 실제 구현에서는 기기 이름보다 Service UUID나 제조사 데이터로 기기를 구분하는 편이 더 안정적일 수 있습니다.

public class BleHelper {

    private static final String TARGET_DEVICE_NAME = "SampleSensor";

    private BluetoothLeScanner bleScanner;
    private BluetoothGatt bluetoothGatt;
    private boolean isScanning = false;

    private final Handler handler = new Handler(Looper.getMainLooper());

    public BleHelper(Context context) {
        BluetoothManager manager =
                (BluetoothManager) context.getSystemService(
                        Context.BLUETOOTH_SERVICE
                );

        BluetoothAdapter adapter = manager.getAdapter();

        if (adapter != null) {
            bleScanner = adapter.getBluetoothLeScanner();
        }
    }

    public void startScan() {
        if (bleScanner == null || isScanning) {
            return;
        }

        isScanning = true;
        bleScanner.startScan(scanCallback);

        // 스캔은 배터리를 사용하므로 10초 후 자동 종료
        handler.postDelayed(this::stopScan, 10_000L);
    }

    public void stopScan() {
        if (bleScanner != null && isScanning) {
            isScanning = false;
            bleScanner.stopScan(scanCallback);
        }
    }

    private final ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();

            if (TARGET_DEVICE_NAME.equals(device.getName())) {
                stopScan();

                // false: 지금 바로 연결 시도
                bluetoothGatt = device.connectGatt(
                        null,
                        false,
                        gattCallback
                );
            }
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.e("BLE", "스캔 실패: " + errorCode);
        }
    };

    private final BluetoothGattCallback gattCallback =
            new BluetoothGattCallback() {

        @Override
        public void onConnectionStateChange(
                BluetoothGatt gatt,
                int status,
                int newState
        ) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.d("BLE", "GATT 연결 성공");

                // 연결 후 기기가 제공하는 Service 목록을 조회
                gatt.discoverServices();

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.d("BLE", "GATT 연결 해제");
                gatt.close();
            }
        }

        @Override
        public void onServicesDiscovered(
                BluetoothGatt gatt,
                int status
        ) {
            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.e("BLE", "Service 탐색 실패");
                return;
            }

            for (BluetoothGattService service : gatt.getServices()) {
                Log.d("BLE", "Service UUID: " + service.getUuid());

                for (BluetoothGattCharacteristic characteristic
                        : service.getCharacteristics()) {
                    Log.d(
                            "BLE",
                            "Characteristic UUID: "
                                    + characteristic.getUuid()
                    );
                }
            }
        }
    };

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

이 예제는 BLE 통신의 시작 단계인 스캔, 연결, Service 탐색까지 보여줍니다. 다음 단계에서는 기기 사양에 맞는 Service UUID와 Characteristic UUID를 찾아 readCharacteristic(), writeCharacteristic(), Notification 설정을 추가하면 실제 센서 데이터를 받을 수 있습니다.

BLE는 “연결된 기기에서 어떤 데이터를 어떤 UUID로 제공하는가”를 이해하는 것이 핵심입니다. 따라서 Android 코드만 보기보다, 연결 대상 기기의 GATT 명세를 함께 확인하는 습관이 중요합니다.


참고: Android BLE 공식 문서 , BLE 기기 스캔 문서 , GATT 연결 문서

댓글

이 블로그의 인기 게시물

Android에서 Bluetooth 통신을 사용하는 방법

Android에서 Bluetooth 통신을 사용하는 방법 Bluetooth는 스마트폰과 차량용 기기, 센서, 블랙박스, 이어폰처럼 가까운 장치끼리 무선으로 데이터를 주고받을 때 사용하는 기능입니다. Android에서는 기기 검색부터 연결, 데이터 송수신까지 API로 처리할 수 있습니다. 이번 글에서는 Bluetooth 통신을 시작할 때 알아두면 좋은 개념과 권한, 그리고 간단한 연결 예제를 정리해 보겠습니다. 목차 Bluetooth 통신을 만들기 전 고려할 점 Bluetooth Classic과 BLE의 차이 Android 버전별 Bluetooth 권한 Bluetooth 통신 흐름 연결 및 데이터 전송 샘플 코드 1. Bluetooth 통신을 만들기 전 고려할 점 Bluetooth는 가까운 거리에서 편리하게 통신할 수 있지만, 항상 연결되어 있다고 가정하면 안 됩니다. 기기 전원이 꺼지거나 통신 범위를 벗어나면 연결이 끊길 수 있고, 페어링 여부나 장치별 지원 프로파일도 다를 수 있습니다. 기기에 Bluetooth 기능이 있는지 확인합니다. 사용자가 Bluetooth를 꺼 둔 경우, 켜도록 안내합니다. 연결·데이터 수신 작업은 메인 스레드가 아닌 별도 스레드에서 처리합니다. 연결 실패와 연결 해제 상황을 항상 처리합니다. 기기마다 통신 규격과 UUID, 데이터 형식이 다를 수 있음을 고려합니다. 2. Bluetooth Classic과 BLE의 차이 Bluetooth는 크게 Bluetooth Classic 과 Bluetooth Low Energy(BLE) 로 나눌 수 있습니다. 이름은 비슷하지만 통신 방식과 잘 맞는 사용 사례가 다릅니다. 구분 Bluetooth Classic BLE 특징 지속적인 데이터 전송에 적합 낮...

안드로이드 기초 파트 1: 안드로이드 개발 환경 설정

파트 1: 안드로이드 개발 환경 설정 1.1 안드로이드 스튜디오 설치 및 설정 1.2 안드로이드 SDK 및 필수 도구 설치 1.3 에뮬레이터 또는 실제 기기 연결 1.4 첫 번째 프로젝트 생성 및 "Hello, World!" 출력 1.5 안드로이드 스튜디오 기본 사용법 (프로젝트 구조, 주요 기능 소개)