Compose 블루투스 SPP 연결

박주현·2024년 1월 21일

회사 프로젝트에서 바코드 리더기로 키보드 연결방식이 아닌 SPP(serial port profile)연결을 통해서 바코드 리딩을 해야하는 앱을 개발해야 했는데, 예제 코드를 검색해봐도 예전 코드들(java) 밖에 없고, 앱을 컴포즈로 개발하고 있었기 때문에 마땅한 예제가 없어서 개발한 경험을 토대로 작성하게 되었습니다.
도움이 되셨으면 좋겠습니다~!

사용 기술

  • Jetpack Compose (bom 2023.04.00)
  • MVI 패턴
  • Hilt

예제 코드

각각의 역할에 대한 상세 설명 및 권한 설정은 다른 예제에도 많이 설명이 되어있으니 스킵하고 바로 코드로 넘어가도록 하겠습니다.
우선, 사용할 클래스들은 다음과 같습니다.

BluetoothConnect.kt // bluetooth 연결 클래스
BluetoothConnectViewModel.kt // viewmodel클래스
HomeScreen.kt // view 영역

BluetoothConnect 클래스를 먼저 살펴보자면

블루투스 Adapter 초기화

private var bluetoothAdapter: BluetoothAdapter

init {
	val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    bluetoothAdapter = bluetoothManager.adapter
}

연결된 디바이스 목록 가져오기

fun getDeviceList(): List<BluetoothDevice> {
    val deviceList: Set<BluetoothDevice> = bluetoothAdapter.bondedDevices
    // 연결된 디바이스 목록 리턴
    return deviceList.toList()
}

디바이스 소켓 연결

private lateinit var socket: BluetoothSocket
private val _sharedFlow = MutableSharedFlow<String>(0)
val sharedFlow = _sharedFlow.asSharedFlow()
    
@SuppressLint("MissingPermission")
suspend fun connectDevice(device: BluetoothDevice) {
	// 소켓 연결
    try {
        socket = device.createRfcommSocketToServiceRecord(SPP_UUID)
        socket.connect()
    } catch (e: Exception) {
        try {
            socket.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    
    // 값을 리딩하는 부분
    try {
        val buffer = byteArrayOf(1024.toByte())
        // 바코드 정보를 담는 변수
        val barcode = mutableStateOf("")
        
        // 소켓이 연결되어 있는 동안 리딩
        while (socket.isConnected) {
            val data = String(buffer.copyOf(socket.inputStream.read(buffer)), Charsets.UTF_8)
            // 바코드 리딩 완료시 마지막에 \r가 들어오기 때문에 구분하기 위한 용도
            if (data != "\r") {
                barcode.value += data
            } else {
            	// emit으로 읽은 바코드 값을 전달한다.
                _sharedFlow.emit(barcode.value)
                // 바코드 초기화
                barcode.value = ""
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
        socket.close()
    }
}
companion object {
    val SPP_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
}

소켓 연결 해제

fun disconnect() {
	try {
    	socket.close()
	} catch (e: IOException) {
    	e.printStackTrace()
	}
}

전체코드

@SuppressLint("MissingPermission")
class BluetoothConnect @Inject constructor(
    @ApplicationContext private val context: Context
) {
    private var bluetoothAdapter: BluetoothAdapter
    private lateinit var socket: BluetoothSocket
    private val _sharedFlow = MutableSharedFlow<String>(0)
    val sharedFlow = _sharedFlow.asSharedFlow()

    init {
        val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter
    }

    fun getDeviceList(): List<BluetoothDevice> {
        val deviceList: Set<BluetoothDevice> = bluetoothAdapter.bondedDevices
        return deviceList.toList()
    }

    fun disconnect() {
        try {
            socket.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    @SuppressLint("MissingPermission")
    suspend fun connectDevice(device: BluetoothDevice) {
        try {
            socket = device.createRfcommSocketToServiceRecord(SPP_UUID)
            socket.connect()
        } catch (e: Exception) {
            try {
                socket.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        try {
            val buffer = byteArrayOf(1024.toByte())
            val barcode = mutableStateOf("")
            while (socket.isConnected) {
                val data = String(buffer.copyOf(socket.inputStream.read(buffer)), Charsets.UTF_8)
                if (data != "\r") {
                    barcode.value += data
                } else {
                    _sharedFlow.emit(barcode.value)
                    barcode.value = ""
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            socket.close()
        }
    }

    companion object {
        val SPP_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
    }
}

ViewModel

@HiltViewModel
class BluetoothConnectViewModel @Inject constructor(
    private val bluetoothConnect: BluetoothConnect,
    @IoDispatcher private val dispatcher: CoroutineDispatcher
) : ViewModel() {

    private val _barcodeSharedFlow = MutableSharedFlow<String>()
    val barcodeSharedFlow = _barcodeSharedFlow.asSharedFlow()

    private val reducer = {..}

    init {
    	// 디바이스 리스트 가져오기
        getDeviceList()
        
        bluetoothConnect.sharedFlow
            .onEach {
            	// 받아온 값 다시 전달
                _barcodeSharedFlow.emit(it)
            }
            .launchIn(viewModelScope)
    }

    {..}

    private fun getDeviceList() {
        viewModelScope.launch(dispatcher) {
            val deviceList = bluetoothConnect.getDeviceList()
            // state 변경
            reducer.setState { copy(deviceList = deviceList) }
        }
    }

	// 디바이스 연결
    private fun connectDevice(device: BluetoothDevice) {
        viewModelScope.launch(dispatcher) {
            bluetoothConnect.connectDevice(device)
        }
    }
    
    // 소켓 연결 해제
    private fun closeSocketDevice() {
        viewModelScope.launch(dispatcher) {
            bluetoothConnect.disconnect()
        }
    }
}

HomeScreen

@Composable
fun HomeScreen(
    navController: NavHostController,
    homeViewModel: HomeViewModel = hiltViewModel(),
    bluetoothViewModel: BluetoothConnectViewModel = hiltViewModel()
) {
	val bluetoothState = bluetoothViewModel.stateFlow.collectAsState().value
    // 연결 가능한 spp device 목록을 bottom sheet로 표시
    BasicModalBottom(
        ```
        // device연결 이벤트
        Button(
        	onClick = { bluetoothState.connectDevice(item) }
        ) {
        	Text(text = item.name)
        }
    )
    Column(modifier = Modifier.fillMaxSize()) {
    	// 연결 가능한 spp device 목록 이벤트
    	Button(
        	onClick = {bluetoothState.getDeviceList()}
        ) {
        	Text(text = "SPP연결")
        }
    }
}

제가 사용한 방법은 이런식으로 사용했고, 더 좋은 방식이 있다면 의견 부탁드립니다.
중간중간 생략한 부분은 핵심 코드는 아니고 디자인영역이거나 사용하는 디자인패턴에 의해 변경될 수 있는 부분이라 생략했습니다.
처음 글을 작성해서 그런지 설명을 어떻게 해야할지 몰라서 사용한 코드 위주로 올리게 되었는데 도움이 되셨으면 좋겠습니다.

profile
Android Developer

0개의 댓글