블루투스에 대해서 - 채팅 프로젝트
블루투스 채팅에 대해서 소개합니다.
C++, C언어, C# 등의 언어에서 네트워크 프로그래밍을 다뤄보면, 소켓을 여는 원리랑 같습니다.
다만 그것보다는 복잡할 것 같습니다.
1. 디자인 패턴(Singleton)
먼저 소스코드를 소개하기에 앞서, 객체지향 프로그래밍의 패턴을 소개합니다.
public class Animal{ private static final Animal instance; private Animal(){ public Animal getInstance(){ if ( instance == null ) return new Animal(); return instance; } } |
싱글톤 패턴(Singleton) |
가볍게 디자인패턴을 다뤄봤습니다. 조금 더 복잡할지도 모르지만......
2. 블루투스
먼저 블루투스(Bluetooth)는 근거리 무선 통신 기술을 이야기 합니다.
거리는 15m 내외~100m 이내입니다. IEEE 802.15.1에 정의되어 있는 스팩입니다.
블루투스는 1대1 무선 통신을 위해 설계된 프로토콜입니다.
"위키피디아에 있는 블루투스 설명" 요약 | |||||||||||||||
블루투스는, 통신에 이용되는 전파의 강도를 클래스라는 개념으로 규정하고 있다.
IEEE에서는 규격명 IEEE 802.15.1으로 등재되어 있으나, 현재 블루투스는 Bluetooth Special Interest Group (SIG)을 통해 관리되고 있다. 이 그룹에는 전기통신, 컴퓨터, 네트워크, 가전 등의 분야의 30,000사 이상의 기업들이 멤버에 가입되어 있다.[3] 블루투스 SIG는 규격의 개발을 감시, 규격의 인증 프로그램의 관리 및 트레이드마크의 보호를 관장하고 있다. [4] 장비 제조사가 블루투스 장비로 인증을 받기 위해서는, SIG에서 제정한 표준 규격을 만족해야 한다. ~~~ [연혁] 1994년에 에릭슨의 사내 프로젝트로 개발 시작하였다.~ | |||||||||||||||
[프로파일] 블루투스는, 여러 종류의 장비로의 통신에 사용되는 규격인 이유로, 장비의 종류에 따라 규정되는 각각의 별도의 프로토콜이 존재한다. 이들 프로토콜의 사용법을 프로파일이라는 용어로 표준화하고 있다. 통신하고자 하는 장비와 장비 간에, 동일한 프로파일을 가지고 있는 경우에만, 그 프로파일을 이용한 통신이 가능하다. 대표적인 프로파일은 다음과 같은 것들이 있다.
| |||||||||||||||
[블루투스 사양] 블루투스 4.0 + LE 이 주제의 자세한 내용은 Bluetooth Low Energy 문서를 참고하십시오. Bluetooth SIG는 블루투스 사양서 버전 4.0(Bluetooth Smart)을 2010년 6월 30일에 채택하였다. 이 사양에는 클래식 블루투스(Classic Bluetooth), Bluetooth high speed 와 Bluetooth low energy 프로토콜이 포함되었다. Bluetooth high speed 는 Wi-Fi 를 바탕으로, 클래식 블루투스는 기존의 레거시 블루투스 프로토콜을 바탕으로 한다. 한편, 종래의 버전과 비교해 대폭적으로 소비전력을 낮춘 Bluetooth Low Energy 는, Bluetooth SIG 공개자료에 의하면, 버튼형 전지 1개만으로도 수년간 구동 가능하도록 되어 있다. 전송 속도는 1Mbps로, 데이터 패킷 사이즈가 8 - 27옥테드로 매우 작아졌다. 가전제품 등에 탑재된 센서와의 데이터 통신을 염두에 두고 만들어진 사양으로, 기존 3.0+HS과 방향성을 달리하여, 제품 제작자는 3.0+HS 및 4.0을 별도로 목적에 맞춰 채용하는 식이 되었다. 블루투스 SIG는 2013년 12월 블루투스 4.1의 새로운 기능을 발표했다[18]. 블루투스 4.1의 주요 특징은 다음과 같다. |
프로파일에 대한 자세한 내용은 공식 가이드에서 소개하고 있습니다. 해당 주제에서는 다루지 않습니다.
3. 연결을 위한 블루투스
프로그래밍으로 구현하기에 앞서서 가장 원초적인 이야기이지만, 블루투스 장치가 2대는 있어야 합니다.
하나는 Server 역할을 하기도 하면서, Client 역할을 할 수 있는 장치가 있어야 되겠습니다.
|
장치 모습(외형, 내부) |
장치를 살펴본 것처럼, 가장 중요한 건 디바이스가 블루투스를 지원하는지를 알아야 합니다.
블루투스 칩셋이 지원된다면, 연결 장치를 확인하게 될 것이고, 채널을 생성해야 합니다.
검색(Discovery) 등을 하게 될 것이며.
요약하면 다음과 같은 API 활동을 수행합니다.
Android 애플리케이션은 Bluetooth API를 사용하여 다음 작업을 수행할 수 있습니다. |
• 다른 블루투스 기기 스캔 |
4. 프로그래밍 구현 - 전략
프로그래밍 구현을 어떠한 순서로 하는지 소개합니다.
<!-- 블루투스 권한 획득 --> | ||
1. 메니페스트 환경 설정을 한다. 마시멜로우 버전 이상부터는 Wifi+Bluetooth+GPS 칩이 연결되어 있는 것으로 보입니다. 안드로이드 공식 메뉴얼과는 다르게 ACCESS_{}_LOCATION의 권한을 허용해줘야 합니다. | ||
private BluetoothAdapter mBluetoothAdapter; BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); | ||
2. 블루투스 장치가 지원하는지 확인한다. 장치가 지원되지 않는데 블루투스를 동작시켜봐야 의미가 없습니다. | ||
if (!mBluetoothAdapter.isEnabled()) { | ||
3. 블루투스 장치가 지원 된다면, 블루투스 어뎁터를 활성화하여 사용할 수 있도록 해야 합니다. mBluetoothAdapter.isEnabled()은 블루투스 어뎁터의 활성화 상태를 {True, False}로 반환합니다. | ||
private void checkBTPermissions(){ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, 1001); //Any number} Toast.LENGTH_SHORT).show(); | ||
4. 안드로이드 버전이 LOLLIPOP버전보다 클 때. 메니페스토(ACCESS_FINE_LOCATION, ACCESS_COURSE_LOCATION) 권한을 획득합니다. 롤리팝 이상 버전에서는 해당 코드를 넣어서 권한을 반드시 획득해야 합니다. (실제 구현에서는 이 다음에 위젯 등의 윤곽을 잡는 구현을 하게 됩니다.) | ||
(BluetoothClientService.java)
private void getPairedDevices() { | ||
5. 페어링된 장치 조회 페어링에는 크게 종류가 두 가지로 구성됩니다. 1. 페어링이 된 장치. 2. 페어링이 되지 않았지만, 페어링 대상이 되는 장치 | ||
| ||
페어링의 종류에는 크게 두 가지로 구성됩니다. | ||
(BluetoothClientService.java)
/** // (핵심 영역) private BroadcastReceiver mDiscoveryReceiver = new BroadcastReceiver() { ------------------------------------------------------------------------------------------------------
| ||
6. 신규 장치 찾기(수신) | ||
(MainActivity.java) private void initDeviceListDialog() {
| ||
7. 페어링 장치 - 화면 구현(5, 6번) - 여기까지 페어링에 관한 이야기입니다. 초기 페어링에 관한 이야기입니다. 연결에 대해서 소개합니다. | ||
(BluetoothClientService.java)
/** 블루투스 디바이스 인스턴스. 사용 가능한 상태로 만들어줘야 한다. final BluetoothStreamingHandler bluetoothStreamingHandler) { private void connectClient() { ---------------------------------------------------------------------------------------------- (MainActivity.java) private void connect(BluetoothDevice device) { private void addText(String text) { | ||
9. 연결에 관한 내용(연결, 메시지 송 수신에 대한 내용) | ||
public class BluetoothClientService {
| ||
10. 생성자 등 구현 |
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ){
case 1:
enableBluetooth();
return true;
case 2:
pairDialog();
return true;
case 3:
discoveryDialog();
return true;
case 4:
showCodeDlg();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void pairDialog() {
boolean connect = mClient.isConnection();
if (!connect && mClient.isEnabled()) {
getPairedDevices();
mDeviceListDialog.show();
} else {
Toast.makeText(getApplicationContext(), "블루투스 활성화 후 사용하세요.", Toast.LENGTH_SHORT).show();
mBTHandler.close();
}
}
private void discoveryDialog(){
if( mClient.isEnabled()) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
}
else
{
Toast.makeText(getApplicationContext(), "블루투스 활성화 후 사용하세요.", Toast.LENGTH_SHORT).show();
}
}
private void showCodeDlg() {
TextView codeView = new TextView(this);
codeView.setText( Html.fromHtml(readCode()) );
codeView.setMovementMethod(new ScrollingMovementMethod());
codeView.setBackgroundColor(Color.parseColor("#202020"));
new AlertDialog.Builder(this, android.R.style.Theme_Holo_Light_DialogWhenLarge)
.setView(codeView)
.setPositiveButton("OK", new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
}).show();
}
private String readCode() {
try {
InputStream is = getAssets().open("HC_06_Echo.txt");
int length = is.available();
byte[] buffer = new byte[length];
is.read(buffer);
is.close();
String code = new String(buffer);
buffer = null;
return code;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
private void checkBTEnabled(){
if ( mClient.isEnabled() ) {
initDeviceListDialog();
initWidget();
}
}
(윤곽 등을 잡는 구현의 예)
5. 공식 가이드에서 소개하는 내용(번외)
1. https://developer.android.com/guide/topics/connectivity/bluetooth.html
2. https://developer.android.com/samples/BluetoothChat/index.html
블루투스 공식 가이드에서 소개하고 있는 것은 BluetoothChat 프로젝트를 예제로서 소개하고 있습니다.
레퍼런스 읽는 것도 무척 중요합니다. "블루투스" 채팅 프로젝트(공식사이트 제공)를 아마 안드로이드 스튜디오로 동작시켜본다면 무척 소스코드가 복잡한 것은 물론이고 동작하지 않습니다.
예전 디바이스 기종으로 한다면, 동작할지도 모릅니다. (최신 안드로이드 스튜디오로 확인해본 결과)
- 빌드가 안 된다는 것이 아니라, 연결부터 채팅 동작이 안 되는 현상을 관찰할 수 있음.
- 아마 블루투스 레퍼런스를 선호하는 분들이 대체적으로 졸업작품 등을 준비하거나 하는 분들이 선호할 것으로 보입니다.
가이드에 있는 내용을 소개합니다. 해설도 넣었습니다.
앞서 소개된 내용을 소개합니다.
* 블루투스 권한
<manifest ... > |
애플리케이션 권한 선언에 대한 자세한 내용은 <uses-permission> 참조를 읽어보세요 |
* 블루투스 설정
블루투스를 사용하여 통신하려면 블루투스가 기기에서 지원되는지 확인하고, 지원되는 경우 활성화해야 합니다.
블루투스가 지원되지 않은 경우 블루투스 기능을 비활성화해야 합니다. 지원되는 블루투스가 비활성화된 경우 개발자는 사용자가 애플리케이션을 떠나지 않고 블루투스를 활성화하도록 요청할 수 있습니다. 이 설정은 BluetoothAdapter를 사용하여 2단계로 수행됩니다.
1.BluetoothAdapter 가져오기
모든 블루투스 액티비티를 위해 BluetoothAdapter가 필요합니다. BluetoothAdapter를 가져오려면 정적 getDefaultAdapter() 메서드를 호출합니다. 그러면 기기의 자체 블루투스 어댑터(블루투스 송수신 장치)를 나타내는 BluetoothAdapter가 반환됩니다. 전체 시스템에 대한 단일 블루투스 어댑터가 있고 애플리케이션이 해당 객체를 사용하여 상호작용할 수 있습니다. getDefaultAdapter()가 null을 반환하는 경우 기기는 블루투스를 지원하지 않습니다
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// Device does not support Bluetooth
}
2. 블루투스 활성화
이제 블루투스를 활성화해야 합니다. isEnabled()를 호출하여 블루투스가 현재 활성화되었는지 확인합니다. 이 메서드가 false를 반환하는 경우 블루투스는 비활성화됩니다. 블루투스 활성화를 요청하려면 ACTION_REQUEST_ENABLE 작업 인텐트를 사용하여 startActivityForResult()를 호출합니다. 그러면 (애플리케이션을 중지하지 않고) 시스템 설정을 통한 블루투스 활성화 요청이 발급됩니다. 예:
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
그림 1과 같이 블루투스를 활성화하기 위해 사용자 권한을 요청하는 대화상자가 표시됩니다. 사용자가 "Yes"를 선택하면 시스템이 블루투스를 활성화하기 시작하고 해당 프로세스가 완료(또는 실패)하면 포커스가 애플리케이션으로 돌아갑니다.
startActivityForResult()로 전달된 REQUEST_ENABLE_BT 상수는 시스템이 requestCode 매개변수로서 onActivityResult() 구현에서 개발자에게 다시 전달하는 지역적으로 정의된 정수(0보다 커야 함)입니다.
블루투스 활성화에 성공하면 액티비티가 onActivityResult() 콜백에서 RESULT_OK 결과 코드를 수신합니다. 오류 때문에 블루투스를 활성화하지 못한 경우(또는 사용자가 "No"를 선택한 경우) 결과 코드는 RESULT_CANCELED입니다.
protected void onActivityResult( int requestCode, int resultCode, Intent data ){
// startAcitivityForResult로 넘긴 값
// -> requestCode가 넘어온다는 이야기.
}
3. 기기 찾기
BluetoothAdapter를 사용하면 기기 검색을 통해 또는 페어링된(연결된) 기기의 목록을 쿼리하여 원격 블루투스 기기를 찾을 수 있습니다.
원격 기기와 처음으로 연결되면 페어링 요청이 자동으로 사용자에게 제공됩니다. 기기가 페어링되면 해당 기기에 대한 기본 정보(예: 기기 이름, 클래스 및 MAC 주소)는 저장되고 Bluetooth API를 사용하여 읽을 수 있습니다. 원격 기기에 대해 알려진 MAC 주소를 사용하면 (기기가 범위 내에 있다고 가정하여) 검색을 수행하지 않고 언제든지 연결을 시작할 수 있습니다.
페어링과 연결은 차이가 있습니다. 페어링은 두 기기가 서로의 존재를 알고 있고 인증에 사용할 수 있는 공유 링크 키를 가지고 있으며 서로 암호화된 연결을 설정할 수 있음을 의미합니다. 연결은 기기가 현재 RFCOMM 채널을 공유하고 있고 데이터를 서로 전송할 수 있음을 의미합니다. 현재 Android Bluetooth API는 RFCOMM 연결을 설정할 수 있기 전에 기기를 페어링하도록 요청합니다. (Bluetooth API와 암호화된 연결을 시작하면 페어링이 자동으로 수행됩니다.)
중요한 부분이어서 색깔팬으로 표기를 하였습니다.
3-1. 페어링된 기기 쿼리
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); |
페어링이 되어진 기기를 검색합니다. |
3-2. 기기 검색
기기 검색을 시작하려면 startDiscovery()를 호출합니다. 이는 비동기 프로세스이며 해당 메서드는 검색이 성공적으로 시작했는지 여부를 나타내는 부울을 즉시 반환합니다. 검색 프로세스는 일반적으로 12초 정도의 조회 스캔과, 블루투스 이름을 가져오는 검색된 각 기기의 페이지 스캔을 포함합니다.
애플리케이션은 검색된 각 기기에 대한 정보를 수신하기 위해 ACTION_FOUND 인텐트에 대한 BroadcastReceiver를 등록해야 합니다. 시스템은 각 기기에 대해 ACTION_FOUND 인텐트를 브로드캐스트합니다. 이 인텐트는 BluetoothDevice 및 BluetoothClass을 각각 포함하는 추가 필드 EXTRA_DEVICE 및 EXTRA_CLASS을 제공합니다. 예를 들어, 기기를 검색할 때 브로드캐스트를 처리하도록 등록할 수 있는 방법은 다음과 같습니다.
// Create a BroadcastReceiver for ACTION_FOUND |
기기 검색 |
연결을 시작하기 위해 BluetoothDevice 객체에서 필요한 것은 MAC 주소입니다. 이 예시에서 MAC 주소는 사용자에게 표시되는 ArrayAdapter의 일부로 저장됩니다. MAC 주소는 연결을 시작하도록 나중에 추출할 수 있습니다. 연결 생성에 대한 자세한 내용은 기기 연결 섹션을 참조하세요.
주의: 기기 검색은 리소스 사용량이 많은 블루투스 어댑터 프로시저입니다. 연결할 기기를 검색한 경우 연결을 시도하기 전에 항상 cancelDiscovery()
를 사용하여 검색을 중단해야 합니다. 또한 이미 기기와 연결 중인 경우 검색은 연결에 사용 가능한 대역폭을 상당히 줄일 수 있으므로 연결 중에 검색을 해서는 안 됩니다.
4. 검색 기능 활성화
로컬 기기를 다른 기기가 검색할 수 있게 하려면 ACTION_REQUEST_DISCOVERABLE 작업 인텐트를 사용하여 startActivityForResult(Intent, int)을 호출합니다. 그러면 (애플리케이션을 중지하지 않고) 시스템 설정을 통한 검색 가능 모드 활성화 요청이 발급됩니다. 기본적으로 기기가 120초 동안 검색 가능하게 됩니다. EXTRA_DISCOVERABLE_DURATION 인텐트 엑스트라를 추가하여 다른 기간을 정의할 수 있습니다. 앱이 설정할 수 있는 최대 기간은 3600초이며 값이 0인 경우 기기가 항상 검색 가능합니다. 0 미만 또는 3600 초과 값은 120초로 자동 설정됩니다. 예를 들어, 다음 스니펫은 기간을 300으로 설정했습니다.
Intent discoverableIntent = new |
그림 2와 같이 기기를 검색 가능하게 하는 사용자 권한을 요청하는 대화상자가 표시됩니다. 사용자가 "Yes"를 선택하면 기기는 지정된 시간 동안 검색 가능하게 됩니다. 그러면 액티비티가 onActivityResult()) 콜백에 대한 호출을 수신하고, 결과 코드는 기기가 검색 가능한 기간과 동일합니다. 사용자가 "No"를 선택하거나 오류가 발생한 경우 결과 코드는 RESULT_CANCELED입니다.
참고: 기기에서 블루투스를 활성화하지 않은 경우 기기 검색 기능을 활성화하면 블루투스가 자동으로 활성화됩니다.
기기는 할당된 시간 동안 검색 가능 모드를 유지합니다. 검색 가능 모드 변경 알림을 받으려면 ACTION_SCAN_MODE_CHANGED
인텐트에 대해 BroadcastReceiver를 등록할 수 있습니다. 이는 새로운 스캔 모드와 이전 스캔 모드를 각각 알려주는 추가 필드 EXTRA_SCAN_MODE
및 EXTRA_PREVIOUS_SCAN_MODE
를 포함합니다. 각각에 대해 가능한 값은 SCAN_MODE_CONNECTABLE_DISCOVERABLE
, SCAN_MODE_CONNECTABLE
또는 SCAN_MODE_NONE
이며, 각 값은 기기가 검색 모드이거나, 검색 모드는 아니지만 연결을 수신할 수 있거나, 검색 모드도 아니고 연결도 수신할 수 없음을 나타냅니다.
원격 기기에 대한 연결을 시작하려면 기기 검색 기능을 활성화할 필요가 없습니다. 원격 기기는 연결을 시작하기 전에 기기를 검색할 수 있어야 하므로 들어오는 연결을 수락하는 서버 소켓을 애플리케이션이 호스팅하려는 경우에만 검색 기능 활성화가 필요합니다.
5. 기기 연결
매우 중요합니다. PIN 코드 등에 관한 문제부터 시작해서 소켓을 생성하는데 필요한 정보 등을 가지고 있습니다.
문제는 이렇게 창을 띄울 수 있냐는 겁니다. 아마도 가이드에는 제시되어 있지 않습니다.
private void connectClient() {
try {
mBluetoothSocket = mConnectedDevice.createRfcommSocketToServiceRecord(mUUID);
// mConnectedDevice.{setPin??}
// ......등.....(BluetoothDevice mConnectedDevice가 가지고 있는 메서드 등을 참고하기 바람.)
mBluetoothAdapter.cancelDiscovery();
mBluetoothSocket.connect();
소켓을 열때, PIN 창이 뜰 것으로 보입니다.
기기연결에 필요한 구성요소를 크게 3가지로 추려보면,
ConnectThread,
AcceptThread,
ConnectedThread 3가지로 살펴볼 수 있습니다.
그런데, 정작 앞서 예제에서는 3가지 쓰레드로 구현하지 않았습니다.
private void connectClient() {
try {
mBluetoothSocket = mConnectedDevice.createRfcommSocketToServiceRecord(mUUID);
} catch (IOException e) {
close();
e.printStackTrace();
mBluetoothStreamingHandler.onError(e);
return;
}
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mBluetoothAdapter.cancelDiscovery();
mBluetoothSocket.connect();
manageConnectedSocket(mBluetoothSocket);
callConnectedHandlerEvent();
mReadExecutor.execute(mReadRunnable);
} catch (final IOException e) {
close();
e.printStackTrace();
mMainHandler.post(new Runnable() {
@Override
public void run() {
mBluetoothStreamingHandler.onError(e);
}
});
mIsConnection.set(false);
try {
mBluetoothSocket.close();
} catch (Exception ec) {
ec.printStackTrace();
}
}
}
});
가이드에서 제시하는 코드를 살펴보면.
private class ConnectThread extends Thread { public ConnectThread(BluetoothDevice device) { // Get a BluetoothSocket to connect with the given BluetoothDevice public void run() { try { // Do work to manage the connection (in a separate thread) /** Will cancel an in-progress connection, and close the socket */ |
1. 블루투스 연결을 시작하는 스레드의 기본적인 예입니다. |
private class AcceptThread extends Thread { |
2. 연결을 수락하는 서버 구성 요소에 대한 간단한 스레드입니다. |
private class ConnectedThread extends Thread { |
3. 연결 관리 |
bluetoothChat 소스코드의 살펴보면,
(BluetoothChatService.java)
/**
* This class does all the work for setting up and managing Bluetooth
* connections with other devices. It has a thread that listens for
* incoming connections, a thread for connecting with a device, and a
* thread for performing data transmissions when connected.
*/
public class BluetoothChatService {
// Debugging
private static final String TAG = "BluetoothChatService";
// Name for the SDP record when creating server socket
private static final String NAME_SECURE = "BluetoothChatSecure";
private static final String NAME_INSECURE = "BluetoothChatInsecure";
// Unique UUID for this application
private static final UUID MY_UUID_SECURE =
UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
private static final UUID MY_UUID_INSECURE =
UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66");
// Member fields
private final BluetoothAdapter mAdapter;
private final Handler mHandler;
private AcceptThread mSecureAcceptThread;
private AcceptThread mInsecureAcceptThread;
private ConnectThread mConnectThread;
private ConnectedThread mConnectedThread;
private int mState;
private int mNewState;
// Constants that indicate the current connection state
public static final int STATE_NONE = 0; // we're doing nothing
public static final int STATE_LISTEN = 1; // now listening for incoming connections
public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
public static final int STATE_CONNECTED = 3; // now connected to a remote device
/**
* Constructor. Prepares a new BluetoothChat session.
*
* @param context The UI Activity Context
* @param handler A Handler to send messages back to the UI Activity
*/
public BluetoothChatService(Context context, Handler handler) {
mAdapter = BluetoothAdapter.getDefaultAdapter();
mState = STATE_NONE;
mNewState = mState;
mHandler = handler;
}
.......
/**
* Start the ConnectThread to initiate a connection to a remote device.
*
* @param device The BluetoothDevice to connect
* @param secure Socket Security type - Secure (true) , Insecure (false)
*/
public synchronized void connect(BluetoothDevice device, boolean secure) {
Log.d(TAG, "connect to: " + device);
// Cancel any thread attempting to make a connection
if (mState == STATE_CONNECTING) {
if (mConnectThread != null) {
mConnectThread.cancel(); // 연결 끊기
mConnectThread = null;
}
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel(); // 연결 끊기
mConnectedThread = null;
}
// Start the thread to connect with the given device
mConnectThread = new ConnectThread(device, secure);
mConnectThread.start();
// Update UI title
updateUserInterfaceTitle();
}
* Start the ConnectedThread to begin managing a Bluetooth connection
*
* @param socket The BluetoothSocket on which the connection was made
* @param device The BluetoothDevice that has been connected
*/
public synchronized void connected(BluetoothSocket socket, BluetoothDevice
device, final String socketType) {
Log.d(TAG, "connected, Socket Type:" + socketType);
// Cancel the thread that completed the connection
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Cancel the accept thread because we only want to connect to one device
if (mSecureAcceptThread != null) {
mSecureAcceptThread.cancel();
mSecureAcceptThread = null;
}
if (mInsecureAcceptThread != null) {
mInsecureAcceptThread.cancel();
mInsecureAcceptThread = null;
}
// Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket, socketType);
mConnectedThread.start();
// Send the name of the connected device back to the UI Activity
Message msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(Constants.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg);
// Update UI title
updateUserInterfaceTitle();
}
/**
* Stop all threads
*/
public synchronized void stop() {
Log.d(TAG, "stop");
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
if (mSecureAcceptThread != null) {
mSecureAcceptThread.cancel();
mSecureAcceptThread = null;
}
if (mInsecureAcceptThread != null) {
mInsecureAcceptThread.cancel();
mInsecureAcceptThread = null;
}
mState = STATE_NONE;
// Update UI title
updateUserInterfaceTitle();
}
/**
* Write to the ConnectedThread in an unsynchronized manner
*
* @param out The bytes to write
* @see ConnectedThread#write(byte[])
*/
public void write(byte[] out) {
// Create temporary object
ConnectedThread r;
// Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED) return;
r = mConnectedThread;
}
// Perform the write unsynchronized
r.write(out);
}
/**
* Indicate that the connection attempt failed and notify the UI Activity.
*/
private void connectionFailed() {
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(Constants.TOAST, "Unable to connect device");
msg.setData(bundle);
mHandler.sendMessage(msg);
mState = STATE_NONE;
// Update UI title
updateUserInterfaceTitle();
// Start the service over to restart listening mode
BluetoothChatService.this.start();
}
/**
* Indicate that the connection was lost and notify the UI Activity.
*/
private void connectionLost() {
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(Constants.TOAST, "Device connection was lost");
msg.setData(bundle);
mHandler.sendMessage(msg);
mState = STATE_NONE;
// Update UI title
updateUserInterfaceTitle();
// Start the service over to restart listening mode
BluetoothChatService.this.start();
}
/**
* This thread runs while listening for incoming connections. It behaves
* like a server-side client. It runs until a connection is accepted
* (or until cancelled).
*/
private class AcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
private String mSocketType;
public AcceptThread(boolean secure) {
BluetoothServerSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure";
// Create a new listening server socket
try {
if (secure) {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,
MY_UUID_SECURE);
} else {
tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord(
NAME_INSECURE, MY_UUID_INSECURE);
}
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e);
}
mmServerSocket = tmp;
mState = STATE_LISTEN;
}
public void run() {
Log.d(TAG, "Socket Type: " + mSocketType +
"BEGIN mAcceptThread" + this);
setName("AcceptThread" + mSocketType);
BluetoothSocket socket = null;
// Listen to the server socket if we're not connected
while (mState != STATE_CONNECTED) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e);
break;
}
// If a connection was accepted
if (socket != null) {
synchronized (BluetoothChatService.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// Situation normal. Start the connected thread.
connected(socket, socket.getRemoteDevice(),
mSocketType);
break;
case STATE_NONE:
case STATE_CONNECTED:
// Either not ready or already connected. Terminate new socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType);
}
public void cancel() {
Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
}
}
}
/**
* This thread runs while attempting to make an outgoing connection
* with a device. It runs straight through; the connection either
* succeeds or fails.
*/
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private String mSocketType;
public ConnectThread(BluetoothDevice device, boolean secure) {
mmDevice = device;
BluetoothSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure";
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
if (secure) {
tmp = device.createRfcommSocketToServiceRecord(
MY_UUID_SECURE);
} else {
tmp = device.createInsecureRfcommSocketToServiceRecord(
MY_UUID_INSECURE);
}
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
}
mmSocket = tmp;
mState = STATE_CONNECTING;
}
public void run() {
Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType);
setName("ConnectThread" + mSocketType);
// Always cancel discovery because it will slow down a connection
mAdapter.cancelDiscovery();
// Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
mmSocket.connect();
} catch (IOException e) {
// Close the socket
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() " + mSocketType +
" socket during connection failure", e2);
}
connectionFailed();
return;
}
// Reset the ConnectThread because we're done
synchronized (BluetoothChatService.this) {
mConnectThread = null;
}
// Start the connected thread
connected(mmSocket, mmDevice, mSocketType);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
}
}
}
/**
* This thread runs during a connection with a remote device.
* It handles all incoming and outgoing transmissions.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket, String socketType) {
Log.d(TAG, "create ConnectedThread: " + socketType);
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
mState = STATE_CONNECTED;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// Keep listening to the InputStream while connected
while (mState == STATE_CONNECTED) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI Activity
mHandler.obtainMessage(Constants.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost();
break;
}
}
}
/**
* Write to the connected OutStream.
*
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// Share the sent message back to the UI Activity
mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
}
..........................................................
(BluetoothChatFragment.java)
public class BluetoothChatFragment extends Fragment {
private static final int REQUEST_ENABLE_BT = 3;
// Intent request codes
private static final int REQUEST_CONNECT_DEVICE_SECURE = 1;
private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2;
private static final int REQUEST_ENABLE_BT = 3;
// Layout Views
private ListView mConversationView;
private EditText mOutEditText;
private Button mSendButton;
/**
* Name of the connected device
*/
private String mConnectedDeviceName = null;
/**
* Array adapter for the conversation thread
*/
private ArrayAdapter<String> mConversationArrayAdapter;
/**
* String buffer for outgoing messages
*/
private StringBuffer mOutStringBuffer;
/**
* Local Bluetooth adapter
*/
private BluetoothAdapter mBluetoothAdapter = null;
/**
* Member object for the chat services
*/
private BluetoothChatService mChatService = null;
...........................
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
mConversationView = (ListView) view.findViewById(R.id.in);
mOutEditText = (EditText) view.findViewById(R.id.edit_text_out);
mSendButton = (Button) view.findViewById(R.id.button_send);
}
private void setupChat() {
Log.d(TAG, "setupChat()");
// Initialize the array adapter for the conversation thread
mConversationArrayAdapter = new ArrayAdapter<String>(getActivity(), R.layout.message);
mConversationView.setAdapter(mConversationArrayAdapter);
// Initialize the compose field with a listener for the return key
mOutEditText.setOnEditorActionListener(mWriteListener);
// Initialize the send button with a listener that for click events
mSendButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Send a message using content of the edit text widget
View view = getView();
if (null != view) {
TextView textView = (TextView) view.findViewById(R.id.edit_text_out);
String message = textView.getText().toString();
sendMessage(message);
}
}
});
// Initialize the BluetoothChatService to perform bluetooth connections
mChatService = new BluetoothChatService(getActivity(), mHandler);
// Initialize the buffer for outgoing messages
mOutStringBuffer = new StringBuffer("");
}
.......
.......
switch (requestCode) {
case REQUEST_CONNECT_DEVICE_SECURE:
// When DeviceListActivity returns with a device to connect
if (resultCode == Activity.RESULT_OK) {
connectDevice(data, true);
}
break;
case REQUEST_CONNECT_DEVICE_INSECURE:
// When DeviceListActivity returns with a device to connect
if (resultCode == Activity.RESULT_OK) {
connectDevice(data, false);
}
break;
case REQUEST_ENABLE_BT:
// When the request to enable Bluetooth returns
if (resultCode == Activity.RESULT_OK) {
// Bluetooth is now enabled, so set up a chat session
setupChat();
} else {
// User did not enable Bluetooth or an error occurred
Log.d(TAG, "BT not enabled");
Toast.makeText(getActivity(), R.string.bt_not_enabled_leaving,
Toast.LENGTH_SHORT).show();
getActivity().finish();
}
}
}
/**
* Establish connection with other device
*
* @param data An {@link Intent} with {@link DeviceListActivity#EXTRA_DEVICE_ADDRESS} extra.
* @param secure Socket Security type - Secure (true) , Insecure (false)
*/
private void connectDevice(Intent data, boolean secure) {
// Get the device MAC address
String address = data.getExtras()
.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
// Get the BluetoothDevice object
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
// Attempt to connect to the device
mChatService.connect(device, secure);
}
...... // 일부분.
중요한 부분을 주로 압축해서 올려봤습니다.
그러나 얻을 수 있는 부분은 연결에 관한 secure모드 등에 대한 명세 등 살펴볼 수 있습니다.
동작을 해보면, 채팅 기능이 되진 않을 것입니다.
다만, BluetoothChat 프로젝트에서는 블루투스 연결에 관한 중요한 이야기를 다루고 있습니다.
6. 블루투스 채팅 - 구현/동작 영상
|
동작 영상1) |
동작 영상2) |
7. 소스코드
<?xml version="1.0" encoding="utf-8"?> |
AndroidManifest.xml |
<?xml version="1.0" encoding="utf-8"?> android:orientation="vertical"> |
activity_main.xml |
<?xml version="1.0" encoding="utf-8"?> |
device_items.xml |
package com.localhost.kr.bluetoothchat; Toast.LENGTH_SHORT).show(); |
MainActivity.java |
|
BluetoothClientService.java |
[소스코드]
8. 참고 자료
1. https://developer.android.com/guide/topics/connectivity/bluetooth.html, 안드로이드 블루투스 가이드
- 안드로이드 공식 블루투스 가이드입니다. 최신 LE(Low Energy) 등에 대한 이야기도 소개하고 있습니다.
공식 사이트입니다.
2. https://developer.android.com/samples/BluetoothChat/index.html, 블루투스 채트 - 셈플
3. https://stackoverflow.com/questions/36163751/android-marshmallow-6-0-1-bluetooth-scan-returning-no-results, Android Marshmallow 6.0.1 Bluetooth Scan Returning No Results
4. http://www.hardcopyworld.com/ngine/android/index.php/archives/154, 블루투스 통신용 앱 구현하기 소스코드 – BluetoothChat
5. https://github.com/pablobuenaposada/arduino-HC-06/tree/master/Android, 블루투스 통신용 앱 구현하기 소스코드(Github)
* 메니페스트에 새 창 추가하는 방법
<!-- 여기서부터 추가 -->
<activity android:name="bluetooth.DeviceListActivity" android:theme="@android:style/Theme.DeviceDefault.Dialog">
<!-- 여기까지 추가 -->
6. http://dev.re.kr/39, 안드로이드에서 시리얼 블루투스 디바이스 통신을 쉽게 하기 위한 클라이언트 클래스.
* 블루투스 활성화하지 않고 해당 소스코드를 실행하였을 때, 오류 발생.
작업명 | 해당 영역 | 내용 | 비고 |
menu/main.xml | | // (mainActivity.java) @Override ~~ public boolean onOptionsItemSelected(MenuItem item) { .... default: | 제거 (수정자: rabbit.white) |
src/BluetoothSerialClient.java | 39 Line | private OnBluetoothEnabledListener mOnBluetoothUpListener; | 삭제 (수정자: rabbit.white) |
src/BluetoothSerialClient.java | 300~400 Line 400~403 Line | public static class BluetoothUpActivity extends Activity { ~ public static interface onBluetoothEnabledListener { public void onBluetoothEnabled(boolean success); | 삭제 (수정자: rabbit.white) |
src/BluetoothSerialClient.java | 95~103 Line | mOnBluetoothUpListener = onBluetoothEnabledListener; -> Intent intent = new Intent(context, BluetoothUpActivity.class); // 오류 발생함. | 삭제 (수정자: rabbit.white) |
src/MainActivity.java | | private void enableBluetooth() { | 변경/추가 (수정자: rabbit.white) |
src/MainActivity.java | 313~326 Line | private void pairDialog() 변경 // if ( mClient.isEnabled() && !connect ) -> pairDialog() 호출 | 변경 (수정자: rabbit.white) |
res/values/strings.xml | 6~8 Line | <string name="action_connect">Connect</string> | 제거 (수정자: rabbit.white) |
src/MainActivity.java | initWidget(); // 2개로 줄임. | 변경 (수정자: rabbit.white) | |
// 이외에 다수 수정을 많이 하였음. |
9. https://www.youtube.com/playlist?list=PLgCYzUzKIBE8KHMzpp6JITZ2JxTgWqDH2, How to Use Bluetooth in Android Studio, Mitch Tabian, Youtube, 2017. 4. 18.
- [구현 관련 영상]
Bluetooth Tutorial - How to Pair, 2:37분(퍼미션에 관한 이야기 - 매니페스트)
-> checkBTPermissions(), 롤리팝 LOLLIPOP SDK 버전 등에 대한 설명.
10. https://ko.wikipedia.org/wiki/%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4, 블루투스, 위키백과
'공부 > 안드로이드(Android)' 카테고리의 다른 글
자바 유틸리티를 활용한 외부DB 로그인 - 구현하기 (0) | 2017.11.27 |
---|---|
다운로드, 프로그래스 진행창 - WebView(웹뷰) 기반 (0) | 2017.11.26 |
다이얼로그, 메뉴 - AlertDialog 구현, 서브 메뉴(2) (0) | 2017.11.10 |
메뉴 - 서브 메뉴 생성(Sub Menu) (0) | 2017.11.10 |
메뉴 - 만들기 소개 - 2 - (컨텍스트 메뉴) (0) | 2017.11.09 |