Android 沾包处理,以串口接入为例 (usb-serial-for-android)

这篇具有很好参考价值的文章主要介绍了Android 沾包处理,以串口接入为例 (usb-serial-for-android)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 前言

我们在通过串口、TCPUDP等方式接收协议的时候,由于单次接收数据有限,导致一条命令可能被分割成多次进行接收。
这种情况下,就需要进行沾包处理,使多次接收的数据,合并成一条数据。本文通过博主本人一个真实的工作案例,实例讲解Android串口的接入和对于沾包的处理。

2. 协议

我们以下方这个协议为例
这是个串口协议,Android设备通过监听串口,读取到具体的数据

前导帧 长度 内容 校验
长度 1Bit 1Bit 0~255Bit 1Bit
0xAA 0~255 Json 校验结果

可以看到,前导帧为1个字节,每当读取到0xAA,就代表一条命令的开始。
第二个字节是长度,占1个字节,表示内容部分占用多少个字节。
最后一个字节用特定的算法,将命令的前面部分进行计算后得到的值,用来校验这条命令是否正确。

  • 如果命令正确,那就正常处理
  • 如果命令错误,就作丢弃处理

3. 验证串口硬件是否正常

可以在平板或手机上下载usb调试宝,设置好波特率 (比如115200,这个根据串口设备设置),然后即可监听到串口发送的数据了。

Android 沾包处理,以串口接入为例 (usb-serial-for-android)

4. 串口接入

我们这里使用了usb-serial-for-android这个串口库

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 判断是否有权限

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 请求权限

如果没有权限,需要先申请权限,这一步很主要,要不然后面肯定是读取不到串口的数据的。

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 打开设备

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有个监听,onNewData就是处理接收数据的地方了。

override fun onNewData(data: ByteArray?) {
    //当接收到数据
}

override fun onRunError(e: Exception?) {
    //当运行出错
}

4.7 关闭设备

当我们要退出App的时候,需要去关闭串口

fun closeDevice() {
    port?.close()
    port = null
}

5. 沾包处理

当我们在onNewData里,我们需要进行沾包处理。
这里我处理沾包的一个思路是在onNewData接收到的数据,存储到一个地方,然后另起一个线程,在那个线程中,再去读取数据。这样,就可以很好地规避在onNewData里,一股脑给到一个ByteArray数组,导致的拆解数据,处理多种异常情况的问题了。

onNewData接收到的数据,我们可以存储到Queue(队列),队列的特性是先进先出(通常但并非一定),这样就可以确保我们先接收到的数据先被读取处理,并且也简化了处理的流程。

5.1 常见的Queue

常见的Queue有这几种,我们这里选用的是LinkedBlockingQueue,没有数据的时候,它具有自动阻塞的能力。

  • ArrayBlockingQueue : 数组实现的有界队列,会自动阻塞,根据调用api不同,有不同特性,当队列容量不足时,有阻塞能力
    • boolean add(E e):在容量不足时,抛出异常。
    • void put(E e):在容量不足时,阻塞等待。
    • boolean offer(E e):不阻塞,容量不足时返回false,当前新增数据操作放弃。
    • boolean offer(E e, long timeout, TimeUnit unit):容量不足时,阻塞times时长(单位为timeunit),如果在阻塞时长内,有容量空闲,新增数据返回true。如果阻塞时长范围内,无容量空闲,放弃新增数据,返回false
  • LinkedBlockingQueue:链式队列,队列容量不足或为0时自动阻塞
    • void put(E e):自动阻塞,队列容量满后,自动阻塞。
    • E take():自动阻塞,队列容量为0后,自动阻塞。
  • ConcurrentLinkedQueue : 基础链表同步队列
    • boolean offer(E e):入队。
    • E peek():查看queue中的首数据。
    • E poll():取出queue中的首数据。
  • DelayQueue: 延时队列,根据比较机制,实现自定义处理顺序的队列。常用于定时任务,如:定时关机。
    • int compareTo(Delayed o):比较大小,自动升序。
    • 比较方法建议和getDelay方法配合完成。如果在DelayQueue是需要按时完成的计划任务,必须配合getDelay方法完成。
    • long getDelay(TimeUnit unit):获取计划时长的方法,根据参数TimeUnit来决定,如何返回结果值。
  • LinkedTransferQueue : 转移队列
    • boolean add(E e):队列会保存数据,不做阻塞等待。
    • void transfer(E e):是TransferQueue的特有方法。必须有消费者(take()方法调用者)。如果没有任意线程消费数据,transfer方法阻塞。一般用于处理及时消息。
  • SynchronousQueue : 同步队列,容量为0,是特殊的TransferQueue,必须先有消费线程等待,才能使用的队列。
    • boolean add(E e):父类方法,无阻塞,若没有消费线程阻塞等待数据,则抛出异常。
    • put(E e):有阻塞,若没有消费线程阻塞等待数据,则阻塞。

详细关于Queue的介绍,详见 https://blog.csdn.net/qq_37050329/article/details/116295082

5.2 启动线程

在打开串口的时候,我们去启动另一个线程。这里我使用到了线程池,newSingleExecutor是一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

private val dataQueue = LinkedBlockingQueue<Byte>()
private val singleExecutor: Executor by lazy {
    Executors.newSingleThreadExecutor()
}
private val readRunnable = Runnable {
    //TODO 具体实现
}
singleExecutor.execute(readRunnable) //启动线程

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 进行沾包处理

readRunnable中,我们去读取dataQueue的数据,当dataQueue没有数据的时候,会进行阻塞,这样就避免了性能的损耗。

val byte = dataQueue.take()

接着,如果我们读到前导帧,就假设读取到了一条命令,按顺序依次读取长度内容校验,所有的值都读取到后,需要对校验值checkSum做效验,具体校验的算法根据协议约定来。
命令校验通过后,就可以取到内容,转化为Json,进一步做业务逻辑处理了。

val PREAMBLE: Byte = 0xAA.toByte()
if (byte == PREAMBLE) { //前导帧
    cmd = Cmd()
    cmd.preamble = PREAMBLE
    log("前导帧:0x${HexUtil.toByteString(PREAMBLE)}")
    cmd.length = dataQueue.take()
    log("长度:${cmd.length}")
    readPayload(dataQueue)
    log("内容:${HexUtil.bytesToHexString(cmd.payload.toByteArray())}")
    val checkSum = dataQueue.take()
    cmd.checkSum = checkSum
    log("校验:0x${HexUtil.toByteString(checkSum)}")
    //TODO 需要对checkSum进行校验,判断命令是否正确
	val json = String(cmd.payload.toByteArray()) //内容转换为Json,这里可以做进一步逻辑处理
    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())
    }
}

至此,对于沾包的处理就完成了

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) { //前导帧
                    cmd = Cmd()
                    cmd.preamble = PREAMBLE
                    log("前导帧:0x${HexUtil.toByteString(PREAMBLE)}")
                    cmd.length = dataQueue.take()
                    log("长度:${cmd.length}")
                    readPayload(dataQueue)
                    log("payload:${HexUtil.bytesToHexString(cmd.payload.toByteArray())}")
                    val checkSum = dataQueue.take()
                    cmd.checkSum = checkSum
                    log("校验: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 字节数组转字符串工具类

附上字节数组转字符串工具类

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是一个参数配置类,附上代码文章来源地址https://www.toymoban.com/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
)

到了这里,关于Android 沾包处理,以串口接入为例 (usb-serial-for-android)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • LangChain大模型应用落地实践(二):使用LLMs模块接入自定义大模型,以ChatGLM为例

    angChain版本:0.0.147 ;(没想到第二更LangChain已经更新到147了) 图1 大模型时间线(2023-arxiv-A Survey of Large Language Models) 模型名称 企业/高校 发布时间 ERNIE Bot(文心一言) 百度 2023年3月 ChatGLM 清华大学 2023年3月 通义千问 阿里 2023年4月 MOSS 复旦大学 2023年4月 从图1中可以看出,

    2024年02月09日
    浏览(44)
  • 基于瑞芯微平台cif接口dvp相机的视频接入(ov2640、rv1126为例)

    CIF,指RK芯片中的VIP模块,用以接收Sensor数据并保存到Memory中,仅转存数据,无ISP功能 DVP,一种并行数据传输接口,即Digital Video Port HSYNC,指DVP接口的行同步信号 PCLK,指Sensor输出Pixel Clock VSYNC,指DVP接口的场同步信号 V4L2,即Video4Linux2,Linux kernel的视频处理模块 视频格式一般

    2024年02月03日
    浏览(43)
  • 最佳实践 · 塔石串口服务器接入 MODBUS 物联网平台

    串口服务器是为RS-232/RS-485/RS-422终端到TCP/IP之间完成数据转换的通讯接口协议转换器。提供RS-232终端与TCP/IP网络的数据双向透明传输,提供串口转TCP/IP功能,RS-232/RS-485/RS-422转TCP/IP的解决方案。可以让RS-232/RS-485/RS-422串口设备立即联接网络。 串口通讯服务器其实就是串口服务器

    2024年02月16日
    浏览(49)
  • 【TI毫米波雷达笔记】UART串口外设配置及驱动(以IWR6843AOP为例)

    【TI毫米波雷达】GPIO初始化、Pinmux引脚复用和UART串口外设配置及驱动(以IWR6843AOP为例) 最基本的工程建立好以后 需要给SOC进行初始化配置 最是基础配置模板 包含了时钟 MPC DSS BSS上电等等 我这里只用了一个串口 引脚为: SOC_XWR68XX_PINN4_PADBD 和 SOC_XWR68XX_PINN4_PADBD 另外 配置了

    2024年02月11日
    浏览(53)
  • stm32F407学习DAY.14 在DMA模式下进行USART串口数据收发(正点原子例程为例)

    目录 一、DMA配置 1、DMA1和DMA2的请求映射 2、DMA挂载总线 3、DMA相关库函数 ​4、DMA配置过程(以串口1为例) 1)进行时钟使能 2)等待DMA可配置 3)初始化DMA(串口1的TX为DMA2 数据流7 通道4,RX为DMA2 数据流5 通道4) a.DMA外设地址par: b.DMA存储器0地址mar: c.数据传输量ndtr: 4)

    2024年02月04日
    浏览(49)
  • 2022最新Android项目导入过程(以Android studio2021.2.1为例)

    当我们访问别人项目的时候,可能由于别人项目版本太老,总是导入不了。常常报出如下错误。 也就是Android Studio版本不支持。我们通常需要修改一些配置信息才能正确导入。 常见的Android项目结构如图。 【注释】打开类型如图 打开Android studio,新建安卓项目。待自动导入完

    2024年02月05日
    浏览(51)
  • android支付宝接入流程

    接入APP支付能力前,开发者需要完成以下前置步骤。 本文档展示了如何从零开始,使用支付宝开放平台服务端 SDK 快速接入App支付产品,完成与支付宝对接的部分。 要在您的应用中接入支付宝 App 支付能力,您需要登录支付宝开放平台open.alipay.com),在开发者中心中创建您的

    2024年04月13日
    浏览(37)
  • android amazon 支付接入

    流程: 申请 Amazon 开发者帐号 --- 在 amazon 控制台添加应用 --- 添加应用内商品(消费类商品,授权类商品,订阅类商品)--- 导出 JSON 文件 ---集成 Amazon 支付 --- 将导出的 JSON 文件 copy 到 /mnt/sdcard/目录下--- 沙河模式下测试支付 --- 上传发布 注意:亚马逊目前使用 V1 签名;2.

    2024年02月11日
    浏览(37)
  • 以ChatGPT为例进行自然语言处理学习——入门自然语言处理

    ⭐️我叫忆_恒心,一名喜欢书写博客的在读研究生👨‍🎓。 如果觉得本文能帮到您, 麻烦点个赞 👍呗! 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧,喜欢的小伙伴给个三连支持一下呗。👍⭐️❤️ Qt5.9专栏 定期更新Qt的一些项目Demo

    2023年04月23日
    浏览(83)
  • Android平台GB28181设备接入端如何实现多视频通道接入?

    技术背景 我们在设计Android平台GB28181设备接入模块的时候,有这样的场景诉求,一个设备可能需要多个通道,常见的场景,比如车载终端,一台设备,可能需要接入多个摄像头,那么这台车载终端设备可以作为主设备,然后,主设备下,配置多个通道,听起来是不是有点儿类

    2024年02月13日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包