본문 바로가기

Android Studio/BLE

[Android][Java]BLE 통신 분석 및 예제(1)

728x90

* 예제 1,2,3 전체 수정(20220702)

나에게 더 친숙한 코드를 사용하고 싶어서 예전에 연습한 적이 있는 SDK 30이하 버전으로 맞추고 다시 실습을 진행해 보았다.

추후 SDK 31이상(Android 12)을 타겟팅하는 코드도 추가할 것이다.

 

내가 사용한 SDK 버전

 

 

몰라서 분석하고 까먹고 분석하고 또 까먹고 다시 또 분석하고.. 정리해 놓기로 결심했다.

안드로이드 공식 문서와 github 예시를 참고했다.

참고로 targetSdkVersion이 31로 올려지면서 Android12 블루투스 권한이 추가되었다.

 

 

 

https://developer.android.com/guide/topics/connectivity/bluetooth-le?hl=ko 

 

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

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

developer.android.com

https://github.com/android/connectivity-samples/tree/master/BluetoothLeGatt

 

GitHub - android/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.

Multiple samples showing the best practices in connectivity on Android. - GitHub - android/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.

github.com

 

https://developer.android.com/about/versions/12/features/bluetooth-permissions?hl=ko 

 

Android 12의 새 블루투스 권한  |  Android Developers

이제 Android 13 개발자 프리뷰를 사용할 수 있습니다. 지금 사용해 보시고 의견을 알려 주세요. Android 12의 새 블루투스 권한 Android 12에서는 BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT 권한을 도입

developer.android.com

https://www.hardcopyworld.com/?p=1132

 

BLE(Bluetooth Low Energy) 이해하기

BLE 프로토콜 스펙에 대해 더욱 상세하게 정리한 자료가 업데이트 되었습니다. 아래 링크의 글도 참고하세요. 블루투스 기초  블루투스 스펙 소개 BLE 프로토콜 상세 링크 <<<<< 현재 문서 . BLE 블

www.hardcopyworld.com

 

 


 

 

주요 용어 및 개념 먼저 정리

용어 설명
GATT
(Geniric Attribut Profile, 포괄적 특성 프로필)
GATT 프로필은 짧은 데이터를 주고 받기 위한 일반적인 사양이다.
현재 모든 저전련 애플리케이션 프로필은 GATT에 기초한다.
ATT
(Attriube Protocol, 속성 프로토콜)
GATT는 ATT 위에 구축된다. ATT는 BLE 기기에서 실행되도록 최적화된다.
이를 위해서 최대한 적은 양의 데이터를 사용한다.
각 속성은 UUID(Universally Unique Identifier)로 고유하게 식별되는데, UUID는 고유 식별 정보에 사용하는 문자열 ID의 표준화된 128비트 형식을 나타낸다.

ATT가 전송하는 속성은 Characteristic(특성)과 Service(서비스)로 구성된다.
Characteristic(특성) Characteristic에는 하나의 값과 특성의 값을 설명하는 0-n 설명자가 포함된다.
특성은 일종의 유형으로, 클래스와 유사하다고 생각하면 된다.
Descriptor(설명자) Descriptor는 특성 값을 설명하도록 정의된 속성이다.
예를 들어 Descriptor는 인간이 읽을 수 있는 설명, 특성 값의 허용 가능한 범위 또는 특성 값에 적용되는 측정 단위를 지정할 수 있다.
Service(서비스) 특성의 모음이다.
예를 들어 "Heart Rate Monitor(심장 박동 모니터)"라는 서비스에는 "heart rate measurement(심장 박동 측정값)"과 같은 특성이 포함된다.

 

 

역할과 책임

Central(중앙) vs Peripheral(주변)

- Central 장치는 폰이나 태블릿과 같은 리소스를 갖춘 장치를 말한다. Peripheral 장치는 리소스가 풍부한 Central 장치에 연결되어 동작하도록 설계된 장치이다. Heart Rate Monitor(심장 박동 모니터) 등 센서 장치들이 해당된다.

Peripheral 장치는 Central 장치가 자신을 인식할 수 있도록 계속 데이터를 송출하는데 이를 advertising(게시)한다라고 한다.

즉, 중앙 역할을 맡은 기기는 주변을 스캔하며 이 advertisement를 찾고, 주변 역할을 맡은 기기는 advertisement를 만든다. 

 

● GATT Server vs GATT Client

- 두 기기 사이에 연결이 설정되었을 때 서로 통신하는 방법을 결정한다.

 

 


 

 

예제

예제 소스코드는 위에서 참고한 github 코드를 참고하였다.

 

필요한 클래스

 

 

BLE 권한

Manifest에 권한을 선언한다.

블루투스 권한을 선언하고 저전력 비콘은 위치와 연결되는 경우가 많기 때문에 위치 권한도 선언한다.

 

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

 

 

DeviceScanActivity.java

위치 접근 권한은 민감한 정보이기 때문에 Manifest에 선언하였어도 사용자에게 접근 권한을 요청하고 승인 받아야 한다.

우선 권한을 이미 승인한 적이 있는 지 확인한다.

 

	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

                requestPermissions(new String[]{android.Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSION_REQUEST_COARSE_LOCATION);
            }
        }

 

 

만약 권한을 부여 받은 적이 없다면 사용자에게 위치 접근 권한을 요청한다.

사용자가 위치 접근에 대한 퍼미션을 거부했을 경우, 다시 한 번 권한 설정에 대해 묻는 절차가 포함되어 있다.

권한 거부를 하게 되면 일부 기능에 제한이 있음을 알려주고 설정으로 이동 혹은 취소를 선택할 수 있게 하였다.

 

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        if (grantResults.length != 0) {

            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, "위치 접근 허용", Toast.LENGTH_SHORT).show();

            } else {

                Toast.makeText(this, "위치 접근 거부", Toast.LENGTH_SHORT).show();

                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setTitle("권한 설정")
                        .setMessage("권한 거부로 인해 일부 기능이 제한됩니다.")
                        .setPositiveButton("설정으로 이동", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                try {
                                    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                                            .setData(Uri.parse("package:" + getPackageName()));
                                    startActivity(intent);
                                    finish();
                                } catch (Exception e) {
                                    e.printStackTrace();
                                    Intent intent = new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
                                    startActivity(intent);
                                }
                            }
                        })

                        .setNeutralButton("취소", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                finish();
                            }
                        })
                        .create().show();
            }
        }
    }

 

 

BluetoothAdapter 가져오기

 

 

접근 권한이 승인되면 먼저 BluetoothAdapter를 초기화하고 가져온다.

BluetoothAdapter는 블루투스 송수신 장치이며, 어댑터를 통해서 원격 디바이스와 데이터를 상호작용할 수 있다.

getSystemService()를 사용하여 BluetoothManager의 인스턴스를 반환한 다음 어댑터를 가져온다.

 

	// BluetoothAdapter 가져오기
        // Initializes a Bluetooth adapter.  For API level 18 and above, get a reference to
        // BluetoothAdapter through BluetoothManager.
        bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();

 

 

BluetoothAdapter를 호출하여 블루투스 활성화를 확인한다.

블루투스가 꺼져있다면, 사용자에게 활성화 요청을 위한 메시지를 표시한다.

메시지에서 사용자가 선택한 결과 값을 startActivityForResult()에 담아 onActivityResult()로 전달한다.

사용자가 요청을 거부하면 앱을 종료시키는 흐름이다.

 

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

 

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        //  User chose not to enable Bluetooth.
        if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) {
            Toast.makeText(this, "블루투스 연결이 거부되었습니다.", Toast.LENGTH_SHORT).show();
            finish();
            return;
        }
    }

 

 

정상적으로 연결이 되었다면 listViewAdpater를 초기화하고 scanLeDevice()에 true를 넣어서 본격적인 BLE 기기 스캔을 시작한다.

 

	//  Initializes list view adapter.
        leDeviceListAdapter = new LeDeviceListAdapter();
        setListAdapter(leDeviceListAdapter);
        scanLeDevice(true);

 

 

    //  BLE 기기 찾기
    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);
        }
    }

 

스캔 작업은 배터리를 많이 소모하므로 원하는 BLE 기기를 찾는 즉시 스캔을 중단해야 한다.

공식 문서에 스캔은 절대로 반복하지말고 스캔에 시간 제한을 설정해야 한다고 적혀있다.

 

SCAN_PERIOD 만큼의 딜레이 후 스캔을 중단한다.

 

스캔 결과는 LeScanCallback()을 통해 반환된다.

스캔한 디바이스 결과를 leDeviceListAdapter.addDevice() 메소드에  넣어주고 notifyDataSetChanged()로 리스트를 갱신한다.

 

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

 

 

다음은 스캔한 디바이스 장치 목록 아이템을 클릭했을 때 호출되는 onListItemClick() 이다.

이 메소드는 DeviceScanActivity 클래스가 ListActivity를 상속받았기 때문에 override 가능한 메소드이다.

 

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);

        final BluetoothDevice device = leDeviceListAdapter.getDevice(position);

        if (device == null) return;

        final Intent intent = new Intent(this, MainActivity.class);
        intent.putExtra(MainActivity.EXTRA_DEVICE_NAME, device.getName());
        intent.putExtra(MainActivity.EXTRA_DEVICE_ADDRESS, device.getAddress());
        startActivity(intent);

        if (mScanning) {
            bluetoothAdapter.stopLeScan(leScanCallback);
            mScanning = false;
        }
    }

 

 

목록 중 선택한 position에서 device를 얻어 BluetoothDevice에 넣어준다.

 

final BluetoothDevice device = leDeviceListAdapter.getDevice(position);

 

 

디바이스가 null이 아닐 경우  디바이스의 이름, 주소를 MainActivity 클래스로 넘긴다.

 

intent.putExtra(MainActivity.EXTRA_DEVICE_NAME, device.getName());
intent.putExtra(MainActivity.EXTRA_DEVICE_ADDRESS, device.getAddress());

 

 

다음은, MainActivity.java 에 대해서!

 

 

728x90