안드로이드 개발 환경에서 다양한 장비와 기기를 네트워크로 연결해 데이터를 주고받는 기능은 필수적입니다.
네트워크 통신에 관심이 많은 저는 이러한 다양한 통신 방식을
하나의 모듈로 통합하고 관리할 수 있는 Connector 모듈을 작성하였습니다.
이번 글에서는 WiFi 소켓 통신, USB 소켓 통신에 이어서
Connector 모듈을 활용해 안드로이드 블루투스 소켓 통신을 중점적으로 다룰 예정입니다.
예제의 주요 코드를 살펴보고, 안드로이드 단말과 단말 간에 데이터를 주고받는 테스트 방법도 확인할 수 있습니다.
또한 예제 코드는 GitHub에서 다운로드하여 직접 확인할 수 있습니다.
Connector 모듈 소개
Connector 모듈은 안드로이드에서 다양한 통신 방식을 하나의 구조로 통합하여 관리할 수 있도록 설계된 모듈입니다.
이 모듈을 통해 WiFi, USB, 블루투스, 시리얼 통신 등의 통신을 구현하고 효율적으로 유지 관리할 수 있으며
통신 방식에 관계없이 일관된 일터페이스를 제공합니다.
블루투스 소켓 통신
블루투스 소켓통신을 간략하게 아래와 같이 이해하면 좋을 것 같습니다.
- 하나의 네트워크(블루투스) 환경을 만든다.
- 서버 측에서 채널을 열고 기다린다.
- 클라이언트 측에서 연결된 블루투스 주소와 UUID를 활용해 연결한다.
- 서로 데이터를 송수신한다.
여기서 잠깐!
최근 WiFi 소켓 통신으로 정리했던 내용과 블루투스 소켓 통신에서 연결하는 방법에 약간의 차이점을 가지는데요,
아래 표를 통해 간략하게 확인해 보시면 좋을 것 같습니다.
WiFi 소켓 통신 | 블루투스 소켓 통신 |
TCP/IP 프로토콜 | RFCOMM 프로토콜 |
서버측에서 Port를 열고 대기 | 서버측에서 채널을 열고 대기 |
클라이언트 측에서 IP 주소와 포트를 통해서 연결 | 클라이언트 측에서 블루투스 주소와 UUID를 통해서 연결 |
블루투스 소켓 통신 예제
블루투스 소켓 통신 예제에서 주요 코드를 살펴보려고 합니다.
해당 예제는 서버 또는 클라이언트 역할을 할 수 있습니다.
1. 권한 추가
블루투스 소켓 통신을 위해 아래 권한을 추가합니다.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
2. ConnectionBT 클래스
블루투스 소켓 통신의 실제 연결 및 데이터 송수신을 처리하는 역할을 하는 클래스입니다.
class ConnectionBT(private val bluetoothDevice: BluetoothDevice? = null) : ConnectionHandler {
private var bluetoothSocket: BluetoothSocket? = null
private var serverSocket: BluetoothServerSocket? = null
private var inputStream: InputStream? = null
private var outputStream: OutputStream? = null
private var uuid = "00001101-0000-1000-8000-00805F9B34FB" // SPP
@SuppressLint("MissingPermission")
override suspend fun connect(): Boolean = withContext(Dispatchers.IO) {
try {
bluetoothSocket = bluetoothDevice?.createRfcommSocketToServiceRecord(UUID.fromString(uuid))
BluetoothAdapter.getDefaultAdapter().cancelDiscovery()
bluetoothSocket?.connect()
inputStream = bluetoothSocket?.inputStream
outputStream = bluetoothSocket?.outputStream
Log.d("Connector(BT)", "Connected in CLIENT")
true
} catch (e: IOException) {
e.printStackTrace()
disconnect()
false
}
}
@SuppressLint("MissingPermission")
suspend fun listen(): Boolean = withContext(Dispatchers.IO) {
try {
serverSocket = BluetoothAdapter.getDefaultAdapter()
.listenUsingRfcommWithServiceRecord("MyBluetoothApp", UUID.fromString(uuid))
bluetoothSocket = serverSocket?.accept()
inputStream = bluetoothSocket?.inputStream
outputStream = bluetoothSocket?.outputStream
Log.d("Connector(BT)", "Connected in SERVER")
true
} catch (e: IOException) {
e.printStackTrace()
disconnect()
false
}
}
override fun disconnect() {
try {
inputStream?.close()
outputStream?.close()
bluetoothSocket?.close()
serverSocket?.close()
inputStream = null
outputStream = null
bluetoothSocket = null
serverSocket = null
Log.d("Connector(BT)", "Disconnected from Bluetooth")
} catch (e: IOException) {
e.printStackTrace()
}
}
override suspend fun sendData(data: ByteArray): Boolean = withContext(Dispatchers.IO) {
try {
outputStream?.write(data)
outputStream?.flush()
Log.d("Connector(BT)", "Data sent: ${String(data).trim()}")
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
override suspend fun receiveData(): ByteArray? = withContext(Dispatchers.IO) {
try {
val buffer = ByteArray(ConnectionUtil.BUFFER_SIZE)
val bytesRead = inputStream?.read(buffer) ?: -1
if (bytesRead > 0) {
val receivedData = buffer.copyOf(bytesRead)
Log.d("Connector(BT)", "Data received: ${String(receivedData).trim()}")
return@withContext receivedData
}
null
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
3. ControllerBT 클래스
ConnectionBT 기능을 관리하고 제어하는 상위 컨트롤러 클래스입니다.
class ControllerBT : BaseController() {
private var connectionBT: ConnectionBT? = null
suspend fun connectTo(bluetoothDevice: BluetoothDevice): Boolean {
connectionBT = ConnectionBT(bluetoothDevice)
return startConnection()
}
suspend fun listenTo(): Boolean {
connectionBT = ConnectionBT()
return connectionBT!!.listen().also { isConnected ->
if (isConnected) connectionType = ConnectionType.BT
}
}
override suspend fun connect(): Boolean {
return (connectionBT?.connect() ?: false).also { isConnected ->
if (isConnected) connectionType = ConnectionType.BT
}
}
override fun disconnect() {
connectionBT?.disconnect()
connectionType = ConnectionType.NONE
}
override suspend fun sendData(data: ByteArray): Boolean {
return connectionBT?.sendData(data) ?: false
}
override suspend fun receiveData(): ByteArray? {
return connectionBT?.receiveData()
}
}
4. MainActivity 동작 구현
ControllerBT를 통해서 블루투스 소켓 연결, 송수신 동작을 각각 구현할 수 있습니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val controllerBT = ControllerBT()
private var bluetoothDevice: BluetoothDevice? = null
private val bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> {
handleBluetoothDevice(intent)
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
bluetoothDevice = null
binding.etBluetoothDevice.setText("")
disconnectAll()
}
}
}
}
override fun onStart() {
super.onStart()
val bluetoothFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
registerReceiver(bluetoothReceiver, bluetoothFilter)
}
override fun onStop() {
super.onStop()
unregisterReceiver(bluetoothReceiver)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.btnBluetoothListen.setOnClickListener {
// 블루투스 채널 오픈 동작
listenBT()
}
binding.btnBluetoothConnect.setOnClickListener {
// 블루투스 소켓 연결 동작
tryConnectToBT()
}
binding.btnDisconnect.setOnClickListener {
disconnectAll()
}
binding.btnSend.setOnClickListener {
// 블루투스 소켓 송신 동작
val message = binding.etMessage.text.toString()
sendMessageToTarget(message)
}
requestBluetoothPermissions()
}
private fun listenBT() {
CoroutineScope(Dispatchers.IO).launch {
updateLogView("[Bluetooth Socket] Listening for connections...\n")
if (controllerBT.listenTo()) {
updateLogView("[Bluetooth Socket] Connected!\n")
// 블루투스 소켓 수신 동작
startReceivingData(controllerBT)
}
}
}
private fun tryConnectToBT() {
try {
CoroutineScope(Dispatchers.IO).launch {
if (bluetoothDevice == null) return@launch
if (controllerBT.connectTo(bluetoothDevice!!)) {
updateLogView("[Bluetooth Socket] Connected!\n")
// 블루투스 소켓 수신 동작
startReceivingData(controllerBT)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun startReceivingData(controller: BaseController) {
CoroutineScope(Dispatchers.IO).launch {
while (controller.connectionType != ConnectionType.NONE) {
val receivedData = controller.receiveData()
receivedData?.let {
val receivedMessage = String(it).trim()
updateLogView("Received: $receivedMessage\n")
}
delay(500)
}
}
}
private fun disconnectAll() {
//controllerWiFi.terminateConnection()
//controllerUSB.terminateConnection()
controllerBT.terminateConnection()
//controllerSerial.terminateConnection()
updateLogView("Disconnected All!\n")
}
private fun sendMessageToTarget(message: String) {
try {
CoroutineScope(Dispatchers.IO).launch {
when {
controllerBT.connectionType == ConnectionType.BT -> {
if (controllerBT.sendData(message.toByteArray())) {
updateLogView("Sent : $message\n")
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun updateLogView(message: String) {
CoroutineScope(Dispatchers.Main).launch {
binding.tvLog.text = "${binding.tvLog.text}$message"
}
}
@SuppressLint("MissingPermission")
private fun handleBluetoothDevice(intent: Intent) {
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as BluetoothDevice?
}
device?.let {
bluetoothDevice = it
binding.etBluetoothDevice.setText(it.name)
}
}
private fun requestBluetoothPermissions() {
val permissions = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.add(Manifest.permission.BLUETOOTH_SCAN)
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
permissions.add(Manifest.permission.BLUETOOTH)
permissions.add(Manifest.permission.BLUETOOTH_ADMIN)
}
permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION)
permissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 100)
}
}
블루투스 소켓 통신 예제 다운로드
블루투스 소켓 통신 테스트: 서버(모바일) - 클라이언트(모바일)
블루투스 소켓 통신 예제가 정상동작하는지 테스트를 해봐야겠죠.
기존에 코드를 가지고 계신 분이 아니라면 예제를 다운로드해서 확인해 보시는 걸 추천드립니다.
테스트 방법은 안드로이드 단말과 단말 간에 블루투스 소켓 통신으로 확인해보려고 합니다.
블루투스 예제는 서버 또는 클라이언트 역할을 두 개 다 할 수 있기 때문에 각각의 안드로이드 단말에서 실행시켜 줍니다.
하나의 네트워크 블루투스 환경이 만들어졌다면 아래와 같이 진행합니다.
1. 서버(안드로이드 예제)
- 오픈 버튼을 통해서 채널을 열고 대기
- 연결이 됐다면 Send Message를 입력하고 보내기 버튼 클릭
2. 클라이언트(안드로이드 예제)
- 서버 측에서 채널이 열렸다면 연결 버튼 클릭
- 연결이 됐다면 Send Message를 입력하고 보내기 버튼 클릭
끝으로..
Connector 모듈을 통해 블루투스 소켓 통신에 대해서 알아보았고 예제를 통해 데이터를 주고받는 테스트도 확인해 보았습니다. 원래 PC와 안드로이드 단말 간에 블루투스 소켓 통신을 테스트하고 싶었는데 PC에 블루투스가 없어서 단말끼리 테스트한 게 조금 아쉽습니다. 이제 다음글에서는 Connector 모듈을 통한 시리얼 통신을 다뤄볼 예정입니다.
좋은 하루 보내세요:)
관련 글
'안드로이드 > 코틀린' 카테고리의 다른 글
안드로이드 CameraX, 카메라 프리뷰 예제 - 사진 촬영 및 저장 구현하기 (0) | 2024.12.05 |
---|---|
안드로이드 UART 시리얼 통신 예제 다운로드 (0) | 2024.11.24 |
안드로이드 USB 소켓 통신 (예제 다운로드) (0) | 2024.11.19 |
안드로이드 WiFi 소켓 통신 (예제 다운로드) (0) | 2024.11.16 |
안드로이드 화면 전환: Navigation 기능 사용법 및 예제 (2) | 2024.07.13 |
댓글