Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

Flutter 개발 Story

BLE에대하여 본문

관련 개념

BLE에대하여

flutter 개발하자 2020. 12. 2. 08:34

정의 및 나오게 된 배경

BLE란 Bluetooth Low Energy의 약자로 말그대로 저전력 블루투스이다.

블루투스는 무선 네트워크를 이용해 기기간 데이터를 통신하는 기술이라고 볼 수 있는데, 

기존의 Classic Bluetooth는 대용량의 데이터를 통신할 수 있는 대신에 전력 소비량이 많아 안드로이드에서 사용하기에는 부담이 컸다. 이를 개선하고자 나온게 위에서 말한 BLE이다.

 

원리

BLE를 구성하는 요소는 다음과 같다.

Controller - Physical, LL, HCI

Host        - L2CAP, SM, ATT, GAP, GATT

APP         - Application

 

각 구성요소들이 어떤 기능을 하는 지는 아래와 같다.

Physical - 패킷 송수신 역할(패킷 - 데이터의 단위 중 하나로 적은 양의 데이터라고 생각하면 쉽다.)

LL(LinkLayer) - RF 상태를 제어하는 역할

(RF란? 주파수로 데이터를 주고 받는 회로)

HCI(HostControllerInterface) - Host와 Controller사이의 인터페이스 역할

L2CAP - 패킷 분배와 재조합 역할

SM(SecurityManager) - 사용자 인증 및 보안역할

ATT(AttributeProtocol) - 주변 기기의 서비스 사용을 돕는 역할

GAP(GenericAccessProfile) - 기기간의 Pairing과 Bonding사용을 통해 기기간의 인터페이스 역할

(Pairing - 블루투스 기기를 이용해 동작할 수 있도록 하는 과정(기기가 공유 비밀키를 생성하는 것))

(Bonding - 후속 연결에서 사용하는 Pairing을 할 때, 생성된 키를 저장하는 것)

GATT(GenericAttributeProfile) - ATT영역에서 받아들인 서비스의 기능을 수행하는 역할

 

각 구성요소들의 기능은 위와 같지만, 직관적으로 이해하기에는 무리가 있다. 그래서 필자는 아래처럼 생각해 보았다. 

예약 전문 음식점을 생각해보자.

전화로 주문받은 식재료를 오토바이를 타고 가져다 준다. 지배인은 가져다 준 식재료를 씻고, 파트들에게 나눠준다. 그리고 안내원들은 예약받은 것이 인정된 손님들이 요청한 음식들을 요리해 가져다 준다.

 

위 예시에서 전화는 HCI, 식재료는 패킷, 식재료를 가져다주는 사람이 Physical, 오토바이가 LL, 식재료를 씻고 나눠주는 지배인이 L2CAP, 안내원들은 ATT, 예약 인정은 SM, 손님들은 주변 기기들, 요리하는 것은 GATT라고 볼 수 있다. 

 

이 구성원들중 안드로이드 개발자가 다루는 구성요소들은 주로 GATT이다. 

그 이유는 안드로이드에서 블루투스 api를 제공하기 때문이다. 

developer.android.com/guide/topics/connectivity/bluetooth

 

블루투스 개요  |  Android 개발자  |  Android Developers

The Android platform includes support for the Bluetooth network stack, which allows a device to wirelessly exchange data with other Bluetooth devices. The application framework provides access to the Bluetooth functionality through the Android Bluetooth…

developer.android.com

 

안드로이드에서 블루투스(BLE X,classic Bluetooth)

블루투스 지원 기기가 서로 데이터를 전송하려면 페어링을 사용해 통신 채널을 만들어야함. 

(검색 가능한 기기가 수신되는 연결 요청 용도로 사용할 수 도 있음)

메인 기기는 서비스 검색을 사용해 검색 가능한 기기를 찾음 -> 검색 가능한 기기가 페어링 요청을 수락하면, 두 기기가 보안키를 교환해 기기간 연결이 됨 (이 키는 나중에 사용할 수 있도록 기기가 캐싱해둠(본딩))

-> 데이터 교환 -> 세션이 완료되면 페어링 요청 기기가 자신과 검색 가능한 기기와 연결해준 채널을 해제함

(하지만 두 기기는 연결된 상태로 유지되므로 서로 범위 내에 있고, 두 기기 중 어느쪽도 연결을 삭제하지만 않았다면 향후 세션에서도 자동으로 다시 서로 연결가능)

ex)블루투스 이어폰으로 따지면, 안드로이드 폰에서 블루투스 이어폰과 데이터를 송수신할 통신채널을 만들고, 폰은 서비스 검색을 통해 블루투스 이어폰을 찾음. -> 블루투스 이어폰이 폰에서 요청한 페어링을 수락하면, 폰과 블루투스 이어폰이 보안키를 교환해 연결이 됨 -> 데이터 교환 -> 세션이 만료되면, 폰이 폰과 이어폰이 연결하게 해준 채널을 해제함.

 

Howto?

1.권한 설정

Manifest

<manifest ... >
 
<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" />
  ...
</manifest>

 

2.블루투스 설정

블루투스를 사용하여 통신하려면, 기기에서 블루투스를 지원하지는 확인하고, 지원되는 경우 활성화 시킴

블루투스가 지원되지 않는 경우 모든 블루투스 기능을 비활성화해야함

블루투스가 지원되지만 비활성화된 경우, 사용자가 앱을 떠나지 않는 상태에서 블루투스를 활성화하도록 요청할 수 있어야함.

2.1)순서

(1)블루투스 어댑터 가져옴(블루투스 activity는 Booluetooth어댑터가 필요함)

//블루투스 어댑터 - 블루투스 송수신 장치

//getDefaultAdapter() - 기기 자체의 블루투스 어댑터를 가져옴

//getDefaultAdapter()값이 null이면 블루투스 지원안하는 것임.

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
   
// Device doesn't support Bluetooth
}

(2)블루투스 활성화

isEnabled()로 블루투스 켜져있는지 확인하고(false리턴하면 안 켜진 거임)

활성화를 요청하려면 인텐트에  ACTION_REQUEST_ENABLE 를 넣어 startActivityForResult()로 요청

if (!bluetoothAdapter.isEnabled()) {
   
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult
(enableBtIntent, REQUEST_ENABLE_BT);
} 

-> 위와 같이 요청하면 대화상자로 블루투스 킬거냐고 물어봄

REQUEST_ENABLE_BT는 0이상인 정수여야하고, onActivityResult()에서 requestCode로 사용됨

블루투스 활성화되면 onActivityResult()에서 RESULT_OK 수신받고, 실패하면(or 사용자가 거부하면) RESULT_CANCELED수신됨

 

옵션으로 앱이 ACTION_STATE_CHANGED 브로드캐스트 인텐트를 수신 대기할 수도 있는데, 이것은 블루투스 상태가 변경될 때마다 시스템이 브로드캐스트합니다. 이 브로드캐스트는 새로운 블루투스 상태와 이전 블루투스 상태를 각각 포함하는 엑스트라 필드 EXTRA_STATE  EXTRA_PREVIOUS_STATE를 포함합니다. 이 엑스트라 필드에 사용 가능한 값은 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF  STATE_OFF입니다. 앱이 블루투스 상태에 적용된 런타임 변경사항을 감지해야 하는 경우, 이 브로드캐스트를 수신 대기하는 것이 유용할 수 있습니다.

 

GATT (General Attribute Profile) 는 ATT (Attribute Protocol) 를 이용하여 장치 사이에 데이터를 전송하는 방식을 결정하는 서비스 프레임워크이다. ATT 는 서비스(Service) 와 특성(Characteristic) 이라는 개념을 사용하여 어떤 데이터, 즉 “attribute” 를 다른 장치에게 보여주도록 한다. 이 때 어트리뷰트를 보여주는 장치를 서버라고 하고, 보는 장치를 클라이언트라고 한다. 마스터 장치는 ATT 서버 또는 ATT 클라이언트가 될 수 있고, 슬레이브 장치도 마찬가지이다. 아니면, 한 장치가 ATT 서버와 클라이언트 두 가지 역할을 모두 맡을 수도 있다. 예를 들면, GAP 슬레이브/peripheral 장치가 GATT 서버가 되고, GAP 마스터/ central 장치가 GATT 클라이언트 장치가 되어 서로 통신할 수 있다. 그리고, ATT 프로토콜 수준에서 가능한 동작들은 다음 표와 같다.

 

*************************************************************************************************************************

BLE관련 작업을 하다보면 characteristic이라는 개념을 마주하게 된다. 특성이란 뜻인데? 이게 뭐고, 뭐에 쓰는걸까?

*************************************************************************************************************************

 

 

3.프로필 작업

블루투스의 특징들은 다양한 기기의 통신에 적용됨

블루투스 프로필 - 더 많은 기기와 호환성을 위해 기기 종류간에 정의된 프로토콜

3.1)프로필 작업 순서

   1. 블루투스 어댑터 가져옴

   2. BluetoothProfile.ServiceListener를 설정함(이 리스너는 서비스에 연결되었거나 끊어진 경우 BluetoothProfile ipc클라이언트에 알림)

   3. getProfileProxy()를 사용해 프로필과 연결된 프로필 프록시 객체와의 연결을 설정

   4. onServiceConnected()에서 프로필 프록시 객체에 대한 핸들을 가져옴

   5. 프로필 프록시 객체가 있는 경우 이를 사용해 연결 상태를 모니터링하고 해당 프로필과 관련된 다른 작업을 수행할 수 있음.

 

//이하 코드 스니펫은 블루투스 헤드셋 프록시 객체에 연결하는 방법임

BluetoothHeadset bluetoothHeadset;
//기기 자체의 블루투스 어댑터를 가져옴

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

//서비스와의 연결을 관리하는리스너를 생성

private BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {

   //블루투스 연결 됐을 시 

    public void onServiceConnected(int profile, BluetoothProfile proxy) {

        //프로필과 블루투스 프로필의 헤드셋 프로필과 동일할 경우
       
if (profile == BluetoothProfile.HEADSET) {

           //블루투스 헤드셋 객체에 프록시 객체를 넣음 
            bluetoothHeadset
= (BluetoothHeadset) proxy;
       
}
   
}
   
public void onServiceDisconnected(int profile) {
       
if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset
= null;
       
}
   
}
};

 

//블루투스 어댑터와 프록시와의 연결

//블루투스 어댑터에 프로필 프록시를 가져옴
bluetoothAdapter.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET);

//블루투스 헤드셋 관련 함수~~

 

//사용후 블루투스 어댑터와 프록시 객체와의 연결을 끊음
bluetoothAdapter.closeProfileProxy(bluetoothHeadset);

 

*스니펫 - 작은 조각이란 의미로 코드에서 개발 시 사용되는 여러 형태의 코드 조각들을 의미

 

4.기기 찾기(Scan)

블루투스 어댑터를 이용해 기기 검색 또는 페어링된 기기목록을 쿼리해 원격 블루투스 기기를 찾을 수 있음.

-기기 검색(Scan)- 주변의 블루투스 지원기기를 찾아 기기에 정보를 요청함(블루투스 기기가 정보 요청을 수락해야됨)

                     - 요청 응답 - 기기 이름, 클래스, 고유 MAC주소와 같은 정보를 공유해 응답

 (원격 기기와 처음으로 연결되면 페어링 요청이 자동으로 사용자에게 제공됨 - 페어링되면 해당 기기에 대한 기본 정보(기기명, 클래스 및 MAC주소)등이 저장되고, Bluetooh API를 사용해 읽을 수 있음)

(MAC주소를 사용하면 검색을 수행하지 않고 언제든지 연결을 할 수 있음)

 

*페어링된 것과 연결된 것의 차이

페어링 - 두 기기가 서로의 존재를 알고, 인증에 사용할 수 있는 공유 링크 키를 갖고 있으며, 서로 암호화된 연결을 설정할 수 있다는 것을 의미

연결 - 기기가 현재 RFCOMM 채털을 공유하고 있고, 데이터를 서로 전송할 수 있다는 것을 의미

        (Bluetooth API는 RFCOMM 연결을 설정하기 전에 페어링 요청부터 먼저함 -> 암호화된 연결을 시작하면 페어링을 자동으로 실행)

5.1)페어링된 기기 쿼리

기기 검색전 페어링된 기기 집합을 쿼리해 원하는 기기가 이미 있는지 확인 -> 있다면 기기의 이름과 MAC주소를 갖고 옴

//pairedDevices에 getBondedDevices()로 페어링된 BluetoothDevice객체 세트를 넣음

Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

//페어링된 객체 세트에 원소가 하나 이상이라면
if (pairedDevices.size() > 0) {

    // device - 페어링된 기기
   
for (BluetoothDevice device : pairedDevices) {

        //기기의 이름

        String deviceName = device.getName();
       // 기기의 MAC주소

       String deviceHardwareAddress = device.getAddress();
   
}
}

4.2)기기 검색

startDiscovery()로 기기 검색을 시작. 시작이 성공적인지는 bool값으로 반환해줌(비동기식)

기기 검색을 시작하려면 startDiscovery()를 호출합니다. 이 프로세스는 비동기식이고 검색이 성공적으로 시작되었는지 나타내는 부울 값을 반환합니다. 일반적으로 검색 프로세스는 12초 정도의 조회 스캔과, 검색된 각 기기의 페이지 스캔을 통해 블루투스 이름을 가져오는 과정이 포함됩니다.

애플리케이션이 검색된 각 기기에 대한 정보를 수신하려면 ACTION_FOUND 인텐트에 대한 BroadcastReceiver를 등록해야 합니다. 시스템이 각 기기에 대하여 이 인텐트를 브로드캐스트합니다. 인텐트에는 엑스트라 필드 EXTRA_DEVICE  EXTRA_CLASS가 포함되고, 이 엑스트라 필드에는 각각 BluetoothDevice  BluetoothClass가 포함됩니다. 다음 코드 스니펫은 기기를 검색할 때 브로드캐스트를 처리하도록 등록하는 방법을 보여줍니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
   
...
    // 기기가 발견됐을때 브로드 캐스트 등록
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);

   //ACTION_FOUND 인텐트에대한 BroadcastReceiver를 등록해 

  //시스템이 각 기기에 대해 이 인텐트를 브로드 캐스트하게 함

    registerReceiver(receiver, filter);
}

//  ACTION_FOUND인텐트에대한 브로드캐스트 리시버
private final BroadcastReceiver receiver = new BroadcastReceiver() {
   
public void onReceive(Context context, Intent intent) {
       
String action = intent.getAction();

        //받은 인텐트의 액션이 BluetoothDevice의 ACTION_FOUND와 동일하다면 
       
if (BluetoothDevice.ACTION_FOUND.equals(action)) {

            //ACTION_FOUND의 인텐트에는 2가지 엑스트라 필드가 있음

            // EXTRA_DEVICE, EXTRA CLASS - 블루투스 디바이스 및 클래스가 포함됨

            //인텐트로부터 블루투스 디바이스 및 클랙스를 갖고 옴

            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            //위를 이용해 기기의 이름과 MAC주소등을 가져옴

            String deviceName = device.getName();
           
String deviceHardwareAddress = device.getAddress();
       
}
   
}
};
@Override
protected void onDestroy() {
   
super.onDestroy();
   
...
   //Activity끝났을 때, ACTION_FOUND 리시버 해제해줘야함.
    unregisterReceiver(receiver);
}

4.3)검색 기능 활성화

로컬 기기를 다른 기기가 검색할 수 있게 하려면,ACTION_REQUEST_DISCOVERABLE 인텐트를 사용해 startActivityForResult(Intent, int)를 호출

-> 이러면 설정 앱으로 이동할 필요 없이 시스템의 검색 가능한 모드를 활성화 해줌

 

Intent discoverableIntent =
       
new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent
.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity
(discoverableIntent);

 

-> 검색 기능 활성화에대한 대화상자를 사용자에게 띄워줌

 

5.기기 연결

두 기기간 연결을 만드려면 서버측, 클라이언트측 메커니즘을 모두 구현해야함. 

(why? 한 기기는 서버 소켓을 결고, 다른 기기는 서버 기기의 MAC주소를 이용해 연결을 시작해야하기 때문)

 - 서버 기기와 클라이언트 기기 둘다 각각 필요한 BluetoothSocket 을 서로 다른 방법으로 가져와야함

(서버는 수신되는 연결을 수락할 때 소켓 정보를 받고, 클라이언트는 서버에 대한 RFCOMM채털을 열때 소켓 정보를 받음) 

 - 서버와 클라이언트는 각각 동일한 RFCOMM 채널에 연결된 BluetoothSocket이 있는 경우 서로 연결된 것으로 간주됨

-> 이 시점에서 각 기기가 입력 및 출력 스트림을 얻을 수 있고, 데이터 전송을 할 수 있음 

 

**

각 기기가 서버 소켓을 열고 연결을 수신 대기하도록 각 기기를 서버로 자동으로 준비하는 구현 기술이 있음 -> 이 기술을 이용해 양쪽 기기 모두 다른 기기와 연결을 시작하고 클라이언트가 될 수 있음 or 한 기기는 연결을 명시적으로 "호스팅"하고 요청 시 서버 소켓을 열 수 있게 되고, 다른 기기는 연결을 시작함

**

 

5.1)서버로 연결

두 기기를 연결하려면 한 기기는 열린 BluetoothServerSocket을 제공해 서버로 작동해야함

서버 소켓 역할 - 연결 요청 수신 대기

                    - 수락시 연결된 BluetoothSocket을 제공

                    (BluetoothServerSocket에서 BluetoothSocket을 가져올 경우, 추가 연결을 수락하지 않으려면 BluetoothServerSocket을 취소해야함)   

순서

  1. listenUsingRfcommWithServiceRecord()를 호출해 BluetoothServerSocket을 가져옴

     

     

    -클라이언트가 이 기기와 연결을 시도할 때는 해당 클라이언트가 연결하고자 하는 서비스를 고유하게 식별해주는 UUID를 제공 (UUID - 범용 식별자) - 이 UUID와 일치해야 연결이 수락됨(fromString(String)을 사용해 UUID를 초기화 할 수 있음)
  2. accept()를 호출해 연결 요청에 대한 수신 대기를 시작

     

    -원격 기기가 이 서버 소켓에 등록된 것과 일치하는 UUID를 사용해 연결 요청을 보내는 경우만 연결이 수락됨. 연결 성공시, accept()가 연결된 BluetoothSocket 반환
  3. 추가 연결을 수락하지 않으려면 close()를 호출

    -close()호출은 서버 소켓과 모든 리소스를 해제하지만 accept()가 반환한 연결된 BluetoothSocket이 닫히지는 않음(TCP/IP와 달리 RFCOMM은 한 번에 채널당 하나의 클라이언트 연결을 허용함) 

accept()호출은 차단 호출이라서 Activity UI 스레드에서 실행하면 안됨 -> 앱이 관리하는 새 스레드에서 BluetoothServerSocket 또는 BluetoothSocket을 사용해 모든 작업을 수행해야함 -> accept()같은 차단된 호출을 중단하려면 또 다른 스레드의 BluetoothServerSocket 또는 BluetoothSocket에서 close()를 호출해야함

 

private class AcceptThread extends Thread {

   //BluetoothServerSocket 선언
   
private final BluetoothServerSocket mmServerSocket;
    

    //accept에 사용할 스레드

   public AcceptThread() {

        //나중에 위에서 선언한 서버 소켓을 할당하기 위한 임시 객체 선언

        //->왜냐면 위에서 선언한 서버 소켓이 final이라서
        BluetoothServerSocket tmp = null;
       
try {

            //MY_UUID는 앱의 UUID 

            //클라이언트 코드에도 사용됨

            //listenUsingRfcommWithServiceRecord()를 이용해 Bluetooth서버 소켓을 가져옴
            tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
       
} catch (IOException e) {
           
Log.e(TAG, "Socket's listen() method failed", e);
       
}

       //final로 선언한 서버소켓에 서버소켓을 할당한 임시 객체 값을 넣음
        mmServerSocket
= tmp;
   
}

 

//accept()로 대기중 연결이 수락되면, 반환된 소켓을 가져오자자 그 소켓을 이용하는 다른 스레드(manageMyConnectedSocket)에넘겨주고 

//대기하는 루프를 중단
   
public void run() {

        // 블루투스 소켓 생성
       
BluetoothSocket socket = null;

        // 소켓 반환 또는 에러 발생까지 리스닝
        while (true) {
           
try {

               //서버 소켓에 accept()를 이용해 연결 요청에대한 수신 대기

               //서버 소켓의 accept()로 BluetoothSocket이 반환되면 연결된 것
                socket
= mmServerSocket.accept();
           
} catch (IOException e) {
               
Log.e(TAG, "Socket's accept() method failed", e);
               
break;
           
}
              
           
if (socket != null) {

                //연결이 수락되면 다른 스레드와 연결된 작업 수행

                // manageMyConnectedSocket는 accept()로 받은 소켓을 이용하는 다른 스레드

                //manageMyConnectedSocket() - 데이터 전송을 위한 스레드를 시작하도록 설계됨.
                manageMyConnectedSocket(socket);
                mmServerSocket
.close();
               
break;
           
}
       
}
   
}
    // 연결된 소켓을 닫고, 그 스레드를 끝냄
    public void cancel() {
       
try {
            mmServerSocket
.close();
       
} catch (IOException e) {
           
Log.e(TAG, "Could not close the connect socket", e);
       
}
   
}
}

5.2) 클라이언트로 연결

열린 서버 소켓에서 연결을 수락하는 원격 기기와의 연결을 시작하려면, 원격 기기를 나타내는 BluetoothDevice객체를 얻어야함 -> BluetoothDevice객체를 이용해 BluetoothSocket을 가져오고 연결 시작

  1. BluetoothDevice를 통해 createRfcommSocketToServiceRecord(UUID)를 호출해 BluetoothSocket을 가져옴.

    이 메서드는 BluetoothSocket 객체를 초기화하는데, 이 객체는 클라이언트가 BluetoothDevice에 연결하도록 허용합니다. 여기에 전달된 UUID는 서버 기기가 listenUsingRfcommWithServiceRecord(String, UUID)를 호출해 BluetoothServerSocket을 열 때 사용한 UUID와 일치해야 함 -> 일치하는 UUID를 사용하려면 UUID 문자열을 애플리케이션에 하드코딩한 다음, 이를 서버와 클라이언트 코드 양쪽에서 모두 참조

  2. connect()를 호출하여 연결시작

    클라이언트가 connect()호출 후, 시스템에 SDP 조회를 실행해 이와 일치하는 UUID를 포함한 원격 기기를 찾음 ->조회 후에는 원격 기기가 연결을 수락하는 경우, 연결되는 동안 RFCOMM 채널을 공유하고, connect()가 반환됨(연결이 실패하거나 connect()의 시간이 초과되면 IOException발생)

    (connect()가 차단 호출이므로, 이 연결 절차는 항상 기본 Activity(UI) 스레드와 분리된 스레드에서 수행해야 함)

 

private class ConnectThread extends Thread {

    //블루투스 소켓과 디바이스 final로 선언
   
private final BluetoothSocket mmSocket;
   
private final BluetoothDevice mmDevice;
   
   
public ConnectThread(BluetoothDevice device) {

       //나중에 위의 소켓을 할당할 임시 소켓 객체 선언

       //이유는 mmSocket이 final이라서
       
BluetoothSocket tmp = null;
        mmDevice
= device;
       
try {

           //createRfcommSocketToServiceRecord()로 블루투스 기기와 연결하기 위해 블루투스 소켓을 가져옴
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
       
} catch (IOException e) {
           
Log.e(TAG, "Socket's create() method failed", e);
       
}

       //createRfcommSocketToServiceRecord로 가져온 블루투스 소켓을 final로 선언한 소켓 객체에 할당
        mmSocket
= tmp;
   
}

   
public void run() {

        //블루투스 기기 검색 취소 - 계속 검색하면 속도 느려짐
       
// Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter
.cancelDiscovery();

       
try {

            // 소켓을 통해 블루투스 기기와 연결 

            // connect()는 연결이 성공하거나 예외발생때까지 돌아감
            mmSocket.connect();
       
} catch (IOException connectException) {
           
// Unable to connect; close the socket and return.
           
try {
                mmSocket
.close();
           
} catch (IOException closeException) {
               
Log.e(TAG, "Could not close the client socket", closeException);
           
}
           
return;
       
}
        // 연결 시도가 성공하면 다른 스레드에 소켓을 줘서 작업 수행
        manageMyConnectedSocket(mmSocket);
   
}

   
// Closes the client socket and causes the thread to finish.
   
public void cancel() {
       
try {
            mmSocket
.close();
       
} catch (IOException e) {
           
Log.e(TAG, "Could not close the client socket", e);
       
}
   
}
}

 

6)연결 관리

블루투스 기기에 연결이 성공하고 나면, 기기에 연결된 BluetoothSocket이 생김 -> 이 소켓을 이용해 기기간에 정보를 공유할 수 있음 

BluetoothSocket을 사용하는 데이터 전송 절차

  1. getInputStream()  getOutputStream()을 각각 사용해 소켓을 통해 전송을 처리하는 InputStream  OutputStream을 가져옴
  2. read(byte[])  write(byte[])를 사용하여 스트림에 데이터를 읽고 씀

스트림에서 읽고 쓰려면 전용 스레드를 사용해야함.

(why?read(byte[]) write(byte[]) 메서드 둘다 차단 호출이라서)

read(byte[]) - 스트림에서 읽을 것이 생길 때까지 차단

write(byte[]) - 블루투스 기기가 read(byte[])를 호출하지 않아 중간 버퍼가 가득차면 흐름 제어를 위해 차단

 

-> 스레드의 기본 루프는 InputStream에서 읽기 전용으로 사용해야 함

스레드의 개별 공용 메서드를 사용해 OutputStream에서 쓰기를 시작 

 

public class MyBluetoothService {
   
private static final String TAG = "MY_APP_DEBUG_TAG";

   //블루투스 서비스로부터 갖고 온 핸들러
   
private Handler handler;

 

    // 서비스와 UI사이에 메세지가 전송될때 사용되는 상수들
    private interface MessageConstants {
       
public static final int MESSAGE_READ = 0;
       
public static final int MESSAGE_WRITE = 1;
       
public static final int MESSAGE_TOAST = 2;

    }

   
private class ConnectedThread extends Thread {
       
private final BluetoothSocket mmSocket;
       
private final InputStream mmInStream;
       
private final OutputStream mmOutStream;

        //스트림을 저장할 버퍼
       
private byte[] mmBuffer;
       
public ConnectedThread(BluetoothSocket socket) {
            mmSocket
= socket;
           
InputStream tmpIn = null;
           
OutputStream tmpOut = null;
           

            // 인풋과 아웃풋스르림을 갖고 옴
            try {
                tmpIn
= socket.getInputStream();
           
} catch (IOException e) {
               
Log.e(TAG, "Error occurred when creating input stream", e);
           
}
           
try {
                tmpOut
= socket.getOutputStream();
           
} catch (IOException e) {
               
Log.e(TAG, "Error occurred when creating output stream", e);
           
}
           // 갖고 온 인풋과 아웃풋 스트림을 final로 선언해놨던 객체들에 넣음
            mmInStream
= tmpIn;
            mmOutStream
= tmpOut;
       
}

       
public void run() {
            mmBuffer
= new byte[1024];

            //read()로부터 반환된 바이트를 넣을 객체
           
int numBytes;

        

            // 예외 발생할 때까지 인풋스트림의 리스닝을 유지
            while (true) {
               
try {

                    // 인풋스트림으로부터 읽어온 값을 위의 버퍼 객체에 넣음
                    numBytes = mmInStream.read(mmBuffer);

                    // UI Activity에 버퍼 객체를 담은 메세지 보냄

                    Message readMsg = handler.obtainMessage(
                           
MessageConstants.MESSAGE_READ, numBytes, -1,
                            mmBuffer
);
                    readMsg
.sendToTarget();
               
} catch (IOException e) {
                   
Log.d(TAG, "Input stream was disconnected", e);
                   
break;
               
}
           
}
       
}


        // MainActivity로부터 블루투스 기기에 데이터를 보내기 위한 호출(함수)
        public void write(byte[] bytes) {
           
try {

               //버퍼 객체를 매게변수로 받아 아웃풀 스트림에 담음
                mmOutStream
.write(bytes);

 

                // UI activity와 보낼 메세지 공유
                Message writtenMsg = handler.obtainMessage(
                       
MessageConstants.MESSAGE_WRITE, -1, -1, mmBuffer);
                writtenMsg
.sendToTarget();
           
} catch (IOException e) {
               
Log.e(TAG, "Error occurred when sending data", e);

                // 데이터 전송 실패 시, activity에게 보낼 메세지를 보냄

                Message writeErrorMsg =
                        handler
.obtainMessage(MessageConstants.MESSAGE_TOAST);
               
Bundle bundle = new Bundle();
                bundle
.putString("toast",
                       
"Couldn't send data to the other device");
                writeErrorMsg
.setData(bundle);
                handler
.sendMessage(writeErrorMsg);
           
}
       
}
        // 연결이 끝났을 때, MainActivity에서 호출할 함수
        public void cancel() {
           
try {
                mmSocket
.close();
           
} catch (IOException e) {
               
Log.e(TAG, "Could not close the connect socket", e);
           
}
       
}
   
}
}

'관련 개념' 카테고리의 다른 글

Provider 패턴  (0) 2021.02.09
BLOC 패턴  (0) 2021.02.09
미디어 세션  (0) 2021.01.18
PubSub모델  (0) 2021.01.14
Comments