이 예제는, 특정 이름을 가진 블루투스를 검색 후 자동 페어링, 연결 후
데이터 송/수신을 하는 예제입니다.
아두이노 우노와 데이터 송/수신 하였고, HC-06을 사용 하였으며, SPP통신입니다.
이전에 안드로이드와 아두이노 블루투스 코드를 포스팅 했었습니다. (Java)
[Android/통신] - [안드로이드] 아두이노와 안드로이드 Bluetooth 통신하기
위의 포스팅과 비교하여 추가된 점과 달라진 점은 아래와 같습니다.
- 페어링된 기기뿐 아니라 페어링 되지 않은 기기의 페어링 진행
- 특정 디바이스를 필터하여 페어링&연결
- 블루투스 connect 상태 체크
- 안드로이드, 아두이노 송/수신 전부 구현
- Kotlin
데이터 바인딩과 라이브데이터를 사용한 MVVM 구조로 짜여진 코드를 리뷰하는 것이므로,
메서드 구현위주로 봐주세요~
순서대로 진행해 보겠습니다.
권한
AndroidManifest.xml에 bluetooth를 위해 아래 permission을 추가합니다.
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Activity에서 권한 추가 코드를 작성합니다.
const val REQUEST_ALL_PERMISSION = 1
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
//Permission
if (!hasPermissions(this, PERMISSIONS)) {
requestPermissions(PERMISSIONS, REQUEST_ALL_PERMISSION)
}
initObserving()
}
...
private fun hasPermissions(context: Context?, permissions: Array<String>): Boolean {
for (permission in permissions) {
if (context?.let { ActivityCompat.checkSelfPermission(it, permission) }
!= PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
// Permission check
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_ALL_PERMISSION -> {
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show()
} else {
requestPermissions(permissions, REQUEST_ALL_PERMISSION)
Toast.makeText(this, "Permissions must be granted", Toast.LENGTH_SHORT).show()
}
}
}
}
블루투스 지원, 활성 체크
Connect 버튼을 누르면 블루투스에 페어링&연결을 시작합니다.
먼저, 블루투스 지원을 체크하고, 블루투스 기능이 꺼져있다면 켜도록 합니다.
전부 활성화되어 있다면, 스캔을 시작합니다.
if (isBluetoothSupport()) { // 블루투스 지원 체크
if(repository.isBluetoothEnabled()){ // 블루투스 활성화 체크
//Progress Bar
setInProgress(true)
//디바이스 스캔 시작
scanDevice()
}else{
// 블루투스를 지원하지만 비활성 상태인 경우
// 블루투스를 활성 상태로 바꾸기 위해 사용자 동의 요청
_requestBleOn.value = Event(true)
}
}else{ //블루투스 지원 불가
//Toast Massage
Util.showNotification("Bluetooth is not supported.")
}
- 블루투스 지원 확인
var mBluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
...
fun isBluetoothSupport():Boolean{
return if(mBluetoothAdapter==null) {
//Toast massage
Util.showNotification("Bluetooth 지원을 하지 않는 기기입니다.")
false
}else{
true
}
}
- 블루투스 활성 요청
블루투스 기능을 키는것을 activity에서 하기 위해, LiveData를 사용하여 보내준 Event를 Observe하였습니다.
//Bluetooth On 요청
viewModel.requestBleOn.observe(this, {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startForResult.launch(enableBtIntent)
})
...
private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.data
//Bluetooth를 활성화 할 경우 Connect 다시 수행
viewModel.onClickConnect()
}
}
StartActivityForResult가 deperacted되어, 위처럼 구현하기위해 gradle에 아래 코드를 추가해주세요.
// for new API replaced startActivityForResult
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta02'
Bluetooth Scan
var foundDevice:Boolean = false
...
fun scanDevice(){
//Progress State Text
progressState.postValue("device 스캔 중...")
//리시버 등록
registerBluetoothReceiver()
//블루투스 기기 검색 시작
val bluetoothAdapter = mBluetoothAdapter
foundDevice = false
bluetoothAdapter?.startDiscovery()
}
- 블루투스 리시버 등록
private var mBluetoothStateReceiver: BroadcastReceiver? = null
...
fun registerBluetoothReceiver(){
//intentfilter
val stateFilter = IntentFilter()
stateFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED) //BluetoothAdapter.ACTION_STATE_CHANGED : 블루투스 상태변화 액션
stateFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
stateFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED) //연결 확인
stateFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) //연결 끊김 확인
stateFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
stateFilter.addAction(BluetoothDevice.ACTION_FOUND) //기기 검색됨
stateFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) //기기 검색 시작
stateFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) //기기 검색 종료
stateFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
mBluetoothStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action //입력된 action
if (action != null) {
Log.d("Bluetooth action", action)
}
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
var name: String? = null
if (device != null) {
name = device.name //broadcast를 보낸 기기의 이름을 가져온다.
}
when (action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val state = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR
)
when (state) {
BluetoothAdapter.STATE_OFF -> {
}
BluetoothAdapter.STATE_TURNING_OFF -> {
}
BluetoothAdapter.STATE_ON -> {
}
BluetoothAdapter.STATE_TURNING_ON -> {
}
}
}
BluetoothDevice.ACTION_ACL_CONNECTED -> {
}
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
//디바이스가 연결 해제될 경우
connected.postValue(false)
}
BluetoothAdapter.ACTION_DISCOVERY_STARTED -> {
}
BluetoothDevice.ACTION_FOUND -> {
if (!foundDevice) {
val device_name = device!!.name
val device_Address = device.address
//블루투스 기기 이름의 앞글자가 "RNM"으로 시작하는 기기만을 검색한다
if (device_name != null && device_name.length > 4) {
if (device_name.substring(0, 3) == "RNM") {
targetDevice = device
foundDevice = true
//찾은 디바이스에 연결한다.
connectToTargetedDevice(targetDevice)
}
}
}
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
if (!foundDevice) {
//Toast massage
Util.showNotification("디바이스를 찾을 수 없습니다. 다시 시도해 주세요.")
//Progress 해제
inProgress.postValue(Event(false))
}
}
}
}
}
//리시버 등록
MyApplication.applicationContext().registerReceiver(
mBluetoothStateReceiver,
stateFilter
)
}
블루투스 리시버를 등록하여, 디바이스를 찾았을때, 디바이스가 연결, 연결 해제 되었을때의 action을 받아 이벤트 처리를 할 수 있습니다.
디바이스를 찾았을 때, 특정 기기의 이름인지 확인하여 원하는 기기의 이름일 경우 연결을 시작합니다.
또한 리시버는 생명주기의 onStop()같은 부분에서, unregister해줍니다.
fun unregisterReceiver(){
if(mBluetoothStateReceiver!=null) {
MyApplication.applicationContext().unregisterReceiver(mBluetoothStateReceiver)
mBluetoothStateReceiver = null
}
}
Connect Device
원하는 BluetoothDevice를 찾았다면, 해당 디바이스에 연결할 수 있습니다.
만약 페어링 되어 있지 않다면, 자동으로 페어링 후 연결합니다.
var targetDevice: BluetoothDevice? = null
var socket: BluetoothSocket? = null
var mOutputStream: OutputStream? = null
var mInputStream: InputStream? = null
...
private fun connectToTargetedDevice(targetedDevice: BluetoothDevice?) {
//Progress state text
progressState.postValue("${targetDevice?.name}에 연결중..")
val thread = Thread {
//선택된 기기의 이름을 갖는 bluetooth device의 object
//SPP_UUID
val uuid = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
try {
// 소켓 생성
socket = targetedDevice?.createRfcommSocketToServiceRecord(uuid)
//Connect
socket?.connect()
/**
* After Connect Device
*/
//연결 상태
connected.postValue(true)
//output, input stream을 열어 송/수신
mOutputStream = bleSocket?.outputStream
mInputStream = bleSocket?.inputStream
// 데이터 수신 시작
beginListenForData()
} catch (e: java.lang.Exception) {
// 블루투스 연결 중 오류 발생
e.printStackTrace()
connectError.postValue(Event(true))
try {
socket?.close()
catch (e: IOException) {
e.printStackTrace()
}
}
}
//연결 thread를 수행한다
thread.start()
}
연결 후 input, output stream을 열고 데이터 송/수신을 할 수 있습니다.
데이터 송/수신
- 안드로이드 데이터 송신
/**
* 블루투스 데이터 송신
* String sendTxt를 byte array로 바꾸어 전송할 수 있다.
* val byteArr = sendTxt.toByteArray(Charset.defaultCharset())
* sendByteData(byteArr)
*/
fun sendByteData(data: ByteArray) {
Thread {
try {
mOutputStream?.write(data) // 프로토콜 전송
} catch (e: Exception) {
// 문자열 전송 도중 오류가 발생한 경우.
e.printStackTrace()
}
}.run()
}
- 안드로이드 데이터 수신
/**
* 블루투스 데이터 수신 Listener
*/
fun beginListenForData() {
val mWorkerThread = Thread {
while (!Thread.currentThread().isInterrupted) {
try {
val bytesAvailable = mInputStream?.available()
if (bytesAvailable != null) {
if (bytesAvailable > 0) { //데이터가 수신된 경우
val packetBytes = ByteArray(bytesAvailable)
mInputStream?.read(packetBytes)
/**
* 한 버퍼 처리
*/
// Byte -> String
val s = String(packetBytes,Charsets.UTF_8)
//수신 String 출력
putTxt.postValue(s)
/**
* 한 바이트씩 처리
*/
for (i in 0 until bytesAvailable) {
val b = packetBytes[i]
Log.d("inputData", String.format("%02x", b))
}
}
}
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
//데이터 수신 thread 시작
mWorkerThread.start()
}
- 아두이노 송/수신 코드
#include <SoftwareSerial.h>
const int pinTx = 5; // 블루투스 TX 연결 핀 번호
const int pinRx = 4; // 블루투스 RX 연결 핀 번호
SoftwareSerial bluetooth( pinTx, pinRx );
void setup()
{
bluetooth.begin(9600); // 블루투스 통신 초기화 (속도= 9600 bps)
Serial.begin(115200);
}
void loop()
{
// 블루투스 수신
if ( bluetooth.available() )
{
Serial.print((char)bluetooth.read());
}
else
{
delay( 10 );
}
// 블루투스 송신
if (Serial.available()) {
//시리얼 모니터에서 입력된 값을 송신
char toSend = (char)Serial.read();
bluetooth.print(toSend);
}
}
전체 소스를 github에서 확인하세요.
'Android > 통신' 카테고리의 다른 글
안드로이드 RxBle 사용하기, 예제 (6) | 2021.06.25 |
---|---|
[안드로이드 Kotlin] BLE(Bluetooth Low Energy) 통신 예제 (39) | 2020.11.02 |
[안드로이드 java] byte 배열 타입별로 변환하기 ― 수신 프로토콜 처리하기 (0) | 2020.06.05 |
[안드로이드] Wifi List (와이파이 목록) 띄우기 ― Popup Window에서 Recycler View 사용하기 (7) | 2020.06.03 |