Android串口通讯 | 专注业务

3,682 阅读8分钟

前言

Long long ago,大Boss交给我一个任务,起初是让我配合第三方及Android开发做一套机器,然后问题就来了,大家都知道,问题肯定百出。因为某些原因,我就莫名其妙的扛起了开发串口通讯的大旗(一万匹草泥马奔腾~),通过夜以继日的在 Github 上面一顿操作,没有找到合适的开发框架,然后就暗暗的安慰我自己(可能大家都没空做开源吧!),然后我就又一顿 Android官网 操作,可算找了一些入口,知道需要使用 NDK 开发,涉及到 C++ 开发。┭┮﹏┭┮ 呜呜~ 我好像上了个假大学,把所有学到的东西都还给老师了。

开卷开卷

言归正传,开卷。先给大家放出入口,先睹为快。接下来再听我徐徐道来。

SerialPortKit 入口

介绍

SerialPortKit 主要是基于Android系统做相应的板子串口通讯,一般都是定制开发板,由厂商做相关自定义。例如:RK3288RK3399等设备

Android开发板 RK3288

总不能因为我找不到相关的SDK就让大家也找不到,哈哈~,虽然最近两年也有不少的串口通讯开发框架,各有各有的优势吧!我最近也卷了一波,整理出来一套开源框架,欢迎大家来使用,也多提出宝贵的意见。如果可以的话,欢迎提提 pr 呀~

热爱工作

特点

  • 支持自定义通讯协议
  • 支持通讯地址校验
  • 支持发送失败重试机制
  • 支持一发一收,一发多收
  • 支持多次接收指令
  • 支持切换串口
  • 支持切换波特率
  • 支持指定接收最大数据长度
  • 支持发送/接收超时检测
  • 支持主线程/子线程
  • 支持多线程并发通讯
  • 支持自定义发送任务Task
  • 支持指令池组装
  • 支持指令工具
  • 支持统一数据结果回调
  • 支持自定义发送Task接收次数
  • 支持统一配置发送Task接收次数

哇~ 支持这么多的功能呀!

怎么使用呢?

哇~ 优点说的这么多,那么怎么使用呢?

安排,必须安排.png

在 Project build.gradle中添加

repositories {
    maven {
        name 'maven-snapshot'
        url 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
    }
}

在 app build.gradle中添加

def serialPortVersion = "1.0.4-SNAPSHOT"

implementation "io.github.zhouhuandev:serial-port-kit-manage:$serialPortVersion" // require kotlin 1.7.0

// 需要使用数据转换工具或串口搜索或完全自定义数据输入输出的开发者可使用 serial-port-kit-core
implementation "io.github.zhouhuandev:serial-port-kit-core:$serialPortVersion" // 可选

如果在 build 过程中爆错 resource android:attr/lStar not found.

.gradle/caches/transforms-2/files-2.1/3c80c501edca1d8bdce41f94be0c4104/core-1.7.0/res/values/values.xml:105:5-114:25: AAPT: error: resource android:attr/lStar not found.

是因为您当前项目的Kotlin版本低于1.7.0导致,需要强制替换统一版本

configurations.all {
    resolutionStrategy {
        force 'androidx.core:core-ktx:1.6.0'
    }
}

更详细的还是看源码吧!

SerialPortKit 入口

指令收发调度器

我们发送指令指定不能放了主线程呀!开玩笑啦~ 耗时的内容尽量还是需要放入线程中进行的。那么问题来了!针对发送一条指令以及收到这条指令的回复是完成了一次通讯。我们如何可以做到又发又收呢?

最近有一个想法一直浮现在我的脑海中,最近不是在看了 虾佬AndroidStartUp 给了我点想点灵感。发送指令与接收指令既然是一对,那么,我是不是可以把发送指令及接收指令做到一起呢?

既然问题已经抛出来了,那就开干。

发送指令及接收指令全部做到一个 Task 里面不就好了,然后通过线程池进行调度处理。

SerialPortTask

internal interface SerialPortTask {

    fun onTaskStart()

    fun run()

    fun onTaskCompleted()

    fun mainThread(): Boolean = false
}

通过调度器就可以进行 dispatch 执行任务

fun dispatch(
    task: BaseSerialPortTask,
    onCompleted: ((task: BaseSerialPortTask) -> Unit)? = null
) {
    if (task.mainThread()) {
        runOnUiThread {
            execute(task)
            onCompleted?.invoke(task)
        }
    } else {
        mExecutor.execute {
            execute(task)
            onCompleted?.invoke(task)
        }
    }
}

private fun execute(task: BaseSerialPortTask) {
    task.onTaskStart()
    task.run()
    task.onTaskCompleted()
}

发送&接收指令数据

发送与接收这一对指令完成一次通讯,肯定要一起啦~

/**
 * 发送数据到串口
 * @param task 发送数据任务
 */
fun sendBuffer(task: BaseSerialPortTask): Boolean {
    xxx
    ...
    var isSendSuccess = true
    mSerialPort?.apply {
        task.stream(outputStream = outputStream)
    }
    manager.dispatcher.dispatch(task) {
        isSendSuccess = it.isSendSuccess
        if (isSendSuccess) {
            it.sendTime = System.currentTimeMillis()
        }
    }
    if (!isSendSuccess) {
        xxx
        ...
    } else {
        if (!tasks.contains(task)) {
            tasks.add(task)
        }
    }
    return isSendSuccess
}

此时取到 Task 以后,通过 mSerialPort 获取输出流,加载入 Task 中,然后使用调度器进行分发任务,这样子指令就发送出去了。

指令发送成功以后记录下发送时间,及装载到任务队列 tasks,以便后面收到数据进行指令匹配及超时检测使用。

接下来就是处理接收到的指令啦~

/**
 * 收到串口端数据
 *
 * @param data 回调数据
 */
fun sendMessage(data: WrapReceiverData) {
    readLock.lock()
    try {
        if (invalidTasks.isNotEmpty()) invalidTasks.clear()
        tasks.forEach { task ->
            manager.config.addressCheckCall?.let {
                if (it.checkAddress(task.sendWrapData(), data)) {
                    onSuccess(task, data)
                }
            } ?: onSuccess(task, data)
        }
        // 移除无效任务
        invalidTasks.forEach { task ->
            tasks.remove(task)
        }
    } finally {
        readLock.unlock()
    }
}

接收到串口端发来的数据以后,通过遍历当前待处理任务队列 tasks,进行检测通讯协议地址的方式进行匹配 Task , 紧接着就是把数据给分发出去。但是若是不匹配的话,就会按照默认策略进行分发,就是所有发送过指令的 Task 都会收到返回的数据。但是非常,非常,非常的不建议这样子去做。有可能会与通讯次数相违背(极端情况下,就是我并发发送出去的指令,只收到了一个指令的数据,而另外一个指令则会留在待分发任务队列中且被会超时机制捕获而报出超时)。

还有一种场景,也可能涉及到,就是发送了一次指令,但是回来的数据因为要做正常的交互,类似于 TCP 的三次握手。

我发送了一次指令,例如:0xAA 0xA1 0x00 0xB5,然后下位机就立即进行回复了一次收到指令 0xAA 0xA1 0x00 0xB5 代表”通讯正常,已收到了指令,我现在要去处理啦~“,然后再把处理的结果数据再次返回。相当于一对多的关系。

private fun onSuccess(task: BaseSerialPortTask, data: WrapReceiverData) {
    if (task.receiveCount < manager.config.receiveMaxCount) {
        task.receiveCount++
        task.waitTime = System.currentTimeMillis()
        switchThread(task) {
            task.onDataReceiverListener().onSuccess(data.apply {
                duration = abs(task.waitTime - task.sendTime)
            })
        }
        if (task.receiveCount == manager.config.receiveMaxCount) {
            invalidTasks.add(task)
        }
    } else {
        invalidTasks.add(task)
    }
}

通过对比当前 TaskConfig 配置的 ReceiveMaxCount 进行处理,然后进行切换线程以及分发回调。

通讯协议自定义校验

大多时候,我们都是与硬件工程师定制好的通讯协议方案,那么问题就来了,我怎么过滤与校验这个数据的准确性呢!怎么进行后续的分发呢?哈哈~不要慌,笔者已经考虑到啦!

SerialPortKit.newBuilder(this)
// 是否自定义校验下位机发送的数据正确性,把校验好的Byte数组装入WrapReceiverData
.isCustom(true, object : OnDataCheckCall {
    override fun customCheck(
        inputStream: InputStream,
        onDataPickCall: (WrapReceiverData) -> Unit
    ): Boolean {
        val tempBuffer = ByteArray(64)
        val bodySize = inputStream.read(tempBuffer)
        return if (bodySize > 0) {
            onDataPickCall.invoke(WrapReceiverData(tempBuffer, bodySize))
            true
        } else {
            false
        }
    }
})

可以通过增加自定义接收回调做相应的过滤以及规则的处理。然后把处理好的数据,通过 onDataPickCall.invoke(WrapReceiverData(buffer, size)) 进行回调过去,进行继续的处理。

通讯协议地址匹配校验

我们在收到下位机发送的指令的时候,要进行匹配 Task,那么根据地址位或者命令位进行匹配岂不是更好!

SerialPortKit.newBuilder(this)
// 校验发送指令与接收指令的地址位,相同则为一次正常的通讯
.addressCheckCall(object : OnAddressCheckCall {
    override fun checkAddress(
        wrapSendData: WrapSendData,
        wrapReceiverData: WrapReceiverData
    ): Boolean {
        return wrapSendData.sendData[1] == wrapReceiverData.data[1]
    }
})

我个人是非常非常非常的建议实现地址匹配的。

WrapSendData 自定义收发超时时长

默认发送超时时长为 3000ms,等待超时时长为 300ms,自定义最大接收次数 0

注:自定义最大接收次数比全局配置最大接收次数优先级高。当自定义最大接收次数为 0 代表默认使用全局最大接收次数

data class WrapSendData
@JvmOverloads constructor(
    var sendData: ByteArray,
    var sendOutTime: Int = 3000,
    var waitOutTime: Int = 300,
    @IntRange(from = 0, to = 3)
    var receiveMaxCount: Int = 0
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as WrapSendData

        if (!sendData.contentEquals(other.sendData)) return false
        if (sendOutTime != other.sendOutTime) return false
        if (waitOutTime != other.waitOutTime) return false
        if (receiveMaxCount != other.receiveMaxCount) return false

        return true
    }

    override fun hashCode(): Int {
        var result = sendData.contentHashCode()
        result = 31 * result + sendOutTime
        result = 31 * result + waitOutTime
        result = 31 * result + receiveMaxCount
        return result
    }
}

WrapSendData 发送数据

SerialPortHelper.portManager.send(WrapSendData(byteArrayOf(0xAA.toByte(),0xA1.toByte(),0x00.toByte(), 0xB5.toByte())),
    object : OnDataReceiverListener {
        override fun onSuccess(data: WrapReceiverData) {
            Log.d(TAG, "响应数据:${TypeConversion.bytes2HexString(data.data)}")
        }

        override fun onFailed(wrapSendData: WrapSendData, msg: String) {
            Log.e(TAG,"发送数据: ${TypeConversion.bytes2HexString(wrapSendData.sendData)}, $msg")
        }

        override fun onTimeOut() {
            Log.e(TAG, "发送或者接收超时")
        }
    })

自定义Task 发送数据

每条指令的发送,在底层是以每个单独的 Task 执行发送,互不干扰。自定义 Task 继承父类 BaseSerialPortTask,同时可监控发送任务开始前做相应的操作,也可以监控发送任务完成后作相应的任务操作,于此同时,可以切换当前发送任务以及最终的 OnDataReceiverListener 监听回调是否执行在主线程。默认是在子线程中执行及回调。

自定义Task

class SimpleSerialPortTask(
    private val wrapSendData: WrapSendData,
    private val onDataReceiverListener: OnDataReceiverListener
) : BaseSerialPortTask() {
    override fun sendWrapData(): WrapSendData = wrapSendData

    override fun onDataReceiverListener(): OnDataReceiverListener = onDataReceiverListener

    override fun onTaskStart() {

    }

    override fun onTaskCompleted() {

    }

    override fun mainThread(): Boolean {
        return false
    }
}


发送Task

SerialPortHelper.portManager.send(SimpleSerialPortTask(WrapSendData(SenderManager.getSender().sendStartDetect()), object : OnDataReceiverListener {
    override fun onSuccess(data: WrapReceiverData) {
        Log.d(TAG, "响应数据:${TypeConversion.bytes2HexString(data.data)}")
    }

    override fun onFailed(wrapSendData: WrapSendData, msg: String) {
        Log.e(
            TAG,
            "发送数据: ${TypeConversion.bytes2HexString(wrapSendData.sendData)}, $msg"
        )
    }

    override fun onTimeOut() {
        Log.e(TAG, "发送或者接收超时")
    }
}))

超时检测

有可能发出的指令一直没收到数据或者有接收到过数据,但是距离上一个数据已经超时,那么这就是一个无效的 Task,此时就需要把其进行移除。

/**
 * 检查超时无效任务
 */
fun checkTimeOutTask() {
    readLock.lock()
    try {
        if (invalidTasks.isNotEmpty()) invalidTasks.clear()
        tasks.forEach { task ->
            if (isTimeOut(task)) {
                switchThread(task) {
                    task.onDataReceiverListener().onTimeOut()
                }
                invalidTasks.add(task)
            }
        }
        // 移除无效任务
        invalidTasks.forEach { task ->
            tasks.remove(task)
        }
    } finally {
        readLock.unlock()
    }
}

/**
 * 检测是否超时
 */
private fun isTimeOut(task: BaseSerialPortTask): Boolean {
    val currentTimeMillis = System.currentTimeMillis()
    return if (task.waitTime == 0L) {
        // 表示一直没收到数据
        val sendOffset = abs(currentTimeMillis - task.sendTime)
        sendOffset > task.sendWrapData().sendOutTime
    } else {
        // 有接收到过数据,但是距离上一个数据已经超时
        val waitOffset = abs(currentTimeMillis - task.waitTime)
        waitOffset > task.sendWrapData().waitOutTime
    }
}

重试机制

当发送指令失败,导致激活了重试机制。

/**
 * 发送数据到串口
 * @param task 发送数据任务
 */
fun sendBuffer(task: BaseSerialPortTask): Boolean {
    xxx
    ...
    if (!isSendSuccess) {
        // 捕获到发送指令失败,代表串口连接有问题,打开重试机制(重新打开串口且重新发送命令)
        onRetryCall?.let {
            if (it.retry()) {
                it.call(task)
            } else {
                switchThread(task) {
                    task.onDataReceiverListener()
                        .onFailed(
                            task.sendWrapData(),
                            "Failed to send, retried ${manager.retryCount} time."
                        )
                }
            }
        }
    } else {
        if (!tasks.contains(task)) {
            tasks.add(task)
        }
    }
    return isSendSuccess
}

此时就会重新打开当前该串口,并进行重新发送。

helper.onRetryCall = object : OnRetryCall {
    override fun retry(): Boolean = retryCount in 1..MAX_RETRY_COUNT

    override fun call(task: BaseSerialPortTask) {
        if (config.debug) {
            Log.d(TAG, "Retry opening the serial port for ${retryCount++}ed!")
        }
        if (open()) {
            send(task)
        }
    }
}

切换串口&波特率

有可能在开发的过程中会涉及到切换串口或者切换波特率的情况,那么肯定给广大朋友提供这样的 API 呀!那是我对大家深沉的热爱!

说的我都害羞了.gif

直接操作,问题不大。

@JvmOverloads
fun switchDevice(path: String = config.path, baudRate: Int = config.baudRate): Boolean {
    check(path != "") { "Path is must important parameters,and it cannot be null!" }
    check(baudRate >= 0) { "BaudRate is must important parameters,and it cannot be less than 0!" }
    config.path = path
    config.baudRate = baudRate
    return open()
}

统一监听数据接口

支持除任务以外的数据回调,增加监听。与发送 Task 任务收到的回调数据是互斥关系(剔除了 Task 回调的数据),优先级低于 Task 任务回调。

此处回调不参与校验地址位,但是仍然可选参与自定义指令规则。


    override fun onResume() {
        super.onResume()
        // 增加统一监听回调
        SerialPortHelper.portManager.addDataPickListener(onDataPickListener)
    }

    override fun onPause() {
        super.onPause()
        // 移除统一监听回调
        SerialPortHelper.portManager.removeDataPickListener(onDataPickListener)
    }

    private val onDataPickListener: OnDataPickListener = object : OnDataPickListener {
        override fun onSuccess(data: WrapReceiverData) {
            Log.d(TAG, "统一响应数据:${TypeConversion.bytes2HexString(data.data)}")
        }
    }

总结

不知道符不符合大家的胃口!这个也是我考虑了良久,甚至睡觉都睡不着,还想着给大家卷出来,嗷~ 不是啦,是开源出来。若是哪里有不足的地方,还请多多提 issues and pr

欢迎Start SerialPortKit 入口