1. 前言
我們?cè)谕ㄟ^串口、TCP
、UDP
等方式接收協(xié)議的時(shí)候,由于單次接收數(shù)據(jù)有限,導(dǎo)致一條命令可能被分割成多次進(jìn)行接收。
這種情況下,就需要進(jìn)行沾包處理,使多次接收的數(shù)據(jù),合并成一條數(shù)據(jù)。本文通過博主本人一個(gè)真實(shí)的工作案例,實(shí)例講解Android串口的接入和對(duì)于沾包的處理。
2. 協(xié)議
我們以下方這個(gè)協(xié)議為例
這是個(gè)串口協(xié)議,Android設(shè)備通過監(jiān)聽串口,讀取到具體的數(shù)據(jù)
前導(dǎo)幀 | 長(zhǎng)度 | 內(nèi)容 | 校驗(yàn) | |
---|---|---|---|---|
長(zhǎng)度 | 1Bit | 1Bit | 0~255Bit | 1Bit |
值 | 0xAA | 0~255 | Json | 校驗(yàn)結(jié)果 |
可以看到,前導(dǎo)幀為1個(gè)字節(jié)
,每當(dāng)讀取到0xAA
,就代表一條命令的開始。
第二個(gè)字節(jié)是長(zhǎng)度,占1個(gè)字節(jié)
,表示內(nèi)容
部分占用多少個(gè)字節(jié)。
最后一個(gè)字節(jié)用特定的算法,將命令的前面部分進(jìn)行計(jì)算后得到的值,用來校驗(yàn)這條命令是否正確。
- 如果命令正確,那就正常處理
- 如果命令錯(cuò)誤,就作丟棄處理
3. 驗(yàn)證串口硬件是否正常
可以在平板或手機(jī)上下載usb調(diào)試寶,設(shè)置好波特率 (比如115200
,這個(gè)根據(jù)串口設(shè)備設(shè)置),然后即可監(jiān)聽到串口發(fā)送的數(shù)據(jù)了。
4. 串口接入
我們這里使用了usb-serial-for-android這個(gè)串口庫
4.1 添加Jitpack倉庫
repositories {
...
maven { url 'https://jitpack.io' }
}
4.2 添加usb-serial-for-android依賴
implementation 'com.github.mik3y:usb-serial-for-android:3.4.6'
4.3 獲取UsbManager
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
4.4 判斷是否有權(quán)限
fun hasPermission(): Boolean {
val driver = getDriver() ?: return false
return usbManager.hasPermission(driver.device)
}
private fun getDrivers(): MutableList<UsbSerialDriver> {
return UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
}
private fun getDriver(): UsbSerialDriver? {
val availableDrivers = getDrivers()
if (availableDrivers.isEmpty()) {
log("availableDrivers is empty.")
return null
}
return availableDrivers[0]
}
4.5 請(qǐng)求權(quán)限
如果沒有權(quán)限,需要先申請(qǐng)權(quán)限,這一步很主要,要不然后面肯定是讀取不到串口的數(shù)據(jù)的。
fun requestPermission() {
val driver = getDriver() ?: return
val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
val permissionIntent = PendingIntent.getBroadcast(
context,
0,
Intent("com.android.example.USB_PERMISSION"),
flags
)
usbManager.requestPermission(driver.device, permissionIntent)
}
4.6 打開設(shè)備
val driver = getDriver() ?: return
val connection = usbManager.openDevice(driver.device) ?: return
log("connection:$connection")
port = driver.ports[0] // Most devices have just one port (port 0)
port?.open(connection)
port?.setParameters(params.baudRate, params.dataBits, params.stopBits, params.parity)
usbIoManager = SerialInputOutputManager(port, this)
usbIoManager.start()
注意這里SerialInputOutputManager
有個(gè)監(jiān)聽,onNewData
就是處理接收數(shù)據(jù)的地方了。
override fun onNewData(data: ByteArray?) {
//當(dāng)接收到數(shù)據(jù)
}
override fun onRunError(e: Exception?) {
//當(dāng)運(yùn)行出錯(cuò)
}
4.7 關(guān)閉設(shè)備
當(dāng)我們要退出App
的時(shí)候,需要去關(guān)閉串口
fun closeDevice() {
port?.close()
port = null
}
5. 沾包處理
當(dāng)我們?cè)?code>onNewData里,我們需要進(jìn)行沾包處理。
這里我處理沾包的一個(gè)思路是在onNewData
接收到的數(shù)據(jù),存儲(chǔ)到一個(gè)地方,然后另起一個(gè)線程,在那個(gè)線程中,再去讀取數(shù)據(jù)。這樣,就可以很好地規(guī)避在onNewData
里,一股腦給到一個(gè)ByteArray
數(shù)組,導(dǎo)致的拆解數(shù)據(jù),處理多種異常情況的問題了。
而onNewData
接收到的數(shù)據(jù),我們可以存儲(chǔ)到Queue
(隊(duì)列),隊(duì)列的特性是先進(jìn)先出(通常但并非一定),這樣就可以確保我們先接收到的數(shù)據(jù)先被讀取處理,并且也簡(jiǎn)化了處理的流程。
5.1 常見的Queue
常見的Queue
有這幾種,我們這里選用的是LinkedBlockingQueue
,沒有數(shù)據(jù)的時(shí)候,它具有自動(dòng)阻塞的能力。
-
ArrayBlockingQueue
: 數(shù)組實(shí)現(xiàn)的有界隊(duì)列,會(huì)自動(dòng)阻塞,根據(jù)調(diào)用api
不同,有不同特性,當(dāng)隊(duì)列容量不足時(shí),有阻塞能力-
boolean add(E e)
:在容量不足時(shí),拋出異常。 -
void put(E e)
:在容量不足時(shí),阻塞等待。 -
boolean offer(E e)
:不阻塞,容量不足時(shí)返回false
,當(dāng)前新增數(shù)據(jù)操作放棄。 -
boolean offer(E e, long timeout, TimeUnit unit)
:容量不足時(shí),阻塞times
時(shí)長(zhǎng)(單位為timeunit
),如果在阻塞時(shí)長(zhǎng)內(nèi),有容量空閑,新增數(shù)據(jù)返回true
。如果阻塞時(shí)長(zhǎng)范圍內(nèi),無容量空閑,放棄新增數(shù)據(jù),返回false
。
-
-
LinkedBlockingQueue
:鏈?zhǔn)疥?duì)列,隊(duì)列容量不足或?yàn)?code>0時(shí)自動(dòng)阻塞-
void put(E e)
:自動(dòng)阻塞,隊(duì)列容量滿后,自動(dòng)阻塞。 -
E take()
:自動(dòng)阻塞,隊(duì)列容量為0
后,自動(dòng)阻塞。
-
-
ConcurrentLinkedQueue
: 基礎(chǔ)鏈表同步隊(duì)列-
boolean offer(E e)
:入隊(duì)。 -
E peek()
:查看queue中的首數(shù)據(jù)。 -
E poll()
:取出queue中的首數(shù)據(jù)。
-
-
DelayQueue
: 延時(shí)隊(duì)列,根據(jù)比較機(jī)制,實(shí)現(xiàn)自定義處理順序的隊(duì)列。常用于定時(shí)任務(wù),如:定時(shí)關(guān)機(jī)。-
int compareTo(Delayed o)
:比較大小,自動(dòng)升序。 - 比較方法建議和
getDelay
方法配合完成。如果在DelayQueue
是需要按時(shí)完成的計(jì)劃任務(wù),必須配合getDelay
方法完成。 -
long getDelay(TimeUnit unit)
:獲取計(jì)劃時(shí)長(zhǎng)的方法,根據(jù)參數(shù)TimeUnit
來決定,如何返回結(jié)果值。
-
-
LinkedTransferQueue
: 轉(zhuǎn)移隊(duì)列-
boolean add(E e)
:隊(duì)列會(huì)保存數(shù)據(jù),不做阻塞等待。 -
void transfer(E e)
:是TransferQueue
的特有方法。必須有消費(fèi)者(take()
方法調(diào)用者)。如果沒有任意線程消費(fèi)數(shù)據(jù),transfer
方法阻塞。一般用于處理及時(shí)消息。
-
-
SynchronousQueue
: 同步隊(duì)列,容量為0
,是特殊的TransferQueue,必須先有消費(fèi)線程等待,才能使用的隊(duì)列。-
boolean add(E e)
:父類方法,無阻塞,若沒有消費(fèi)線程阻塞等待數(shù)據(jù),則拋出異常。 -
put(E e)
:有阻塞,若沒有消費(fèi)線程阻塞等待數(shù)據(jù),則阻塞。
-
詳細(xì)關(guān)于Queue的介紹,詳見 https://blog.csdn.net/qq_37050329/article/details/116295082
5.2 啟動(dòng)線程
在打開串口的時(shí)候,我們?nèi)?dòng)另一個(gè)線程。這里我使用到了線程池,newSingleExecutor
是一個(gè)單線程池,它只會(huì)用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序執(zhí)行。
private val dataQueue = LinkedBlockingQueue<Byte>()
private val singleExecutor: Executor by lazy {
Executors.newSingleThreadExecutor()
}
private val readRunnable = Runnable {
//TODO 具體實(shí)現(xiàn)
}
singleExecutor.execute(readRunnable) //啟動(dòng)線程
5.3 定義Cmd用來接收命令
class Cmd {
companion object {
const val PREAMBLE: Byte = 0xAA.toByte()
}
var preamble: Byte? = null
var length: Byte = -1
var payload = ArrayList<Byte>()
var checkSum: Byte? = null
fun clear() {
preamble = null
length = -1
payload.clear()
checkSum = null
}
}
5.4 進(jìn)行沾包處理
在readRunnable
中,我們?nèi)プx取dataQueue
的數(shù)據(jù),當(dāng)dataQueue
沒有數(shù)據(jù)的時(shí)候,會(huì)進(jìn)行阻塞,這樣就避免了性能的損耗。
val byte = dataQueue.take()
接著,如果我們讀到前導(dǎo)幀,就假設(shè)讀取到了一條命令,按順序依次讀取長(zhǎng)度
、內(nèi)容
、校驗(yàn)
,所有的值都讀取到后,需要對(duì)校驗(yàn)值checkSum
做效驗(yàn),具體校驗(yàn)的算法根據(jù)協(xié)議約定來。
命令校驗(yàn)通過后,就可以取到內(nèi)容,轉(zhuǎn)化為Json,進(jìn)一步做業(yè)務(wù)邏輯處理了。
val PREAMBLE: Byte = 0xAA.toByte()
if (byte == PREAMBLE) { //前導(dǎo)幀
cmd = Cmd()
cmd.preamble = PREAMBLE
log("前導(dǎo)幀:0x${HexUtil.toByteString(PREAMBLE)}")
cmd.length = dataQueue.take()
log("長(zhǎng)度:${cmd.length}")
readPayload(dataQueue)
log("內(nèi)容:${HexUtil.bytesToHexString(cmd.payload.toByteArray())}")
val checkSum = dataQueue.take()
cmd.checkSum = checkSum
log("校驗(yàn):0x${HexUtil.toByteString(checkSum)}")
//TODO 需要對(duì)checkSum進(jìn)行校驗(yàn),判斷命令是否正確
val json = String(cmd.payload.toByteArray()) //內(nèi)容轉(zhuǎn)換為Json,這里可以做進(jìn)一步邏輯處理
cmd.clear()
} else {
Log.e("Heiko", "被拋棄:0x${HexUtil.toByteString(byte)}")
}
private fun readPayload(dataStack: LinkedBlockingQueue<Byte>) {
for (i in 0 until cmd.length) {
cmd.payload.add(dataStack.take())
}
}
至此,對(duì)于沾包的處理就完成了
6. 附錄
6.1 封裝的串口工具類
附上基于usb-serial-for-android封裝好的串口工具類完整代碼
class UsbSerialManager(
private val context: Context,
private val params: UsbSerialParams,
private val receiver: (String) -> Unit
) :
SerialInputOutputManager.Listener {
private var port: UsbSerialPort? = null
private lateinit var usbIoManager: SerialInputOutputManager
private val dataQueue = LinkedBlockingQueue<Byte>()
private var cmd: Cmd = Cmd()
private val singleExecutor: Executor by lazy {
Executors.newSingleThreadExecutor()
}
private val readRunnable: Runnable
private val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
init {
readRunnable = Runnable {
while (port?.isOpen == true || dataQueue.isNotEmpty()) {
val byte = dataQueue.take()
if (byte == PREAMBLE) { //前導(dǎo)幀
cmd = Cmd()
cmd.preamble = PREAMBLE
log("前導(dǎo)幀:0x${HexUtil.toByteString(PREAMBLE)}")
cmd.length = dataQueue.take()
log("長(zhǎng)度:${cmd.length}")
readPayload(dataQueue)
log("payload:${HexUtil.bytesToHexString(cmd.payload.toByteArray())}")
val checkSum = dataQueue.take()
cmd.checkSum = checkSum
log("校驗(yàn):0x${HexUtil.toByteString(checkSum)}")
receiver.invoke(String(cmd.payload.toByteArray()))
cmd.clear()
} else {
Log.e("Heiko", "被拋棄:0x${HexUtil.toByteString(byte)}")
}
}
}
}
private fun readPayload(dataStack: LinkedBlockingQueue<Byte>) {
for (i in 0 until cmd.length) {
cmd.payload.add(dataStack.take())
}
}
fun requestPermission() {
val driver = getDriver() ?: return
val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
val permissionIntent = PendingIntent.getBroadcast(
context,
0,
Intent("com.android.example.USB_PERMISSION"),
flags
)
usbManager.requestPermission(driver.device, permissionIntent)
}
private fun getDrivers(): MutableList<UsbSerialDriver> {
return UsbSerialProber.getDefaultProber().findAllDrivers(usbManager)
}
private fun getDriver(): UsbSerialDriver? {
val availableDrivers = getDrivers()
if (availableDrivers.isEmpty()) {
log("availableDrivers is empty.")
return null
}
return availableDrivers[0]
}
fun hasPermission(): Boolean {
val driver = getDriver() ?: return false
return usbManager.hasPermission(driver.device)
}
fun openDevice() {
if (port?.isOpen == true) {
log("port is opened.")
return
}
val driver = getDriver() ?: return
debugLogDrivers()
val connection = usbManager.openDevice(driver.device) ?: return
log("connection:$connection")
port = driver.ports[0] // Most devices have just one port (port 0)
port?.open(connection)
port?.setParameters(params.baudRate, params.dataBits, params.stopBits, params.parity)
usbIoManager = SerialInputOutputManager(port, this)
usbIoManager.start()
singleExecutor.execute(readRunnable)
log("usbIoManager.start")
}
private fun debugLogDrivers() {
if (params.debug) {
getDrivers().forEach {
val device = it.device
log(
"deviceId:${device.deviceId} " +
" deviceName:${device.deviceName} " +
" deviceProtocol:${device.deviceProtocol} " +
" productName:${device.productName}" +
" productId:${device.productId}" +
" manufacturerName:${device.manufacturerName}" +
" configurationCount:${device.configurationCount}" +
" serialNumber:${device.serialNumber}" +
" vendorId:${device.vendorId}"
)
}
}
}
fun closeDevice() {
port?.close()
port = null
}
private fun receive(data: ByteArray?) {
log("receive:${HexDump.dumpHexString(data)}", "RRRRRRR")
if (data == null) return
for (byte in data) {
dataQueue.put(byte)
}
}
override fun onNewData(data: ByteArray?) {
receive(data)
}
override fun onRunError(e: Exception?) {
log("onRunError:${e?.message}")
}
private fun log(message: String, tag: String = "Heiko") {
Log.i(tag, message)
}
}
class Cmd {
companion object {
const val PREAMBLE: Byte = 0xAA.toByte()
}
var preamble: Byte? = null
var length: Byte = -1
var payload = ArrayList<Byte>()
var checkSum: Byte? = null
fun clear() {
preamble = null
length = -1
payload.clear()
checkSum = null
}
}
6.2 字節(jié)數(shù)組轉(zhuǎn)字符串工具類
附上字節(jié)數(shù)組轉(zhuǎn)字符串工具類文章來源:http://www.zghlxwxcb.cn/news/detail-415570.html
public class HexUtil {
public static byte[] hexStringToBytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}
private static byte charToByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
public static String bytesToHexString(byte[] b) {
if (b.length == 0) {
return null;
}
StringBuilder sb = new StringBuilder("");
for (int i = 0; i < b.length; i++) {
int value = b[i] & 0xFF;
String hv = Integer.toHexString(value);
if (hv.length() < 2) {
sb.append(0);
}
sb.append("0x").append(hv).append(" ");
}
return sb.toString();
}
public static String toByteString(byte b) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
return hex.toUpperCase();
}
}
6.3 UsbSerialParams
UsbSerialParams
是一個(gè)參數(shù)配置類,附上代碼文章來源地址http://www.zghlxwxcb.cn/news/detail-415570.html
data class UsbSerialParams(
var baudRate: Int, //比如 115200
var dataBits: Int, //比如 8
var stopBits: Int, //比如 UsbSerialPort.STOPBITS_1
var parity: Int, //比如 UsbSerialPort.PARITY_NONE
var debug: Boolean
)
到了這里,關(guān)于Android 沾包處理,以串口接入為例 (usb-serial-for-android)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!