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 64或64 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 开始 |
六、下一步
把拼好的帧真正发出去,三条路都支持直接发字节数组:
- UDP 实验 ——
UDPXmitter.XmitByteArray() - BLE 低功耗蓝牙 ——
WriteBytes - HC-05 经典蓝牙 —— 发送字节
扫码添加客服咨询