BLE를 이해하기 앞서, 블루투스 연결에 대한 기본을 정리한다.
블루투스를 연결하기 위해서는 Device가 블루투스 사용 가능한지 확인이 필요하고,
사용이 가능하다면 다른 블루투스를 검색한다.
원하는 블루투스 기기가 보이면 페어링을 시도한다.
그리고 데이터를 교환한다.
- 블루투스 가능 여부 확인
- 블루투스 기기 검색
- 데이터 교환
크게 이렇게 3가지 동작으로 되며, 그 내부에 페어링된 기기를 재 검색하지 않고 MAC주소로만 근처에 있을 경우 바로 페어링 되도록 하는 부가적인 역할 등 다양한 세부 조작이 필요하다.
블루투스 키보드의 키 레이아웃을 커스텀한 이후 블루투스를 만진적이 없는데,
이렇게 다시 마주한다.
블루투스를 사용하는 목적은 바로 무선으로 데이터를 교환하는 것이 목적이다.
데이터를 교환한다는 것은 쉽게 음악을 재생하면 블루투스 기기로 동일한 음악이 재생되도록 하는 것이다.
블루투스는 클래식 블루투스(일반)과 BLE(Bluetooth Low Energy)가 있다.
클래식 블루투스는 Android기기 간 스트리밍 및 통신을 하는 배터리를 많이 사용하는 작업에 적합한 옵션이다.
전력 요구사항이 낮은 BLE는 Android 4.3(API 18)부터 지원한다.
난 3가지를 생각했지만, Android문서에서는 블루투스 통신에 필요한 4가지 주요 작업을 다음과 같이 말한다.
- 블루투스 설정
- 로컬 영역에서 페어링 되었거나 사용 가능한 기기 찾기
- 기기 연결
- 기기간 데이터 전송
내용은 동일하다.
블루투스 기기가 서로 데이터를 통신하려면?
페어링 프로세스를 사용해 통신 채널을 형성한다. 벌써부터 뭔가 말이 어렵다. 페어링 프로세스라는 말은 연결을 뜻하고, 연결하기 위해서 서로의 채널을 맞추는 것을 뜻한다. 내가 KBS를 보고 싶으면 KBS채널을 틀고, KBS도 KBS채널에 방송을 송출하는 것처럼, 서로의 채널을 맞춰야 영상을 보내고, 또 수신하는 것이 가능해진다라는 말이다.
하지만 TV처럼 누구나 보는 것이 페어링 요청을 수락하면 서로 보안 키를 교환한다.
보안 키는 나중에 재사용이 가능하도록 캐싱한다.
이 과정(세션)이 완료되면 연결해준 채널을 해제한다.
권한
블루투스를 사용하려면 두 개의 권한을 선언해야 한다.
첫 번째는 BLUETOOTH권한이며,
두 번째는 ACCESS_FINE_LOCATION이다.
BLUETOOTH권한은 연결 요청, 연결 수락 및 데이터 전송과 같은 블루투스 통신을 수행하는데 사용된다.
ACCESS_FINE_LOCATION은 블루투스 스캔시 사용자 위치에 대한 정보를 수집하기 때문이다.
<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>
프로필 작업이라는 것이 있는데, 이것은 브루투스 기반 통신에 대한 인터페이스 사양이다.
예로 헤드폰을 연결하기 위해서 서로 약속된 Hands-Free라는 프로필을 지원해야 연결이 가능하다.
- 헤드셋
BluetoothHeadset클래스라고 블루투스 헤드셋 서비스를 제어하는 프록시이다. 이 곳에 블루투스 헤드셋 및 핸즈프리(v1.5) 프로필이 포함되어 있다.
- A2DP(Advanced Audio Distribution Profile)
블루투스 연결을 통햇 고품질 오디오가 기기 간 스트리밍할 수 있도록 한다. BluetoothA2dp클래스를 사용.
- 의료기기
Android4.0(API 14)부터 HDP(블루투스 의료 기기 프로필)지원, 블루투스를 사용해서 의료기기를 사용한다.
예를 들어 심박 측정기, 혈압계, 체온계, 체중계와 통신하는 앱을 만들 수 있다.
블루투스 헤드셋(프록시) 연결 예시
BluetoothProfile.ServiceListener로 설정한다. 이 리스너가 서비스에 연결되거나 끊긴경우 BluetoothProfile IPC에 알린다.
getProfileProxy() 함수로 프로필과 연결된 프로필 프록시 객체와의 연결을 설정한다. (Web사용시 getWebSetting() 사용해서 설정하듯이)
onServiceConnected() 에서 프로필 프록시 객체에 대한 핸들을 가져온다.
위의 과정은 뭔가 프록시, 핸들 어려운 듯 보이지만, 그저 헤드셋을 블루투스로 연결하고 헤드셋의 현재 상태를 실시간 감지하는 것으로 이해하면 된다.
var bluetoothHeadset: BluetoothHeadset? = null
// Get the default adapter
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
private val profileListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = proxy as BluetoothHeadset
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = null
}
}
}
// Establish connection to the proxy.
bluetoothAdapter?.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET)
// ... call functions on bluetoothHeadset
// Close proxy connection after use.
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)
bluetoothHeadset null인스턴스로 만든 뒤
bluetoothAdpater객체를 만든다.
profileListener를 정의해준 후 adapter에 장착한다.
bluetoothHeadset을 수신하면 profileListener에서 객체를 초기화한다.
의료기기 프로필
BluetoothHealth API로 의료 기기와 통신하는 앱을 만들 수 있다.
BluetoothHealth, BluetoothHealthCallback, BluetoothHealthAppConfiguration
BluetoothHealthAppConfiguration객체에서 의료 데이터를 수신한다.
블루투스 설정
이 모든 것을 하려면 사실 블루투스가 되는지 안되는지 확인이 필요하다.
BluetoothAdapter를 통해서 알 수 있다. BluetoothAdapter를 가져오려면 정적 getDefaultAdapter를 호출한다.
getDefaultAdapter가 null을 반환하면 해당 기기는 블루투스를 지원하지 않는 것이다.
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
// Device doesn't support Bluetooth
}
블루투스가 현재 디바이스에서 활성화 되었는지 확인하기 위해서 isEnabled()함수를 사용하여 확인해주고,
활성화되어 있지 않다면 사용자에게 요청해주어야 한다.
if (bluetoothAdapter?.isEnabled == false) {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
또 BluetoothAdapter로 기기를 찾을 수 있다(:Discover)
기기 이름, 클래스 및 MAC주소를 공유 받는다.
페어링과 연결은 차이점이 있다.
페어링 : 두 기기가 서로의 존재를 알고, 인증에 사용 가능한 공유 링크 키를 가지고 있고, 서로 암호화된 연결을 설정할 수 있음
연결 : 두 기기가 현재 RFCOMM채널을 공유하고 있고 데이터를 서로 전송할 수 있다. 현재 Android Bluetooth API는 RFCOMM연결을 하기 전에 기기를 페어링하도록 요청한다.
기기를 검색하기 전에 먼저 원하던 기기가 페어링되었는지 확인하는 것이 좋다.
그렇게 하려면 getBondedDevices()를 호출한다.
이렇게하면 BluetoothDevice객체가 반환 된다.
val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
val deviceName = device.name
val deviceHardwareAddress = device.address // MAC address
}
MAC주소만 있다면 연결이 된다.
난 이 부분이 잘 이해가 안된다. MAC주소만 있으면 연결이 된다니.. 테스트를 통해서 알아보겠다.
기기검색
startDiscovery()를 호출하여 검색하며, 이 프로세스는 비동기식이고 검색 성공 여부를 Boolean으로 반환한다.
검색은 약 12초 정도 스캔하고 검색된 각 기기의 페이지 스캔을 통해 블루투스 이름을 가져오는 과정이 포함된다.
각 기기에 대한 정보를 수집하려면 ACTION_FOUNDED인텐트에 대한 BroadcastReceiver를 등록해야한다.
인텐트에는 ExtraField에 EXTRA_DEVICE, EXTRA_CLASS를 포함시킨다.
override fun onCreate(savedInstanceState: Bundle?) {
...
// Register for broadcasts when a device is discovered.
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
registerReceiver(receiver, filter)
}
// Create a BroadcastReceiver for ACTION_FOUND.
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action: String = intent.action
when(action) {
BluetoothDevice.ACTION_FOUND -> {
// Discovery has found a device. Get the BluetoothDevice
// object and its info from the Intent.
val device: BluetoothDevice =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
val deviceName = device.name
val deviceHardwareAddress = device.address // MAC address
}
}
}
}
override fun onDestroy() {
super.onDestroy()
...
// Don't forget to unregister the ACTION_FOUND receiver.
unregisterReceiver(receiver)
}
검색 되도록 만들기
로컬 기기를 다른 기기가 검색할 수 있도록 하는것, ACTION_REQUEST_DISCOVERABLE인텐트를 사용한다.
기본적으로 120초, 2분간 기기가 검색 가능한 상태가 된다. 최대 3600초(1시간)으로 설정할 수 있다. 그렇게 하려면 EXTRA_DISCOVERABLE_DURATION엑스트라로 설정한다.
val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivity(discoverableIntent)
putExtra로 BluetoothAdapter.EXTRA_DISCOVERALBE_DURATION, 300)에 300초(5분)을 설정한 것
기기연결
여기부터 이제 블루투스의 메인이다. 두 기기를 연결하려면 서버용도와 클라이언트 용도 모두 구현되어야 한다. 양쪽 중 하나는 서버 소켓을 열고 다른 기기는 해당 서버 기기의 MAC주소를 사용하여 연결을 시작해야 한다.
서버는 데이터를 가진 곳, 클라이언트는 데이터를 가져가려는 곳이라고 이해하면 된다. 즉 서버의 데이터를 사용하기 위해서 클라이언트는 서버가 어디있는지 알아야 하고, 서버는 클라이언트들이 데이터를 사용할 수 있도록 한다.
그 통로의 문을 '소켓'이라고 이해한다.
클라이언트는 서버의 RFCOMM채널을 열 때 소켓 정보를 제공하며, 서버는 수신되는 연결을 수락할 때 소켓 정보를 받는다. 서로 동일한 RFCOMM채널에 연결된 BluetoothSocket이 있는 경우 연결된 것으로 간주한다.
두 기기를 연결하려면 한 기기는 BluetoothServerSocket을 제공하여 서버형태로 작동해야 한다. 서버 소켓의 목적은 들어오는 연결 요청을 수신 대기하고 수락 시 연결된 BluetoothSocket을 제공한다. listenUsingRfcommWithServiceRecord()를 호출하여 BluetoothServerSocket을 가져온다.
accept()하면 원격 기기가 이 수신 대기 서버 소켓에 등록된 것과 일치하는 UUID를 사용하여 연결 요청을 보내는 경우에만 연결이 수락된다. 연결 성공시 BluetoothSocket을 반환한다.
BluetoothSocket을 반환 받은 뒤에 BluetoothServerSocket을 닫아준다.
연결관리
기기를 성공적으로 연결하면 BluetoothSocket이 생기고, 이 BluetoothSocket으로 정보를 공유할 수 있다.
getInputStream, getOutputStream으로 소켓을 통해 전송을 처리하는 InputStream 및 OutputStream을 가져온다.
read(byte[]), write(byte[])을 사용하여 스트림에 데이터를 읽고 쓴다.
Bluetooth API는 android.bluetooth package에 있다. 클래스 및 인터페이스에 대한 명세.
- BluetoothAdapter : 로컬 블루투스 송수신 장치를 나타낸다. 블루투스 상호작용을 위한 진입점이고, 다른 블루투스 기기를 검색하고 연결된 기기 목록을 쿼리하고 알려진 MAC주소로 BluetoothDevice를 인스턴스화 한다. 또 다른 기기로부터 통신을 수신 대기하는 BluetoothServerSocket을 만들 수 있다.
- BluetoothDevice : 원격 블루투스 기기를 나타낸다. 이를 사용하여 BluetoothSocket을 통해 원격 기기와의 연결을 요청하거나 이름, 주소, 클래스 및 연결 상태와 같은 기기 정보를 쿼리한다.
- BluetoothSocket : InputStream, OutputStream을 통해 앱이 다른 블루투스 기기와 데이터를 교환할 수 있게 허용하는 연결 지점
- BluetoothServerSocket : 들어오는 요청을 수신하는 소켓이다. 두 대의 Android기기를 연결하려면 한 기기가 이 클래스를 사용해서 서버 소켓을 열어야 한다. 원격 블루투스 기기가 이 기기로 연결 요청을 보내면 해당 기기가 연결을 수락한 다음 연결된 BluetoothSocket을 반환한다.
- BluetoothProfile : 블루투스 기반 통신에 대한 무선 인터페이스 사양이다. 프로토콜 같은? 예시로 Hands-Free프로필이 있다.
- BluetoothHeadset : 블루투스 헤드셋을 휴대폰과 함께 사용할 수 있도록 지원함, Bluetooth Headset 프로필 및 Hands-Free(v1.5)프로필이 포함됨
- BluetoothA2dp : A2DP(Advanced Audio Distribute Profile) 블루투스 연결을 통해 기기 간에 고품질 오디오를 스트리밍할 수 있는 방법을 정의함
- BluetoothHealth : 블루투스 서비스를 제어하는 의료기기 프로필 프록시를 나타냄
- BluetoothHealthCallback : 블루투스 채널 상태의 변경에 대한 업데이트를 받으려면 이 클래스를 확장하고 콜백 메서드를 구현한다.
- BluetoothHealthAppConfiguration : 원격 블루투스 의료 기기와 통신하기 위해 타사 블루투스 의료 앱을 등록
- BluetoothProfile.ServiceListener : 특정 프로필을 실행하는 내부 서비스와 연결하거나 연결 끊을 때 BluetoothPorifle IPC클라이엊ㄴ트에 알리는 인터페이스이다.
https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ko#Profiles
블루투스 개요 | 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
적용
블루투스를 사용할 수 있는 기기인지 확인
/**
* [isBluetoothEnabled] check Bluetooth Enabled state.
* */
private fun isBluetoothEnabled(): Boolean {
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
bluetoothAdapter = bluetoothManager.adapter
return bluetoothAdapter?.isEnabled ?: false
}
블루투스 활성화
블루투스 사용이 가능하지만, 현재 디바이스가 블루투스를 ON하지 않은 경우
/**
* this method deprecated startActivityForResult method.
* [startActivityForResultLauncher] is register, and apply callback code insnippet.
* */
private val startActivityForResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {result ->
NLog.i("startACtivityForResultLauncher.. result:$result")
// callback
if (result.resultCode == RESULT_OK) {
executeBluetooth()
} else {
// Bluetooth not Enabled
}
}
// request Bluetooth Enable
val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResultLauncher.launch(enableBluetoothIntent)
승인한 경우 RESULT_OK, 거부한 경우 RESULT_CANCELED로 반환함.
만약에, 앱이 블루투스의 상태를 계속 감지하고 싶은 경우 BroadcaseReceiver를 사용하여 감지한다.
해당 IntentFilter로는 ACTION_STATE_CHANGED를 사용하여 감지한다.
블루투스 기기가 원래 있던 것인지 아니면 새로운 것인지는 EXTRA_STATE, EXTRA_PREVIOUS_STATE로 Filter로 거른다.
블루투스 기기 찾기
BluetoothAdapter를 사용하면 기기 검색 또는 페어링된 기기 목록을 쿼리하여 원격 블루투스 기기를 찾을 수 있다.
블루투스 기기를 찾기 전에 먼저 적절한 블루투스 권한이 있는지 확인한다.
권한 목록
/**
* [requiredPermissions] required Permissions List.
* */
private val requiredPermissions = mutableListOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(Manifest.permission.BLUETOOTH_SCAN)
add(Manifest.permission.BLUETOOTH_ADVERTISE)
add(Manifest.permission.BLUETOOTH_CONNECT)
}
}.toTypedArray()
권한 1차 체크
/**
* [requestPermissionsLauncher] not use onPermissionResult override method.
* */
private val requestPermissionsLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
NLog.i("requestPermissionsLauncher.. it:$it")
// if isDenied
// requestPermissions()
}
권한 요청
/**
* [requestPermissions] if permissions deneied this re-call method.
* */
private fun requestPermissions() {
requestPermissionsLauncher.launch(requiredPermissions)
}
원격 기기와 '처음'으로 연결되면 페어링 요청이 자동으로 사용자에게 제공된다. 기기가 페어링되면 기기에 관한 기본 정보 (기기이름, 클래스, MAC주소)가 저장되며 블루투스 API를 사용하여 읽을 수 있다. 원격 기기에 관해 알려진 MAC주소를 사용하면 기기가 여전히 범위 내에 있다면 검색을 실행하지 않고 언제든지 연결을 시작할 수 있다.
페어링과 연결의 차이점
- 페어링
두 기기가 서로의 존재를 알고, 인증에 사용할 수 있는 공유 링크 키를 보유하고, 서로 암호화된 연결을 설정할 수 있음
- 연결
현재 RFCOMM채널을 공유하고 있고 서로 데이터를 전송할 수 있음, 블루투스 API로 RFCOMM연결하기 이전에 기기를 페어링하도록 요구한다. 블루투스 API와 암호화된 연결을 시작하면 페어링이 자동으로 실행한다.
페어링된 기기를 찾는 방법
device.address가 MAC주소이다.
/**
* 페어링된 기기 쿼리
* [searchPairedDevices] search paired bluetooth devices before now.
* */
@SuppressLint("MissingPermission")
private fun searchPairedDevices() {
NLog.d("searchPairedDevices..")
val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach{ device ->
val deviceName = device.name
val deviceHardwareAddress = device.address
NLog.i("searchPairedDevices.. deviceName:$deviceName, deviceHardwareAddress(MAC):$deviceHardwareAddress")
}
}
새 기기를 검색하는 방법
startDiscovery()를 호출하는데 해당 프로세스는 비동기식이며, 검색이 성공적으로 시작되었는지 Boolean으로 값을 반환한다. 검색은 약 12초 동안 조회한다. 즉, startDiscovery라는 함수 자체가 검색을 시작을 나타냄.
이렇게 검색된 각 기기의 정보를 수신하려면 앱에서 BroadcastReceiver를 등록해주어야 한다.
startDiscovery했을시 기기를 수신받을 수 있게 해주는 Receiver
private val receiverBluetoothDiscover = object: BroadcastReceiver() {
@SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
NLog.v("receiverBluetoothDiscover.. onReceive..")
val action: String = intent?.action!!
when (action) {
BluetoothDevice.ACTION_FOUND -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val device: BluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!!
val deviceName = device.name
val deviceHardwareAddress = device.address // MAC address
NLog.i("receiveBluetoothDiscover.. ACTION_FOUND.. device:$deviceName, mac:$deviceHardwareAddress")
}
}
}
}
}
Receiver등록
private fun registerDiscoveryBluetoothDevices() {
val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
registerReceiver(receiverBluetoothDiscover, filter)
}
startDiscovery하기 전 주의사항
기기 검색을 한다는 것은 블루투스 어댑터의 리소스가 많이 소모된다. 연결할 기기를 찾은 후에는 해당 기기와 연결하기 전에 cancelDiscovery()를 실행해준 뒤(검색중지) 연결한다. 또 기기에 연결된 상태에서 다른 블루투스 기기를 찾기 위해서 startDiscovery를 하면 안된다. 검색 프로세스가 기존 연결에 사용할 수 있는 대역폭을 크게 줄이기 떄문이다.
@SuppressLint("MissingPermission")
private fun startDiscoveryBluetoothDevices() {
bluetoothAdapter?.startDiscovery()
}
하지만 계속해서 discovery결과 값은 false를 받는다. 무엇이 문제인가보니,
startDiscovery는 'android.permission.BLUETOOTH_SCAN' 권한이 필요하며, 해당 권한이 위치가 필요한지 안필요한지를 명시적으로 설정해주어야 한다.
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s"/>
그럼, 잘 검색된다.
테스트 해보니 약 14~20초에 검색이 중단된다.
로컬 기기가 블루투스 검색되도록 만들기
다른기기에서 로컬 기기를 검색할 수 있으려면 ACTION_REQUEST_DISCOVERABLE인텐트를 사용하여 startActivityForResult(Intent, int)를 호출한다. 기본적으로 기기는 2분 동안 검색이 가능한 상태가 되고, EXTRA_DISCOVERABLE_DURATION을 추가하면 최대 1시간까지 검색 가능하도록 만들 수 있다.
private fun discoverableLocalDevice() {
NLog.d("discoverableLocalDevice..")
val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 180)
}
startActivityForResultLauncher.launch(discoverableIntent)
}
result:ActivityResult{resultCode=180, data=null}
블루투스 기기 연결
두 기기 간에 연결을 생성하려면 서버 측과 클라이언트 측 모두 구현해야한다. 매커니즘? 그런 말은 어렵다. 서버 쪽 역할을 하는 기기는 데이터를 갖고 있는 기기이고, 클라이언트 쪽 역할을 하는 기기는 데이터를 사용하려는 기기이다.
그럼 서버 측 기기는 서버 소켓을 열어야 한다. 그리고 클라이언트 측 기기는 서버 기기의 MAC주소를 사용해 연결을 시작해야 한다.
연결하려면 BluetoothSocket이 필요한데, 서버와 클라이언트는 서로 다른 방식으로 BluetoothSocket을 획득한다.
서버는 들어오는 연결이 수락될 때 소켓 정보(BluetoothSocket)를 수신하고,
클라이언트는 서버에 대한 RFCOMM채널을 열 때 소켓 정보(BluetoothSocket)을 제공한다.
서버와 클라이언트는 각각 동일한 RFCOMM채널에 연결된 BluetoothSocket이 있을 때 서로 연결된 것으로 간주한다. 이 시점에서 각 기기는 입력 및 출력 스트림을 가져올 수 있고 데이터 전송을 시작할 수 있다.
※서로 서버 형태로 열어두고 연결을 시작하면 연결을 먼저 시도한 쪽이 클라이언트가 된다. 이것도 하나의 연결 방법
서버 측
BluetoothServerSocket으로 서버 역할 한다. 서버 소켓의 목적은 들어오는 연결 요청을 수신 대기하고 요청이 수락된 후 연결된 BluetoothSocket을 제공하는 것. BluetoothServerSocket에서 BluetoothSocket을 가져오는 경우 기기가 추가 연결을 수락하도록 하지 않는 한 BluetoothServerSocket은 삭제될 수 있으며, 삭제해야 한다
UUID 만드는 방법
/**
* make UUID
* */
private fun makeUuid(name: String): String {
// val uniqueId: String = UUID.fromString(name).toString()
return UUID.fromString(name).toString()
}
BluetoothServerSocket가져오기
listenUsingRfcommWithServiceRecord(String, UUID) 호출.
accpt()함수로 연결 요청을 시작한다.연결이 수락되거나 예외 발생시 반환된다. 원격 기기가 이 수신 대기 서버소켓에 등록된 UUID와 일치하는 UUID가 포함된 연결 요청을 보낸 경우에만 연결이 허용된다.
성공시 BluetoothSocket을 반환한다.
추가 연결을 수락하지 않으려면 'close()'를 호출한다. 서버 소켓과 모든 리소스가 해제되지만, accept로 반환된 BluetoothSocket은 닫히지 않는다.
accept는 UI스레드에서 실행하지 않는다.
/**
* BluetoothServerSocket Thread
*
* */
@SuppressLint("MissingPermission")
private inner class AcceptThread: Thread() {
private val serviceName = "AcceptThread-BluetoothServerSocket"
private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(serviceName, makeUuid(serviceName))
}
override fun run() {
// super.run()
var shouldLoop = true
while (shouldLoop) {
val socket: BluetoothSocket? = try {
mmServerSocket?.accept()
} catch (e: IOException) {
NLog.e("Socket's accept() method failed : ${e.message}")
shouldLoop = false
null
}
socket?.also {
// manageMyConnectedSocket(it) // 데이터 전송 용도
mmServerSocket?.close()
shouldLoop = false
}
}
}
fun cancel() {
try {
mmServerSocket?.close()
} catch (e: IOException) {
e.stackTraceToString()
}
}
}
클라이언트 측
본인의 기기를 나타내는 BluetoothDevice객체를 가져와야 한다. BluetoothDevice를 사용해서 BluetoothSocket을 가져오고 연결을 시작한다. BluetoothDevice의 createRfcommSocketToServiceRecord(UUID)를 호출하여 BluetoothSocket을 가져온다. createRfcommSocketToServiceRecord에 사용된 UUID는 서버측에서 사용한 listenUsingRfcommWithServiceRecord에서 사용한 UUID와 일치해야 한다. UUID는 하드코딩한 후 서버와 클라이언트 쪽에서 참조한다.
connect함수를 사용해서 연결을 시작한다. 클라이언트가 해당 메서드를 호출하면 SDP(Service Discovery Protocol)을 실행하여 UUID가 있는 원격 기기를 찾고, 조회에 성공하고 원격 기기가 연결을 수락하면 RFCOMM채널을 공유하고 connect메서드를 반환한다. 연결을 실패하거나 12초 후 타임아웃되면 IOException이 발생한다.
connect함수는 차단 호출이므료 UI스레드가 아닌 분리된 스레드에서 연결을 시도한다.
'dev > aos' 카테고리의 다른 글
[AOS] Execution failed for task ':app:mergeExtDexDebug'. (0) | 2024.01.13 |
---|---|
[AOS] Android Studio Alt + F12 not work (0) | 2024.01.13 |
[AOS] Gradle Plugin, Version location (0) | 2024.01.12 |
[AOS] Retrofit API 통신 기본 사용 방법 (0) | 2024.01.10 |
[AOS] BuildType, Variant 빌드 (0) | 2024.01.04 |