做硬件的用户常问:单片机那边的协议是「A5 5A 开头、带长度、带校验」的一串字节,App Inventor 能不能按这种格式收发? 收到的数据时多时少、偶尔一帧被拆成两次,怎么才能稳定解析?
能实现,不需要拓展。 用到两样现成的东西:
没有硬件也没关系:本文附一个 Python 脚本,在电脑上扮演设备。它会回复命令,还会故意把一帧拆成两半发、把两帧粘在一起发、夹一个校验错误的坏帧, 正好检验 App 的解析够不够稳。等真设备做好了,把服务器地址换成设备的 IP 即可。
协议
帧格式(所有帧都一样):
| 帧头 | 长度 | 数据 | 校验 |
|---|---|---|---|
A5 5A |
数据的字节数 N | N 个字节 | 前面所有字节相加取低 8 位 |
数据的第 1 个字节是命令:
| 方向 | 数据 | 含义 |
|---|---|---|
| App → 设备 | 01 |
读温湿度 |
| 设备 → App | 81 + 温度(2 字节,乘 10,有符号,高字节在前)+ 湿度(1 字节) |
温湿度回复 |
| App → 设备 | 02 + 01/00 |
开灯 / 关灯 |
| 设备 → App | 82 + 当前状态 |
开关灯回复 |
| 设备 → App | 90 + 序号 |
每 2 秒主动推送一次 |
例如「读温湿度」命令就是 A5 5A 01 01 01:帧头、长度 1、命令 01、校验(A5+5A+01+01 = 0x101,取低 8 位 01)。
准备
- 电脑装好 Python 3(不需要装任何库),下载 binary_device.py,运行
python binary_device.py。 窗口显示「模拟设备已启动,端口 8888」和电脑 IP。电脑和手机连同一个 Wi-Fi;IP 不对时在电脑上用ipconfig(Windows)或ifconfig查局域网 IP。 - App 里拖一个 Socket客户端(服务器地址填电脑 IP,端口
8888)、一个二进制组件, 三个按钮(按钮_读取、按钮_开灯、按钮_关灯)和四个标签(标签_发送、标签_温湿度、标签_灯、标签_推送)。
实验一:发出第一条命令
先只做「发」:打开 App 就连接,点按钮把「读温湿度」命令拼好发出去。
when Screen1.Initialize() {
Socket客户端1.Connect()
}
when 按钮_读取.Click() {
var 帧 = 二进制1.AppendChecksum(二进制1.Build([165, 90, 1, 1]), "SUM8")
Socket客户端1.SendBytes(帧)
标签_发送.Text = join("发送 ", 二进制1.ToHex(帧))
}
跑起来看:点「读取」,标签显示「发送 A5 5A 01 01 01」,电脑窗口同时打印「收到 A5 5A 01 01 01」和一行「回复 A5 5A 04 81 …」。 两边字节一致,说明拼包对了。窗口里打印「校验不对,丢弃」就回头检查拼接的内容。
实验二:拆包并解析回复
设备的回复和推送会源源不断地到来。TCP 不保证「发一次收一次」,所以不能拿到一段就当一帧, 要先接到全局「缓冲」里,再按帧头和长度一帧一帧地取。把实验一的代码换成下面这一整段:
global 缓冲 = []
global 丢弃 = 0
when Screen1.Initialize() {
Socket客户端1.Connect()
}
procedure 发送命令(数据) {
var 帧 = 二进制1.AppendChecksum(二进制1.Build([165, 90, length(数据), 数据]), "SUM8")
Socket客户端1.SendBytes(帧)
标签_发送.Text = join("发送 ", 二进制1.ToHex(帧))
}
when 按钮_读取.Click() {
发送命令([1])
}
when Socket客户端1.BytesReceived(字节列表) {
listAppend(缓冲, 字节列表)
拆帧()
}
procedure 拆帧() {
var 继续 = true
while (继续) {
var 头 = 二进制1.IndexOf(缓冲, [165, 90], 1)
if (头 == 0) {
if (length(缓冲) > 1) {
缓冲 = 二进制1.SubBytes(缓冲, length(缓冲), 1)
}
继续 = false
} else {
if (头 > 1) {
缓冲 = 二进制1.SubBytes(缓冲, 头, length(缓冲) - 头 + 1)
}
if (length(缓冲) < 4) {
继续 = false
} else {
var 帧长 = 缓冲[3] + 4
if (length(缓冲) < 帧长) {
继续 = false
} else {
var 帧 = 二进制1.SubBytes(缓冲, 1, 帧长)
缓冲 = 二进制1.SubBytes(缓冲, 帧长 + 1, length(缓冲) - 帧长)
if (二进制1.VerifyChecksum(帧, "SUM8")) {
处理一帧(帧)
} else {
丢弃 = 丢弃 + 1
}
}
}
}
}
}
procedure 处理一帧(帧) {
var 命令 = 帧[4]
if (命令 == 129) {
标签_温湿度.Text = join("温度 ", 二进制1.BytesToNumber(帧, 5, 2, true) / 10, " ℃ 湿度 ", 帧[7], " %")
} else if (命令 == 130) {
标签_灯.Text = choose(帧[5] == 1, "灯:开", "灯:关")
} else if (命令 == 144) {
标签_推送.Text = join("推送序号 ", 帧[5], " 丢弃坏帧 ", 丢弃)
}
}
跑起来看:点「读取」,标签_温湿度 显示和电脑窗口「回复」那一行对应的温湿度。例如回复 A5 5A 04 81 FF 38 2D …
显示「温度 -20 ℃ 湿度 45 %」:FF 38 按有符号是 -200,除以 10 得 -20。温度是随机的,多点几次看看零下的情况。
命令字节 129、130、144 就是十六进制的 81、82、90。拆帧的三个要点:
- 找不到帧头时只留最后一个字节(它可能是被切开的帧头的前一半),缓冲不会越攒越多;
- 帧头前面的杂字节先扔掉,缓冲总是从帧头开始;
- 不够一帧就停下等下一段数据;够了就切出一帧核对校验,再看剩下的还有没有完整帧。
实验三:开灯关灯
再加两个按钮。命令帧直接写成列表,和实验一一样拼好就发:
when 按钮_开灯.Click() {
Socket客户端1.SendBytes(二进制1.AppendChecksum([165, 90, 2, 2, 1], "SUM8"))
}
when 按钮_关灯.Click() {
Socket客户端1.SendBytes(二进制1.AppendChecksum([165, 90, 2, 2, 0], "SUM8"))
}
跑起来看:点「开灯」,电脑窗口打印「收到 A5 5A 02 02 01 04」,App 的 标签_灯 变成「灯:开」。 这个标签是设备回复后才改的,所以它显示的是设备的真实状态,不是按钮按了什么。
实验四:看半包、粘包和坏帧
什么都不用改,看 标签_推送 和电脑窗口。模拟设备每 2 秒推送一次,轮流制造三种情况:
| 电脑窗口 | 实际发生的事 | App 显示 |
|---|---|---|
| 推送(拆成两半) | 一帧分两次到达:第一次只有 3 个字节 | 等第二段到了才显示这个序号 |
| 推送(两帧粘在一起) | 两帧一次到达 | 连着处理两帧,序号跳到后一个 |
| 推送(校验错误的坏帧) | 最后一个字节被改坏 | 这一帧被丢弃,「丢弃坏帧」加 1,序号跳过它 |
跑起来看:十几秒后显示类似「推送序号 5 丢弃坏帧 1」,序号从 3 直接到 5,4 号是那个坏帧。 如果去掉拆帧、直接拿每次收到的字节当一帧解析,「拆成两半」那次会解析出错,「粘在一起」那次会丢掉后一帧。
常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
| 连不上 | IP 不对、不在同一 Wi-Fi,或电脑防火墙拦了 8888 端口 | 用 ipconfig 查局域网 IP;防火墙放行 Python |
| 电脑窗口显示「校验不对,丢弃」 | 拼帧时长度或内容写错 | 把 标签_发送 的内容和协议表逐个字节对一下 |
| 温度显示 6533.6 这种大数 | 「字节转整数」的「有符号」选成了假 | 温度可能为负,选真 |
| 解析偶尔出错 | 没有拆帧,直接把每次收到的字节当一帧 | 用实验二的拆帧写法 |
| 换成真设备后数字对不上 | 设备的字节顺序或校验算法不同 | 按设备协议文档改「高位优先」和校验类型;先用「字节转十六进制」看原始字节 |
平台说明
- 安卓、鸿蒙:本文所有积木两边写法相同,二进制组件的结果逐字节一致。
- 换成蓝牙、WebSocket 或 MQTT 连接设备时,只需要换收发积木,拼帧、拆帧、解析的部分不用改,见二进制数据专题。
扫码添加客服咨询