和原生 API 不同的是,因為我們此時已經(jīng)知道了我們的 ESP32 主板的設備信息,以及使用的驅(qū)動(CDC),所以我們就不使用原生的查找可用設備的方法了,我們這里直接指定我們已知的這個設備(當然,你也可以繼續(xù)使用原生API的查找和連接方法):
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val customTable = ProbeTable()
// 添加我們的設備信息,三個參數(shù)分別為 vendroId、productId、驅(qū)動程序
customTable.addProduct(0x1a86, 0x55d3, CdcAcmSerialDriver::class.java)
val prober = UsbSerialProber(customTable)
// 查找指定的設備是否存在
val drivers: List
連接到設備后,下一步就是和數(shù)據(jù)交互,這里封裝的十分方便,只需要獲取到 UsbSerialPort
后,直接調(diào)用它的 read()
和 write()
即可讀寫數(shù)據(jù):
port = driver.ports[0] // 大多數(shù)設備都只有一個 port,所以大多數(shù)情況下直接取第一個就行
port.open(connection)
// 設置連接參數(shù),波特率9600,以及 “8N1”
port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
// 讀取數(shù)據(jù)
val responseBuffer = ByteArray(1024)
port.read(responseBuffer, 0)
// 寫入數(shù)據(jù)
val sendData = byteArrayOf(0x6F)
port.write(sendData, 0)
此時,一個完整的,用于測試我們上述 ESP32 程序的代碼如下:
@Composable
fun SerialScreen() {
val context = LocalContext.current
Column(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { scanDevice(context) }) {
Text(text = "查找并連接設備")
}
Button(onClick = { switchLight(true) }) {
Text(text = "開燈")
}
Button(onClick = { switchLight(false) }) {
Text(text = "關燈")
}
}
}
private fun scanDevice(context: Context) {
val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val customTable = ProbeTable()
customTable.addProduct(0x1a86, 0x55d3, CdcAcmSerialDriver::class.java)
val prober = UsbSerialProber(customTable)
val drivers: List
運行這個程序,并且連接設備。
可以看到輸出的是 byte 的 101,轉(zhuǎn)換為 ASCII 即為 “e”。
然后我們點擊 “開燈”、“關燈” 效果如下:
對了,這里發(fā)送的數(shù)據(jù) “0x6F” 即 ASCII “o” 的十六進制,同理,“0x63” 即 “c”。
可以看到,可以完美的和我們的 ESP32 開發(fā)版進行通信。
實例
無論使用什么方式與串口通信,我們在安卓APP的代碼層面能夠拿到的數(shù)據(jù)已經(jīng)是處理好了的數(shù)據(jù)。
即,在上一篇文章中我們說過串口通信的一幀數(shù)據(jù)包括起始位、數(shù)據(jù)位、校驗位、停止位。但是我們在安卓中使用時一般拿到的都只有 數(shù)據(jù)位 的數(shù)據(jù),其他數(shù)據(jù)已經(jīng)在底層被解析好了,無需我們?nèi)リP心怎么解析,或者使用。
我們可以直接拿到的就是可用數(shù)據(jù)。
這里舉一個我之前用過的某型號驅(qū)動版的例子。
這塊驅(qū)動版關于通信的信息如圖:
可以看到,它采用了 RS485 的通信方式,波特率支持 9600 或 38400,8位數(shù)據(jù)位,無校驗,1位停止位。
并且,它還規(guī)定了一個數(shù)據(jù)協(xié)議。
在它定義的協(xié)議中,第一位為地址;第二位為指令;第三位到第N位為數(shù)據(jù)內(nèi)容;最后兩位為CRC校驗。
需要注意的是,這里定義的協(xié)議是基于串口通信的,不要把這個協(xié)議和串口通信搞混了,簡單來說就是在串口通信協(xié)議的數(shù)據(jù)位中又定義了一個自己的協(xié)議。
而且可以看到,雖然定義串口參數(shù)時沒有指定校驗,但是在它自己的協(xié)議中指定了使用 CRC 校驗。
另外,弱弱的吐槽一句,這個驅(qū)動版的協(xié)議真的不好使。
在實際使用過程中,主機與驅(qū)動版的通信數(shù)據(jù)無法保證一定會在同一個數(shù)據(jù)幀中發(fā)送完成,所以可能會造成“粘包”、“分包”現(xiàn)象,也就是說,數(shù)據(jù)可能會分幾次發(fā)過來,而且你不好判斷這數(shù)據(jù)是上次沒發(fā)送完的數(shù)據(jù)還是新的數(shù)據(jù)。
我使用過的另外一款驅(qū)動版就方便的多,因為它會在幀頭加上開始符號和數(shù)據(jù)長度,幀尾加上結束符號。
這樣一來,即使出現(xiàn)“粘包”、“分包”我們也能很好的給它解析出來。
當然,它這樣設計協(xié)議肯定是有它的道理的,無非就是減少通信代價之類的。
我還遇到過一款十分簡潔的驅(qū)動版,直接發(fā)送一個整數(shù)過去表示執(zhí)行對應的指令。
驅(qū)動版回傳的數(shù)據(jù)同樣非常簡單,就是一個數(shù)字,然后事先約定各個數(shù)字表示什么意思……
說歸說,我們還是繼續(xù)來看這款驅(qū)動版的通信協(xié)議:
這是它的其中一個指令內(nèi)容,我們發(fā)送指令 “1” 過去后,它會返回當前驅(qū)動版的型號和版本信息給我們。
因為我們的主板是定制工控主板,所以使用的通信方式是直接用 android-serialport-api。
最終發(fā)送與接收回復也很簡單:
/**
* 將十六進制字符串轉(zhuǎn)成 ByteArray
* */
private fun hexStrToBytes(hexString: String): ByteArray {
check(hexString.length % 2 == 0) { return ByteArray(0) }
return hexString.chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
private fun isReceivedLegalData(receiveBuffer: ByteArray): Boolean {
val rcvData = receiveBuffer.copyOf() //重新拷貝一個使用,避免原數(shù)據(jù)被清零
if (cmd.cmdId.checkDataFormat(rcvData)) { //檢查回復數(shù)據(jù)格式
isPkgLost = false
if (cmd.cmdId.isResponseBelong(rcvData)) { //檢查回復命令來源
if (!AdhShareData.instance.getIsUsingCrc()) { //如果不開啟CRC檢驗則直接返回 true
resolveRcvData(cmdRcvDataCallback, rcvData, cmd.cmdId)
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.Success, rcvData, 0, rcvData.size, cmd.cmdId)
}
return true
}
if (cmd.cmdId.checkCrc(rcvData)) { //檢驗CRC
resolveRcvData(cmdRcvDataCallback, rcvData, cmd.cmdId)
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.Success, rcvData, 0, rcvData.size, cmd.cmdId)
}
return true
}
else {
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseCrcError, ByteArray(0), -1, -1, cmd.cmdId)
}
return false
}
}
else {
coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseNotFromThisCmd, ByteArray(0), -1, -1, cmd.cmdId)
}
return false
}
}
else { //數(shù)據(jù)不符合,可能是遇到了分包,繼續(xù)等待下一個數(shù)據(jù),然后合并
isPkgLost = true
return isReceivedLegalData(cmd)
/*coroutineScope.launch(Dispatchers.Main) {
cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseWrongFormat, ByteArray(0), -1, -1, cmd.cmdId)
}
return false */
}
}
// ……省略初始化和連接代碼
// 發(fā)送數(shù)據(jù)
val bytes = hexStrToBytes("0201C110")
outputStream.write(bytes, 0, bytes.size)
// 解析數(shù)據(jù)
val recvBuffer = ByteArray(0)
inputStream.read(recvBuffer)
while (receiveBuffer.isEmpty()) {
delay(10)
}
isReceivedLegalData()
本來打算直接發(fā)我封裝好的這個驅(qū)動版的協(xié)議庫的,想了想,好像不太合適,所以就大概抽出了這些不完整的代碼,懂這個意思就行了,哈哈。
總結
從上面介紹的兩種方式可以看出,兩種方式使用各有優(yōu)缺點。
使用 android-serialport-api 可以直接讀取串口數(shù)據(jù)內(nèi)容,不需要轉(zhuǎn)USB接口,不需要驅(qū)動支持,但是需要 ROOT,適合于定制安卓主板上已經(jīng)預留了 RS232 或 RS485 接口且設備已 ROOT 的情況下使用。
而使用 USB host ,可以直接讀取USB接口轉(zhuǎn)接的串口數(shù)據(jù),不需要ROOT,但是只支持有驅(qū)動的串口轉(zhuǎn)USB芯片,且只支持使用USB接口,不支持直接連接串口設備。
各位可以根據(jù)自己的實際情況靈活選擇使用什么方式來實現(xiàn)串口通信。
當然,除了現(xiàn)在介紹的這些串口通信,其實還有一個通信協(xié)議在實際使用中用的非常多,那就是 MODBUS 協(xié)議。
-
plc
+關注
關注
5001文章
12942瀏覽量
459188 -
串口通信
+關注
關注
34文章
1601瀏覽量
55233 -
安卓
+關注
關注
5文章
2107瀏覽量
56692 -
ESP32
+關注
關注
17文章
936瀏覽量
16659
發(fā)布評論請先 登錄
相關推薦
評論