[Android] Android bluetooth connect and receive data(안드로이드 블루투스 연결 및 데이터 수신)

How to connect bluetooth device and receive data in android device


이 글은 일반적으로 많이 사용되는 HC-06 블루투스 모듈과 통신하기 위한 방법을 설명한다. HC-06은 블루투스 2.0을 이용하며 최근 많이 사용되고 있는 저전력 블루투스 모듈인 BLE와는 다르다.

1. 블루투스 관련 권한 설정

안드로이드 디바이스에서 블루투스 관련 기능을 사용하기 위해서는 블루투스 관련 권한 설정을 해야한다.

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

안드로이드 디바이스에서 블루투스 권한을 얻기 위해서는 AndroidManifest.xml 파일에 위의 코드 두줄을 추가해 줘야 한다.

이 글에서는 다루지 않고 따로 정리하겠지만 블루투스 연결뿐만 아니라 주변에 있는 블루투스 기기 검색을 위해서는 다음과 같이 ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION 에 대한 권한도 필요하다.

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

2. 블루투스 활성화 및 연결

블루투스 연결 관련 함수들을 class 로 작성해서 사용하는 것이 좋은 방법이며 다음과 같은 맴버 변수를 선언하였다.

private static final int REQUEST_ENABLE_BT = 3;
public BluetoothAdapter mBluetoothAdapter = null;
Set<BluetoothDevice> mDevices;
int mPairedDeviceCount;
BluetoothDevice mRemoteDevice;
BluetoothSocket mSocket;
InputStream mInputStream;
OutputStream mOutputStream;
Thread mWorkerThread;
int readBufferPositon;      //버퍼 내 수신 문자 저장 위치
byte[] readBuffer;      //수신 버퍼
byte mDelimiter = 10;

가장 먼저 사용하는 기기가 블루투스를 지원하는지, 지원한다면 블루투스가 켜져있는지 확인해야 한다.

public void CheckBluetooth(){

    mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
	if (mBluetoothAdapter == null) {
		// 장치가 블루투스 지원하지 않는 경우
		Toast.makeText(activity, "Bluetooth no available", Toast.LENGTH_SHORT).show();
	} else {
		// 장치가 블루투스 지원하는 경우
		if (!mBluetoothAdapter.isEnabled()) {
			// 블루투스를 지원하지만 비활성 상태인 경우
			// 블루투스를 활성 상태로 바꾸기 위해 사용자 동의 요첨
			Intent enableBtIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE);
			activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
		} else {
			// 블루투스를 지원하며 활성 상태인 경우
			// 페어링된 기기 목록을 보여주고 연결할 장치를 선택.
			selectDevice();
		}
	}
}

이상이 없는 경우에 pairing 목록에서 연결하고자 하는 기기를 검색해서 사용자에게 선택하도록 한다. 이때 bluetooth 모듈에 연결하기 위해서는 기기와 paring 한 기록이 있어야 한다. paring이 되어 있다는 의미는 기기의 정보가 핸드폰에 등록이 되어 있으며 서로 존재를 알고 있다는 의미이다. pairing은 핸드폰의 블루투스 설정에서 기기 검색을 통해 가능하다. (코드를 통해서 pairing 과정을 수행할 수도 있다. 하지만 이 글에서는 페어링이 되어 있는 기기에 연결하는 과정만을 설명한다) selectDevice() 함수는 현재 핸드폰에 등록되어 있는 pairing목록을 화면의 띄워서 사용자에게 연결하고자 하는 기기를 선택할 수 있도록 화면을 띄워준다.

private void selectDevice() {
    //페어링되었던 기기 목록 획득
    mDevices = mBluetoothAdapter.getBondedDevices();
    //페어링되었던 기기 갯수
    mPairedDeviceCount = mDevices.size();
    //Alertdialog 생성(activity에는 context입력)
    AlertDialog.Builder builder = new AlertDialog.Builder(activity);
    //AlertDialog 제목 설정
    builder.setTitle("Select device");


    // 페어링 된 블루투스 장치의 이름 목록 작성
    final List<String> listItems = new ArrayList<String>();
    for (BluetoothDevice device : mDevices) {
        listItems.add(device.getName());
    }
    if(listItems.size() == 0){
        //no bonded device => searching
        Log.d("Bluetooth", "No bonded device");
    }else{
        Log.d("Bluetooth", "Find bonded device");
        // 취소 항목 추가
        listItems.add("Cancel");

        final CharSequence[] items = listItems.toArray(new CharSequence[listItems.size()]);
        builder.setItems(items, new DialogInterface.OnClickListener() {
            //각 아이템의 click에 따른 listener를 설정
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Dialog dialog_ = (Dialog) dialog;
                // 연결할 장치를 선택하지 않고 '취소'를 누른 경우
                if (which == listItems.size()-1) {
                    Toast.makeText(dialog_.getContext(), "Choose cancel", Toast.LENGTH_SHORT).show();
                
                } else {
                    //취소가 아닌 디바이스를 선택한 경우 해당 기기에 연결
                    connectToSelectedDevice(items[which].toString());
                }

            }
        });

        builder.setCancelable(false);    // 뒤로 가기 버튼 사용 금지
        AlertDialog alert = builder.create();
        alert.show();   //alert 시작
    }
}

connectToSelectedDevice() 함수는 선택된 기기로 연결하는 과정을 시작하는 함수이다.

private void connectToSelectedDevice(final String selectedDeviceName) {
    //블루투스 기기에 연결하는 과정이 시간이 걸리기 때문에 그냥 함수로 수행을 하면 GUI에 영향을 미친다
    //따라서 연결 과정을 thread로 수행하고 thread의 수행 결과를 받아 다음 과정으로 넘어간다.
    
    //handler는 thread에서 던지는 메세지를 보고 다음 동작을 수행시킨다.
    final Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.what == 1) // 연결 완료
            {
                try {
                    //연결이 완료되면 소켓에서 outstream과 inputstream을 얻는다. 블루투스를 통해
                    //데이터를 주고 받는 통로가 된다.
                    mOutputStream = mSocket.getOutputStream();
                    mInputStream = mSocket.getInputStream();
                    // 데이터 수신 준비
                    beginListenForData();
                    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {    //연결 실패
                Toast.makeText(activity,"Please check the device", Toast.LENGTH_SHORT).show();
                try {
                    mSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    //연결과정을 수행할 thread 생성
    Thread thread = new Thread(new Runnable() {
        public void run() {
            //선택된 기기의 이름을 갖는 bluetooth device의 object
            mRemoteDevice = getDeviceFromBondedList(selectedDeviceName);
            UUID uuid = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");

            try {
                // 소켓 생성
                mSocket = mRemoteDevice.createRfcommSocketToServiceRecord(uuid);
                // RFCOMM 채널을 통한 연결, socket에 connect하는데 시간이 걸린다. 따라서 ui에 영향을 주지 않기 위해서는
                // Thread로 연결 과정을 수행해야 한다.
                mSocket.connect();
                mHandler.sendEmptyMessage(1);
            } catch (Exception e) {
                // 블루투스 연결 중 오류 발생
                mHandler.sendEmptyMessage(-1);
            }
        }
    });
    
    //연결 thread를 수행한다
    thread.start();
}

//기기에 저장되어 있는 해당 이름을 갖는 블루투스 디바이스의 bluetoothdevice 객채를 출력하는 함수
//bluetoothdevice객채는 기기의 맥주소뿐만 아니라 다양한 정보를 저장하고 있다.

BluetoothDevice getDeviceFromBondedList(String name) {
    BluetoothDevice selectedDevice = null;
    mDevices = mBluetoothAdapter.getBondedDevices();
    //pair 목록에서 해당 이름을 갖는 기기 검색, 찾으면 해당 device 출력
    for (BluetoothDevice device : mDevices) {
        if (name.equals(device.getName())) {
            selectedDevice = device;
            break;
        }
    }
    return selectedDevice;
}

socket에 연결이 완료되면 데이터를 수신할 thread (beginListenForDat()) 를 실행한다.

//블루투스 데이터 수신 Listener
protected void beginListenForData() {
    final Handler handler = new Handler();
    readBuffer = new byte[1024];  //  수신 버퍼
    readBufferPositon = 0;        //   버퍼 내 수신 문자 저장 위치
    
    // 문자열 수신 쓰레드
    mWorkerThread = new Thread(new Runnable() {
        @Override
        public void run() {

            while (!Thread.currentThread().isInterrupted()) {

                try {

                    int bytesAvailable = mInputStream.available();
                    if (bytesAvailable > 0) { //데이터가 수신된 경우
                        byte[] packetBytes = new byte[bytesAvailable];
                        mInputStream.read(packetBytes);
                        for (int i = 0; i < bytesAvailable; i++) {
                            byte b = packetBytes[i];
                            if (b == mDelimiter) {
                                byte[] encodedBytes = new byte[readBufferPositon];
                                System.arraycopy(readBuffer, 0, encodedBytes, 0, encodedBytes.length);
                                final String data = new String(encodedBytes, "US-ASCII");
                                readBufferPositon = 0;
                                handler.post(new Runnable() {
                                    public void run() {
                                        //수신된 데이터는 data 변수에 string으로 저장!! 이 데이터를 이용하면 된다.
                                    
                                        char[] c_arr = data.toCharArray(); // char 배열로 변환
                                        if (c_arr[0] == 'a') {
                                            if (c_arr[1] == '1') {
                                            
                                            //a1이라는 데이터가 수신되었을 때
                                            
                                            }
                                            if (c_arr[1] == '2') {
                                            
                                            //a2라는 데이터가 수신 되었을 때
                                            }
                                        }
                                    }
                                });
                            } else {
                            readBuffer[readBufferPositon++] = b;
                            }
                        }
                    }
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    //데이터 수신 thread 시작
    mWorkerThread.start();
}

마지막으로 데이터를 전송하는 방법은 다음과 같다.

public void SendResetSignal(){
    String msg = "bs00000";
    try {
        mOutputStream.write(msg.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

이번 글에서는 pairing목록에서 기기 정보를 가져와서 블루투스 통신을 수행하는 방법에 대해서 다루었다. 다음에는 기기가 pairing목록에 없을 때 기기를 검색해서 pairing과정까지 한번에 수행하는 방법을 알아볼 것이다.




[SLAM] Bayes filter(베이즈 필터)

SLAM framework에서 Bayes filter에 대한 설명.


본 글은 University Freiburg의 Robot Mapping 강의를 바탕으로 이해하기 쉽도록 정리하려는 목적으로 작성되었습니다. 개인적인 의견을 포함하여 작성되기 때문에 틀린 내용이 있을 수도 있습니다. 틀린 부분은 지적해주시면 확인 후 수정하겠습니다.

SLAM(Simultaneous Localization and Mapping)

SLAM은 simultaneous localization and mapping의 줄임말로 위치추정(localization)과 지도생성(mapping)을 동시에 하는 연구분야를 의미한다. 이는 닭이 먼저냐 달걀이 먼저냐의 문제와 비슷하다. 자기의 위치를 추정하기 위해서는 주변환경에 대한 정보가 필요하다. 반면에 로봇이 얻을 수 있는 데이터를 이용해서 지도를 만들기 위해서는 로봇이 자신의 위치가 어디에 있는지를 정확히 알아야 한다. 따라서 위치를 알 수 없으면 지도를 만들 수 없고, 반대로 지도가 없으면 위치를 알 수 없다. 이러한 문제를 풀기 위해서 지도의 생성과 위치 추정을 동시에 수행하는 것이 SLAM이다.

State estimation

State estimation은 로봇에 주어지는 입력과, 로봇의 센서로부터 얻어지는 데이터로부터 현재의 로봇의 위치인 state와 주변환경에 대한 지도를 추정 방법이다.

p(\mathbf{x}\mid \mathbf{z}, \mathbf{u})

위의 식은 기본적인 state estimation을 의미한다. \mathbf{x} 는 로봇의 위치 및 지도(주변의 land mark들의 위치)를 의미하는 vector이며, \mathbf{z} 는 로봇의 센서로부터 얻어지는 데이터로 observation이라고 부르며, \mathbf{u} 는 센서의 움직임을 제어하는 입력으로 control input이라고 부른다. state estimation은 이러한 control input과 observation의 데이터를 통해 현재의 위치와 지도를 추정한다.

bayes theorem

베이즈 정리는 확률론과 통계학에서 두 확률변수의 사전확률(prior)과 사후확률(posterior) 사이의 관계를 나타내는 정리이다.

P(A \mid B) = \frac{P(B \mid A)P(A)}{P(B)}

P(A) 는 A의 prior로, 사건 B에 대한 어떠한 정보를 알지 못하는 것을 의미한다. P(A \mid B) 는 B의 값이 주어진 경우 A의 posterior이다. P(B \mid A) 는 A가 주어졌을 때 B의 조건부 확률이다.

bayes 정리의 자세한 내용은 wiki를 참고한다.

Recursive bayes filter

위에서 설명한 state estimation은 bayes filter의 과정으로 설명할 수 있으며, 각 step의 state를 반복적으로 계산함으로써 계산할 수 있기 때문에 recursive bayes filter로 부른다. 전체적인 recursive bayes filter의 식은 다음과 같다.

\begin{aligned}
bel(x_t) &= p(x_t \mid z_{1:t},u_{1:t}) \\
       &= \eta p(z_t \mid x_t, z_{1:t-1}, u_{1:t})p(x_t \mid z_{1:t-1},u_{1:t}) \\
       &= \eta p(z_t \mid x_t)p(x_t \mid z_{1:t-1},u_{1:t}) \\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, z_{1:t-1}, u_{1:t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) dx_{t-1}\\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) dx_{t-1}\\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t-1}) dx_{t-1}\\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t}) bel(x_{t-1}) dx_{t-1}\\
\end{aligned}

위의 식은 recursive bayes filter를 유도하는 과정을 모두 표현하고 있기 때문에 다소 복잡해 보인다. 우선 전체적인 식을 이해하기 위해서 맨 처음과 맨 마지막 식만을 보면 다음과 같다.

\begin{aligned}
bel(x_t)  &= p(x_t \mid z_{1:t},u_{1:t}) \\
          &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t}) bel(x_{t-1}) dx_{t-1}\\
\end{aligned}

bel(x_t) 는 처음부터 현재까지의 observation( z )와 control input( u )을 알고 있을 때 현재 state( x_t )의 확률을 의미한다. 위의 식에서 bel(x_t) 의 식은 bel(x_{t-1}) 의 integral로 표현되어 있기 때문에 만약 p(z_t \mid x_t)p(x_t \mid x_{t-1}, u_t) 에 대한 정보를 알고 있다면 반복적인 계산을 통해 현재 state의 확률을 계산할 수 있음을 알 수 있다. 여기서 p(z_t \mid x_t) 는 현재의 state에서 센서 데이터의 확률인 observation model이며, p(x_t \mid x_{t-1}, u_t) 은 현재의 control input에 대해 이전 state에서 현재 state로의 update를 나타내는 motion model를 의미한다. 위의 식을 Recursive bayes filter라고 한다. Recursive bayes filter는 Kalman filter의 기본이 되는 식이다. 다음은 recursive bayes filter의 유도과정을 간단하게 살펴본다. 유도에 관심이 없고 전체적인 흐름만 보고자 한다면 다음 설명은 넘어가도 좋다.

Recursive bayes filter의 유도과정

\begin{aligned}
bel(x_t) &= p(x_t \mid z_{1:t},u_{1:t}) \\
\end{aligned}

control input과 observation을 알고 있을 때 현재 state의 확률을 의미하는 belief의 정의

\begin{aligned}
bel(x_t) &= p(x_t \mid z_{1:t},u_{1:t}) \\
       &= \eta p(z_t \mid x_t, z_{1:t-1}, u_{1:t})p(x_t \mid z_{1:t-1},u_{1:t}) \\
\end{aligned}

기본적인 bayes rule이다. 현재 시점 t의 observation은 현재의 state에서 얻어진 data이므로 따로 분리하여 위와 같이 정의한다. p(x_t \mid z_{1:t-1},u_{1:t}) 는 t시점까지의 control input, 그리고 t-1시점 까지의 observation을 알고 있을 때의 현재 시점 t의 state, p(z_t \mid x_t, z_{1:t-1}, u_{1:t}) 는 현재 state에서 얻어진 observation의 확률이다. \eta 는 normalize term이다.

\begin{aligned}
bel(x_t)
       &= \eta p(z_t \mid x_t, z_{1:t-1}, u_{1:t})p(x_t \mid z_{1:t-1},u_{1:t}) \\
       &= \eta p(z_t \mid x_t)p(x_t \mid z_{1:t-1},u_{1:t}) \\
\end{aligned}

Markov Assumption은 현재의 state는 바로 이전 state에 의해서만 영향을 받는다는 것이다. 즉 이전 state를 결정하기 위한 데이터들은 알지 못해도 이전 state만 알고 있다면 현재 state를 결정할 수 있다는 것이다. Markov assumtion에 의해 p(z_t \mid x_t, z_{1:t-1}, u_{1:t})p(z_t \mid x_t)로 표현 할 수 있다. 왜냐하면 현재의 observation은 현재의 state인 x_t에만 영향을 받으며, z_{1:t-1}u_{1:t}는 현재 state x_t에만 영향을 미치기 때문이다.

\begin{aligned}
bel(x_t)
       &= \eta p(z_t \mid x_t)p(x_t \mid z_{1:t-1},u_{1:t}) \\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, z_{1:t-1}, u_{1:t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) dx_{t-1}\\
\end{aligned}

다음 식은 total probability(전체확률) 법칙에 의해 위와같이 전개된다. 전체확률 법칙은 간단하게 표현하면 다음과 같다.

P(A) = \int_B P(A \mid B)P(B) dB

즉 A는 x_t 이며, B는 x_{t-1} 이다.

\begin{aligned}
bel(x_t)
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, z_{1:t-1}, u_{1:t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) dx_{t-1}\\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) dx_{t-1}\\
\end{aligned}

위 과정은 앞에서 설명한 Markov assumtion에 의해서 x_t 에 영향을 미치는 x_{t-1}u_t만 남기고 정리된다.

\begin{aligned}
bel(x_t)
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) dx_{t-1}\\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t-1}) dx_{t-1}\\
\end{aligned}

이 과정 또한 Markov assumption으로 p(x_{t-1} \mid z_{1:t-1}, u_{1:t}) 에서 u_t는 t-1시점의 state인 x_{t-1} 에 영향을 미치지 않기 때문에 제거 될 수 있다.

\begin{aligned}
bel(x_t)
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t})p(x_{t-1} \mid z_{1:t-1}, u_{1:t-1}) dx_{t-1}\\
       &= \eta p(z_t \mid x_t)\int_{x_{t-1}}p(x_t \mid x_{t-1}, u_{t}) bel(x_{t-1}) dx_{t-1}\\
\end{aligned}

따라서 위와 같은 과정을 통해 최종적으로 식은 위와같이 정리되며, recursive bayes filter의 식으로 정리된다.

본 글을 참조하실 때에는 출처 명시 부탁드립니다.




Pagination