我们在通过串口、TCP、UDP等方式接收协议的时候,由于单次接收数据有限,导致一条命令可能被分割成多次进行接收。
这种情况下,就需要进行沾包处理,使多次接收的数据,合并成一条数据。本文通过博主本人一个真实的工作案例,实例讲解Android串口的接入和对于沾包的处理。
我们以下方这个协议为例
这是个串口协议,Android设备通过监听串口,读取到具体的数据
| 前导帧 | 长度 | 内容 | 校验 | |
|---|---|---|---|---|
| 长度 | 1Bit | 1Bit | 0~255Bit | 1Bit |
| 值 | 0xAA | 0~255 | Json | 校验结果 |
可以看到,前导帧为1个字节,每当读取到0xAA,就代表一条命令的开始。
第二个字节是长度,占1个字节,表示内容部分占用多少个字节。
最后一个字节用特定的算法,将命令的前面部分进行计算后得到的值,用来校验这条命令是否正确。
可以在平板或手机上下载usb调试宝,设置好波特率 (比如115200,这个根据串口设备设置),然后即可监听到串口发送的数据了。

我们这里使用了usb-serial-for-android这个串口库
repositories {
...
maven { url 'https://jitpack.io' }
}
implementation 'com.github.mik3y:usb-serial-for-android:3.4.6'
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
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]
}
如果没有权限,需要先申请权限,这一步很主要,要不然后面肯定是读取不到串口的数据的。
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)
}
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?) {
//当运行出错
}
当我们要退出App的时候,需要去关闭串口
fun closeDevice() {
port?.close()
port = null
}
当我们在onNewData里,我们需要进行沾包处理。
这里我处理沾包的一个思路是在onNewData接收到的数据,存储到一个地方,然后另起一个线程,在那个线程中,再去读取数据。这样,就可以很好地规避在onNewData里,一股脑给到一个ByteArray数组,导致的拆解数据,处理多种异常情况的问题了。
而onNewData接收到的数据,我们可以存储到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
在打开串口的时候,我们去启动另一个线程。这里我使用到了线程池,newSingleExecutor是一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
private val dataQueue = LinkedBlockingQueue<Byte>()
private val singleExecutor: Executor by lazy {
Executors.newSingleThreadExecutor()
}
private val readRunnable = Runnable {
//TODO 具体实现
}
singleExecutor.execute(readRunnable) //启动线程
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
}
}
在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())
}
}
至此,对于沾包的处理就完成了
附上基于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
}
}
附上字节数组转字符串工具类
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();
}
}
Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack
我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢
似乎无法为此找到有效的答案。我正在阅读Rails教程的第10章第10.1.2节,但似乎无法使邮件程序预览正常工作。我发现处理错误的所有答案都与教程的不同部分相关,我假设我犯的错误正盯着我的脸。我已经完成并将教程中的代码复制/粘贴到相关文件中,但到目前为止,我还看不出我输入的内容与教程中的内容有什么区别。到目前为止,建议是在函数定义中添加或删除参数user,但这并没有解决问题。触发错误的url是http://localhost:3000/rails/mailers/user_mailer/account_activation.http://localhost:3000/rails/mai
当我在我的Rails应用程序根目录中运行rakedoc:app时,API文档是使用/doc/README_FOR_APP作为主页生成的。我想向该文件添加.rdoc扩展名,以便它在GitHub上正确呈现。更好的是,我想将它移动到应用程序根目录(/README.rdoc)。有没有办法通过修改包含的rake/rdoctask任务在我的Rakefile中执行此操作?是否有某个地方可以查找可以修改的主页文件的名称?还是我必须编写一个新的Rake任务?额外的问题:Rails应用程序的两个单独文件/README和/doc/README_FOR_APP背后的逻辑是什么?为什么不只有一个?
目前,Itembelongs_toCompany和has_manyItemVariants。我正在尝试使用嵌套的fields_for通过Item表单添加ItemVariant字段,但是使用:item_variants不显示该表单。只有当我使用单数时才会显示。我检查了我的关联,它们似乎是正确的,这可能与嵌套在公司下的项目有关,还是我遗漏了其他东西?提前致谢。注意:下面的代码片段中省略了不相关的代码。编辑:不知道这是否相关,但我正在使用CanCan进行身份验证。routes.rbresources:companiesdoresources:itemsenditem.rbclassItemi
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我是Ruby的新手,有些闭包逻辑让我感到困惑。考虑这段代码:array=[]foriin(1..5)array[5,5,5,5,5]这对我来说很有意义,因为i被绑定(bind)在循环之外,所以每次循环都会捕获相同的变量。使用每个block可以解决这个问题对我来说也很有意义:array=[](1..5).each{|i|array[1,2,3,4,5]...因为现在每次通过时都单独声明i。但现在我迷路了:为什么我不能通过引入一个中间变量来修复它?array=[]foriin1..5j=iarray[5,5,5,5,5]因为j每次循环都是新的,我认为每次循环都会捕获不同的变量。例如,这绝对
我对图像处理完全陌生。我对JPEG内部是什么以及它是如何工作一无所知。我想知道,是否可以在某处找到执行以下简单操作的ruby代码:打开jpeg文件。遍历每个像素并将其颜色设置为fx绿色。将结果写入另一个文件。我对如何使用ruby-vips库实现这一点特别感兴趣https://github.com/ender672/ruby-vips我的目标-学习如何使用ruby-vips执行基本的图像处理操作(Gamma校正、亮度、色调……)任何指向比“helloworld”更复杂的工作示例的链接——比如ruby-vips的github页面上的链接,我们将不胜感激!如果有ruby-
我有一个集合选择:此方法的单选按钮是什么?谢谢 最佳答案 Rails3中没有这样的助手。在Rails4中,它是collection_radio_buttons. 关于ruby-on-rails-rails上的ruby:radiobuttonsforcollectionselect,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/18525986/