본문 바로가기
안드로이드/코틀린

안드로이드 UART 시리얼 통신 예제 다운로드

by 시작이반의반 2024. 11. 24.

안드로이드 시리얼 통신
안드로이드 시리얼 통신

시리얼 통신은 무선 네트워크 환경에 영향을 받지 않고

안정성과 보안성을 유지할 수 있어 안드로이드 개발에서 많이 사용됩니다.

이번 글에서는 안드로이드 UART 시리얼 통신을 중심으로 다뤄보겠습니다.

 

저는 WiFi, USB, 블루투스 소켓 통신을 지원하는 Connector 모듈을 작성했으며,

최근 이 모듈에 UART 시리얼 통신 기능을 추가하였습니다.

이 글에서는 Connector 모듈을 활용해 안드로이드에서 시리얼 통신을 구현하는 방법을 구체적으로 살펴보려고 합니다.

UART 시리얼 통신 예제 코드는 GitHub에서 다운로드 가능합니다.

 

 

 

Connector 모듈 소개

 

안드로이드 시리얼 통신 Connector 모듈
Connector 모듈

Connector 모듈은 안드로이드에서 다양한 통신 방식을 하나의 구조로 통합하여 관리할 수 있도록 설계된 모듈입니다.

이 모듈을 통해 WiFi, USB, 블루투스, 시리얼 통신 등의 통신을 구현하고 효율적으로 유지 관리할 수 있으며

통신 방식에 관계없이 일관된 인터페이스를 제공합니다.

 

 

UART 시리얼 통신

 

안드로이드 UART 시리얼 통신
안드로이드 UART 시리얼 통신

안드로이드 UART 시리얼 통신은 아래와 같은 방식으로 작동합니다.

  • 시리얼 통신 케이블로 기기와 안드로이드 단말을 연결한다.
  • 서버 측에서는 보드레이트, 데이터 비트, 정지 비트, 패리티 설정 값을 가지며 COM 포트에서 대기한다.
  • 클라이언트 측에서는 같은 설정으로 서버와 연결한다.
  • 서로 데이터를 송수신한다.
# UART 시리얼 통신이란?
UART(Universal Asynchronous Receiver/Transmitter) 시리얼 통신은 시리얼 통신 방식 중에 하나로,
비동기 방식으로 데이터를 송수신합니다.

# UART 시리얼 통신 특징
Baud rate, 데이터 비트, 정지 비트, 패리티 설정을 통해 통신 규격을 맞춥니다.
Tx와 Rx 두 개의 핀을 사용하여 데이터를 직렬로 주고받습니다.
별도의 클럭 신호 없이 비동기적으로 작동하며 간단하고 안정적인 통신 방식으로 널리 사용됩니다.

 

 

 

안드로이드 UART 시리얼 통신 라이브러리

 

usb-serial-for-android 라이브러리를 사용하면 안드로이드 개발에서 보다 쉽게 UART 시리얼 통신을 구현할 수 있습니다.

usb-serial-for-android 라이브러리

  • 안드로이드 API 12 이상부터 지원
  • Android USB Host API를 사용하여 USB 연결 및 데이터 송수신 처리
  • FTDI, CP210x, PL2303, CH340/CH341, CDC/ACM 등 다양한 시리얼 칩셋 지원
  • 직관적인 인터페이스와 메서드로 보다 쉽게 구현 가능
  • GitHub에서 소스 코드를 제공하며 필요에 따라 커스터마이징 가능

 

 

 

 

UART 시리얼 통신 예제

 

시리얼 통신 예제에서 주요 코드를 살펴보려고 합니다.

해당 예제는 안드로이드 단말에 사용할 클라이언트 측 코드입니다.

 

1. usb-serial-for-android 라이브러리 추가

jitpack.io 저장소를 아래와 같이 추가해 주세요.

// Gradle 6.8 이전의 설정인 경우
allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}
// Gradle 6.8 이후의 설정인 경우
dependencyResolutionManagement {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

 

usb-serial-for-android 라이브러리를 아래와 같이 추가해 주세요.

dependencies {
    ....

    // Serial
    implementation("com.github.mik3y:usb-serial-for-android:3.8.0")
}

 

2. USB 권한 추가

AndroidManifest.xml에 아래와 같이 USB 권한을 추가해 주세요.

<uses-feature android:name="android.hardware.usb.host" />

 

 

3. ConnectionSerial 클래스

UART 시리얼 통신의 실제 연결 및 데이터 송수신을 처리하는 역할을 하는 클래스입니다.

class ConnectionSerial(context: Context, private val usbDevice: UsbDevice) : ConnectionHandler {
    private val usbManager: UsbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
    private var usbSerialPort: UsbSerialPort? = null

    override suspend fun connect(): Boolean = withContext(Dispatchers.IO) {
        try {
            val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
            if (availableDrivers.isEmpty()) {
                Log.d("Connector(Serial)", "No USB serial devices found")
                return@withContext false
            }

            val driver = availableDrivers.find { it.device == usbDevice } ?: return@withContext false
            val connection = usbManager.openDevice(driver.device) ?: return@withContext false

            usbSerialPort = driver.ports[0]
            usbSerialPort?.open(connection)
            usbSerialPort?.setParameters(
                BAUD_RATE,
                UsbSerialPort.DATABITS_8,
                UsbSerialPort.STOPBITS_1,
                UsbSerialPort.PARITY_NONE
            )

            Log.d("Connector(Serial)", "Connected to USB Serial device")
            true
        } catch (e: Exception) {
            e.printStackTrace()
            false
        }
    }

    override fun disconnect() {
        try {
            usbSerialPort?.close()
            usbSerialPort = null

            Log.d("Connector(Serial)", "Disconnected")
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override suspend fun sendData(data: ByteArray): Boolean = withContext(Dispatchers.IO) {
        try {
            usbSerialPort?.let {
                it.write(data, TIMEOUT)

                Log.d("Connector(Serial)", "Data sent: ${String(data).trim()}")
                true
            } ?: false
        } catch (e: Exception) {
            e.printStackTrace()
            false
        }
    }

    override suspend fun receiveData(): ByteArray? = withContext(Dispatchers.IO) {
        try {
            val buffer = ByteArray(BUFFER_SIZE)
            usbSerialPort?.let {
                var receivedData: ByteArray? = null
                val bytesRead = it.read(buffer, TIMEOUT)
                if (bytesRead > 0) {
                    receivedData = buffer.copyOf(bytesRead)

                    Log.d("Connector(Serial)", "Data received: ${String(receivedData).trim()}")
                }
                receivedData
            }
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }
}

 

4. ControllerSerial 클래스

ConnectionSerial 기능을 관리하고 제어하는 상위 컨트롤러 클래스입니다.

class ControllerSerial(private val context: Context) : BaseController() {
    private var connectionSerial: ConnectionSerial? = null

    suspend fun connectTo(usbDevice: UsbDevice): Boolean {
        val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
        return if (usbManager.hasPermission(usbDevice)) {
            connectionSerial = ConnectionSerial(context, usbDevice)
            startConnection()
        } else {
            Log.d("Connector(Serial)", "usbDevice has no Permission")
            false
        }
    }

    override suspend fun connect(): Boolean {
        return (connectionSerial?.connect() ?: false)
            .also { isConnected ->
                if (isConnected) connectionType = ConnectionUtil.ConnectionType.SERIAL
            }
    }

    override fun disconnect() {
        connectionSerial?.disconnect()
        connectionType = ConnectionUtil.ConnectionType.NONE
    }

    override suspend fun sendData(data: ByteArray): Boolean {
        return connectionSerial?.sendData(data) ?: false
    }

    override suspend fun receiveData(): ByteArray? {
        return connectionSerial?.receiveData()
    }
}

 

 

5. MainActivity 동작 구현

ControllerSerial을 통해서 UART 시리얼 통신 연결동작, 송수신 동작을 각각 구현하였습니다.

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private val controllerSerial = ControllerSerial(this)

    private var usbDevice: UsbDevice? = null

    private val ACTION_USB_PERMISSION = "com.example.connectorex.USB_PERMISSION"

    private val usbSerialReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
                    handleUsbDevice(intent)
                }

                UsbManager.ACTION_USB_DEVICE_DETACHED -> {
                    usbDevice = null
                    binding.etSerialDevice.setText("")

                    disconnectAll()
                }
            }
        }
    }

    override fun onStart() {
        super.onStart()

        val usbStatusFilter = IntentFilter().apply {
            addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
            addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
        }
        registerReceiver(usbSerialReceiver, usbStatusFilter)
    }

    override fun onStop() {
        super.onStop()

        unregisterReceiver(usbSerialReceiver)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.btnSerialConnect.setOnClickListener {
            // UART 시리얼 통신 연결 동작
            tryConnectToSerial()
        }

        binding.btnDisconnect.setOnClickListener {
            disconnectAll()
        }

        binding.btnSend.setOnClickListener {
            // UART 시리얼 통신 송신 동작
            val message = binding.etMessage.text.toString()
            sendMessageToTarget(message)
        }
    }

    private fun tryConnectToSerial() {
        try {
            CoroutineScope(Dispatchers.IO).launch {
                if (usbDevice == null) return@launch
                if (controllerSerial.connectTo(usbDevice!!)) {
                    updateLogView("[Serial] Connected!\n")
                    
                    // UART 시리얼 통신 수신 동작
                    startReceivingData(controllerSerial)
                }
            }
        } 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 {
                    controllerSerial.connectionType == ConnectionType.SERIAL -> {
                        if (controllerSerial.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"
        }
    }

    private fun handleUsbDevice(intent: Intent) {
        val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
        } else {
            @Suppress("DEPRECATION")
            intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) as UsbDevice?
        }

        device?.let {
            usbDevice = it
            binding.etSerialDevice.setText(it.deviceName)
        }

        val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
        if (!usbManager.hasPermission(device)) {
            val usbPermissionIntent = PendingIntent.getBroadcast(
                this,
                0,
                Intent(ACTION_USB_PERMISSION),
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )
            usbManager.requestPermission(device, usbPermissionIntent)
        }
    }
}

 

 

 

UART 시리얼 통신 예제 다운로드

 

안드로이드 UART 시리얼 통신 예제 다운로드
안드로이드 UART 시리얼 통신 예제 실행 화면

 

 

 

끝으로..

 

시리얼 통신 방법 중에 하나로 UART 시리얼 통신에 대해서 알아보았습니다. UART 시리얼 통신은 직접 테스트해보지 못해서 아쉬움이 있네요. 다양한 통신을 관리하기 위해 Connector라는 모듈을 작성했고 글로 정리하면서 개념적으로 많은 공부가 되었던 것 같습니다. 이 글이 누군가에게도 도움이 되었다면 기쁠 것 같습니다.

 

좋은 하루 보내세요:)

 

 

관련 글

안드로이드 WiFi 소켓 통신

 

안드로이드 WiFi 소켓 통신 (예제 다운로드)

안드로이드 개발 환경에서 다양한 장비와 기기를 네트워크로 연결해 데이터를 주고받는 기능은 필수적입니다.네트워크 통신에 관심이 많은 저는 이러한 다양한 통신 방식을하나의 모듈로 통합

joo-selfdev.tistory.com

안드로이드 USB 소켓 통신

 

안드로이드 USB 소켓 통신 (예제 다운로드)

안드로이드 개발 환경에서 다양한 장비와 기기를 네트워크로 연결해 데이터를 주고받는 기능은 필수적입니다.네트워크 통신에 관심이 많은 저는 이러한 다양한 통신 방식을하나의 모듈로 통합

joo-selfdev.tistory.com

안드로이드 블루투스 소켓 통신

 

안드로이드 블루투스 소켓 통신 예제 다운로드

안드로이드 개발 환경에서 다양한 장비와 기기를 네트워크로 연결해 데이터를 주고받는 기능은 필수적입니다.네트워크 통신에 관심이 많은 저는 이러한 다양한 통신 방식을하나의 모듈로 통합

joo-selfdev.tistory.com

댓글