Android Serial Port (Kotlin)
A modern, thread-safe, and reactive Android Serial Port library written in Kotlin. This library is a modernization of the popular xmaihh/Android-Serialport library, leveraging Kotlin Coroutines and Flow for asynchronous data handling.
- Modern Stack: 100% Kotlin, Coroutines, and Flow.
- Reactive API: Read data as a hot
Flow<ByteArray>. - Packet Handling: Built-in support for “sticky packets” with multiple parsing strategies (delimiter, fixedLength, startEndMarker, lengthPrefixed, customParser).
- Device Discovery:
SerialPortFinderto list available serial ports on the device. - Root Support: Helper to request root permissions for accessing serial ports.
- Thread Safety: I/O operations are offloaded to
Dispatchers.IO.
Installation
Option 1: Maven Central (Recommended)
Add the dependency to your module’s build.gradle.kts:
dependencies {
implementation("io.github.onatakduman:kserialport:1.1.3")
}Option 2: JitPack
Add JitPack repository to settings.gradle.kts:
dependencyResolutionManagement {
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}Add the dependency:
dependencies {
implementation("com.github.onatakduman:kserialport:1.1.3")
}Usage
1. List Available Devices
val finder = SerialPortFinder()
val devices = finder.getAllDevices() // Returns Array<String> e.g., ["ttyS0 (serial)", ...]
val paths = finder.getAllDevicesPath() // Returns Array<String> e.g., ["/dev/ttyS0", ...]2. Open Serial Port
try {
// 8N1 (Default)
val serialPort = SerialPort("/dev/ttyS1", 9600)
// Custom Configuration: 8E1 (8 Data bits, Even Parity, 1 Stop bit)
// val serialPort = SerialPort(
// path = "/dev/ttyS1",
// baudRate = 9600,
// dataBits = SerialPort.DATA_BITS_8,
// stopBits = SerialPort.STOP_BITS_1,
// parity = SerialPort.PARITY_EVEN
// )
val connection = serialPort.open()
// Use connection...
} catch (e: SecurityException) {
// Handle permission error
} catch (e: IOException) {
// Handle open error
}3. Read Data (Reactive)
Use the readFlow to receive data. You can apply packet handling operators directly.
// Simple read - get raw bytes as they arrive
lifecycleScope.launch {
connection.readFlow.collect { bytes ->
Log.d("Serial", "Received: ${bytes.toHexString()}")
}
}4. Packet Parsing (Sticky Packet Handling)
Serial data often arrives fragmented or concatenated. Use these operators to extract complete packets:
Delimiter-Based
Split packets by a delimiter sequence (e.g., newline, CRLF):
connection.readFlow
.delimiter(byteArrayOf(0x0A)) // Split by \n
.collect { packet ->
Log.d("Serial", "Line: ${String(packet)}")
}Fixed-Length Packets
When packets always have the same size:
connection.readFlow
.fixedLength(32) // 32-byte packets
.collect { packet ->
// Process 32-byte packet
}Start/End Marker Frames
For protocols that frame data with markers (e.g., STX/ETX):
val STX = byteArrayOf(0x02)
val ETX = byteArrayOf(0x03)
connection.readFlow
.startEndMarker(startMarker = STX, endMarker = ETX)
.collect { packet ->
// Packet data between STX and ETX
}Length-Prefixed Packets
Common in binary protocols where a header contains the payload length:
// Protocol: [1-byte length][payload]
connection.readFlow
.lengthPrefixed(
lengthFieldOffset = 0, // Length at byte 0
lengthFieldSize = 1, // 1-byte length field
bigEndian = true
)
.collect { packet ->
// Complete packet including header
}
// More complex: [2-byte header][2-byte length BE][payload]
connection.readFlow
.lengthPrefixed(
lengthFieldOffset = 2, // Length field starts at byte 2
lengthFieldSize = 2, // 2-byte length (big-endian)
headerSize = 4, // Total header is 4 bytes
bigEndian = true
)
.collect { packet -> }Custom Parser
For complex protocols, implement your own logic:
connection.readFlow
.customParser { buffer ->
// Return null if no complete packet yet
if (buffer.size < 4) return@customParser null
// Parse your protocol...
val packetEnd = findPacketEnd(buffer)
if (packetEnd == -1) return@customParser null
// Return the packet and remaining bytes
PacketParser.ParseResult(
packet = buffer.copyOfRange(0, packetEnd),
remaining = buffer.copyOfRange(packetEnd, buffer.size)
)
}
.collect { packet -> }5. Write Data
lifecycleScope.launch {
val data = "Hello World".toByteArray()
connection.write(data)
}6. Close Connection
connection.close()License
Apache License 2.0