ByteArray 实验 - 拼一个二进制协议帧,再把它拆开

« 返回 ByteArray 拓展文档  IoT 专题

ByteArray 实验:拼一帧二进制数据

这个实验做完你会得到什么

点一下按钮,屏幕上出现一串十六进制 AA 05 01 00 64 6E —— 这就是一帧完整的 二进制协议数据;再点一下,把它按帧头、长度、命令、参数、校验和逐段拆解回来

跟硬件打交道时,对方要的往往不是文字而是字节发送 "100"发送 0x64 是 两回事。这一篇把”怎么按协议拼字节、怎么看清拼对没有”讲通, 后面接蓝牙、UDP、串口都用得上。

项目 说明
难度 ★★★☆☆
用时 约 40 分钟
前置 无。不需要任何硬件,纯软件实验,屏幕上就能验证

一、导入扩展

UrsAI2ByteArray.zip(解压后导入 .aix)  示例工程:ByteArrayTest.aia

这个扩展把一段字节数组包成组件,提供两类操作:

类别 方法 说明
(追加到末尾) AddByte / AddWord / AddDWord / AddASCIIString / AddUTF8String 分别是 1 / 2 / 4 字节整数和字符串
(从读指针往后) ReadByte / ReadWord / ReadDWord / ReadASCIIString 读完指针自动前移
查看 ToHex / ToString / Size / Available 调试全靠 ToHex

二、界面组件

组件 命名 关键属性
按钮 按钮_拼帧 文本「拼一帧」
按钮 按钮_解析 文本「拆开看」
按钮 按钮_清空 文本「清空」
标签 标签_十六进制 字号 20、等宽,文本「–」
标签 标签_解析 高度 200,显示拆解结果
标签 标签_状态  
UrsAI2ByteArray UrsAI2ByteArray1 不可见

三、我们要拼的帧格式

自定义一个最常见的协议格式,一共 6 个字节:

位置 名称 长度 本例取值
1 帧头 1 字节 0xAA(十进制 170)
2 长度 1 字节 5(后面还有 5 个字节)
3 命令 1 字节 1 = 设置亮度
4-5 参数 2 字节 100
6 校验和 1 字节 前面几个字节相加取低 8 位

帧头是干什么的:串口/网络上的数据是流式的,接收方需要一个标志知道”新的一帧从这里开始”。 挑一个不常出现的值(0xAA = 二进制 10101010)当帧头是行业惯例。

长度字段让接收方知道还要再读几个字节才算读完一帧 —— 没有它就只能靠猜。


四、分步搭代码块

第 1 步:拼出这一帧

global 命令 = 1
global 参数 = 100

when 按钮_拼帧.Click() {
  UrsAI2ByteArray1.Clear()
  UrsAI2ByteArray1.AddByte(170)
  UrsAI2ByteArray1.AddByte(5)
  UrsAI2ByteArray1.AddByte(命令)
  UrsAI2ByteArray1.AddWord(参数)
  UrsAI2ByteArray1.AddByte((170 + 5 + 命令 + 参数) % 256)
  标签_十六进制.Text = UrsAI2ByteArray1.ToHex
  标签_状态.Text = join("共 ", UrsAI2ByteArray1.Size, " 字节")
}

中途验证:标签上出现一串十六进制,字节数显示 6这是本实验最关键的一步 —— ToHex 是唯一能让你”看见”字节的手段,拼错了立刻看得出来。

Clear 再拼。这个组件是有状态的,不清空就会在上一次的数据后面继续追加, 越拼越长 —— 这是最常见的错误,而且现象很迷惑(第一次对,第二次开始就不对)。

AddWord 是 2 个字节:100 会写成 00 6464 00,取决于 MsbFirst 属性 (高位在前 / 低位在前)。跟硬件对接时这个必须和对方一致,字节序搞反是 二进制协议里最典型的坑,表现是数值离谱地大或小。

校验和取 % 256:一个字节装不下超过 255 的数,必须取低 8 位。

第 2 步:把它拆回来

when 按钮_解析.Click() {
  UrsAI2ByteArray1.ReadIndex = 1
  var 帧头 = UrsAI2ByteArray1.ReadByte()
  var 长度 = UrsAI2ByteArray1.ReadByte()
  var 命令码 = UrsAI2ByteArray1.ReadByte()
  var 参数值 = UrsAI2ByteArray1.ReadWord()
  var 校验 = UrsAI2ByteArray1.ReadByte()
  标签_解析.Text = join("帧头 ", 帧头, "\n长度 ", 长度, "\n命令 ", 命令码, "\n参数 ", 参数值, "\n校验 ", 校验)
}

中途验证:拆出来的五个值应该是 170 / 5 / 1 / 100 / (校验和),和拼进去的一致。

读之前把 ReadIndex 归 1。读指针会随着每次 Read 往后移,不复位的话 第二次点「拆开看」就读到数据末尾之后了 —— Available 会变成 0。

读的顺序必须和写的顺序、宽度完全对应:写的时候 AddWord(2 字节), 读的时候就必须 ReadWord。用 ReadByte 去读只会拿到半个数。

第 3 步:校验和对不对

when 按钮_校验.Click() {
  UrsAI2ByteArray1.ReadIndex = 1
  var 累加 = 0
  for i = 1 to UrsAI2ByteArray1.Size - 1 {
    累加 = 累加 + UrsAI2ByteArray1.GetByteAt(i)
  }
  var 帧内校验 = UrsAI2ByteArray1.GetByteAt(UrsAI2ByteArray1.Size)
  if 累加 % 256 = 帧内校验 {
    标签_状态.Text = "校验通过"
    标签_状态.TextColor = "&HFF43A047"
  } else {
    标签_状态.Text = join("校验失败:算出 ", 累加 % 256, ",帧内是 ", 帧内校验)
    标签_状态.TextColor = "&HFFE53935"
  }
}

中途验证:点「校验」显示绿色的”校验通过”。

GetByteAt(位置)按下标随机读取,不动读指针,适合做校验这种要来回看的事; ReadByte()顺序读取,会推进指针。两者别混用。

下标从 1 开始(App Inventor 的列表也是 1 基),不是 0。

第 4 步:加一段文本

协议里常常要带设备名之类的字符串:

when 按钮_带文本.Click() {
  UrsAI2ByteArray1.Clear()
  UrsAI2ByteArray1.AddByte(170)
  UrsAI2ByteArray1.AddUTF8String("客厅灯")
  标签_十六进制.Text = UrsAI2ByteArray1.ToHex
  标签_状态.Text = join("共 ", UrsAI2ByteArray1.Size, " 字节;这三个汉字占 ", UrsAI2ByteArray1.GetUTF8ByteSize("客厅灯"), " 字节")
}

中途验证:字节数显示 10 —— 帧头 1 字节 + 三个汉字 9 字节。

一个汉字在 UTF-8 里是 3 个字节,不是 1 个。给硬件发中文之前一定要确认 对方能不能处理多字节字符 —— 很多单片机固件只认 ASCII, 那就得用 AddASCIIString(一个字符一个字节,中文会丢)。 这也是蓝牙中文乱码那类问题的根源。


五、排错表

现象 原因与处理
第二次点拼帧,字节数翻倍 没先 Clear()。组件是有状态的,会一直追加
拆出来全是 0 或读不到 ReadIndex 没归 1,指针停在末尾。Available 能看还剩几个字节
数值离谱(如 25600 而不是 100) 字节序反了。改 MsbFirst 属性,和对方约定一致
拆出来的值和写进去的对不上 读写的宽度不匹配:AddWord 必须配 ReadWord
校验和总是不对 忘了 % 256;或累加范围算错(要算到 Size - 1,最后一字节是校验本身)
中文发过去是乱码 对方不支持 UTF-8。改用 ASCII,或双方约定编码
GetByteAt(0) 报错 下标从 1 开始

六、下一步

把拼好的帧真正发出去,三条路都支持直接发字节数组:


相关文档

文档反馈