BLE 调试助手:一个 App 完成低功耗蓝牙的扫描、连接、选特征、读取、订阅通知和发送,收到的每一包同时显示十六进制和 UTF-8 文本。
这一篇回答的搜索:appinventor 的 ble、BLE 蓝牙接收数据、如何连接 BLE 蓝牙、BLE 扩展怎么用、BluetoothLE 总是获取空列表、不同的写 UUID 与读 UUID、使用 BLE 时出现 error 3300 9005、BLE 接收数据乱码、CH582 与 App Inventor 连接。
界面

图为由最终 .aia 生成的布局预览(列表内容为示例),不是真机截图。
怎么用
准备:一块会广播的 BLE 设备(ESP32、nRF52、CH582、HM-10、micro:bit 等)。导入 .aia 后用 AI 伴侣或编译安装到手机,首次扫描时允许「附近设备」(Android 12+)或「位置信息」权限。
- 扫描:点「开始扫描」,15 秒后自动停止。附近设备按信号强弱排序,没有名字的显示为「(未命名设备)」。
中途验证:列表里应能看到你的设备名和信号 -40 ~ -90 dBm。看不到就查下面的常见问题。 - 连接:点一下设备。连上后标题栏显示「已连接 设备名」,② 里自动列出全部特征,并默认选中上次用过的那个。
- 选特征:卡片里会写出这个特征「可读 / 可写 / 可通知」。透传模块通常是两个特征:一个可写(手机→设备)、一个可通知(设备→手机),这就是「读写 UUID 不同」的原因,收发时要分别选。
- 收发:选可通知的特征点「开启通知」,设备上报的数据自动进日志;选可写的特征,输入文本或十六进制点「发送」。
中途验证:日志里出现收 你好↵ [E4 BD A0 E5 A5 BD 0A]这样的行,前面是文本、方括号里是原始字节。
下次打开 App,点「重连 设备名」会先扫描、扫到后自动连上。
核心代码
完整积木在 .aia 里,下面摘出最关键的四段,每段都能单独拖进自己的项目用。
扫描并列出设备
DeviceList 是「地址 名称 信号」用逗号连起来的一串,已按信号强弱排好序。拆开后用地址连接(ConnectWithAddress),不用序号——扫描期间列表会变,序号容易对不上。
global 设备地址表 = []
when 按钮_扫描.Click() {
设备地址表 = []
低功耗蓝牙1.StartScanning()
}
when 低功耗蓝牙1.DeviceFound() {
var 行列表 = []
设备地址表 = []
foreach 项 in split(低功耗蓝牙1.DeviceList, ",") {
var 段 = split(trim(项), " ")
if length(段) >= 3 {
listadd(设备地址表, 段[1])
listadd(行列表, 列表显示框_设备.CreateElement(段[2], join(段[1], " 信号 ", 段[length(段)], " dBm"), ""))
}
}
列表显示框_设备.Elements = 行列表
}
when 列表显示框_设备.AfterPicking() {
低功耗蓝牙1.StopScanning()
低功耗蓝牙1.ConnectWithAddress(设备地址表[列表显示框_设备.SelectionIndex])
}
连接后列出特征,并判断能读、能写、能通知
DeviceCharacteristics 是「[服务UUID, 特征UUID, 名称]」三元组的列表。Connected 事件在服务发现完成之后才触发,这时读它一定有值。
global 特征表 = []
when 低功耗蓝牙1.Connected() {
特征表 = 低功耗蓝牙1.DeviceCharacteristics
var 选项 = []
foreach 三元 in 特征表 {
listadd(选项, join(三元[3], " · ", segment(三元[2], 1, 8)))
}
下拉框_特征.Elements = 选项
}
when 下拉框_特征.AfterSelecting(selection) {
var 服务 = 特征表[下拉框_特征.SelectionIndex][1]
var 特征 = 特征表[下拉框_特征.SelectionIndex][2]
标签_特征能力.Text = join("可读:", 低功耗蓝牙1.CanReadCharacteristic(服务, 特征), " 可写:", 低功耗蓝牙1.CanWriteCharacteristic(服务, 特征), " 可通知:", 低功耗蓝牙1.CanRegisterForCharacteristic(服务, 特征))
}
收到的字节同时显示十六进制和中文(解决乱码)
一律用 RegisterForBytes 按字节接收,再自己转换:每个字节转成两位十六进制;把它们拼成 %E4%BD%A0 交给 Web 客户端的 UriDecode,就能按 UTF-8 还原出中文。文本对不上时看方括号里的 HEX,一眼就知道设备到底发了什么。
global 十六进制表 = "0123456789ABCDEF"
function 两位十六进制(n) {
return join(segment(十六进制表, floor(n / 16) + 1, 1), segment(十六进制表, n % 16 + 1, 1))
}
when 低功耗蓝牙1.BytesReceived(serviceUuid, characteristicUuid, byteValues) {
var 十六进制 = ""
var 百分号串 = ""
foreach 字节 in byteValues {
十六进制 = join(十六进制, 两位十六进制(字节), " ")
百分号串 = join(百分号串, "%", 两位十六进制(字节))
}
标签_日志.Text = join(Web客户端1.UriDecode(百分号串), " [", trim(十六进制), "]\n", 标签_日志.Text)
}
十六进制文本转字节发送
A0 01 FF、0xA0,0x01,0xff 都能识别:去掉 0X、空格、逗号后每两位一个字节,startsat 在 0123456789ABCDEF 里找到的位置减 1 就是这一位的值。
global 十六进制表 = "0123456789ABCDEF"
global 服务 = ""
global 特征 = ""
when 按钮_发送.Click() {
var 纯 = replace(replace(replace(upper(文本输入框_发送.Text), "0X", ""), " ", ""), ",", "")
var 字节 = []
var 出错 = textLength(纯) % 2 != 0
for i = 1 to textLength(纯) step 2 {
var 高 = startsat(十六进制表, segment(纯, i, 1)) - 1
var 低 = startsat(十六进制表, segment(纯, i + 1, 1)) - 1
if 高 < 0 or 低 < 0 {
出错 = true
} else {
listadd(字节, 高 * 16 + 低)
}
}
if 出错 {
底部提示条1.Show("十六进制格式不对:每个字节两位,如 A0 01 FF")
} else {
低功耗蓝牙1.WriteBytes(服务, 特征, false, 字节)
}
}
只支持「带响应写入」的特征,WriteBytes 不会报错但设备收不到——成品里先用 CanWriteCharacteristic 判断,不行就自动换成 WriteBytesWithResponse。
常见问题
| 现象 | 原因与处理 |
|---|---|
| 一直扫不到设备 | ① 没给权限:Android 12+ 要「附近设备」,旧系统要「位置信息」且要打开定位开关;② 设备没在广播——被别的手机/电脑连着时大多数 BLE 设备会停止广播,先断开那边;③ 这是经典蓝牙(HC-05/HC-06),BLE 扫不到,用蓝牙台灯遥控那种蓝牙客户端。 |
| BluetoothLE 总是获取空列表 | DeviceList 只在扫描期间逐步填充,刚点完扫描立刻读是空的。要在 DeviceFound 事件里读;直接用序号连接空列表会报 9007(列表为空)或 9006(序号越界)。 |
| 报错 3300 / 9005 | 3300 是蓝牙组件错误的外层编号,后面的 9005 表示「当前没有连接」:在 Connected 之前、或设备已经断开后调用了读、写、订阅或断开。等 Connected 事件后再操作,并在 Disconnected 里禁用收发按钮(成品就是这么做的)。 |
| 连上了,但特征列表是空的 | 个别设备服务发现慢,点「重新读取特征列表」。仍为空说明设备固件没有声明 GATT 服务。 |
| 发送后设备没反应 | ① 选的特征不可写(看「能力」那行);② 设备只认带响应写入;③ 文本模式下组件默认在字符串末尾补一个 0x00——成品已把「字符串以空字符结尾 NullTerminateStrings」设为假,自己做项目时记得也关掉;④ 设备按行解析,打开「末尾加 \n」。 |
| 收到的是乱码 | 看方括号里的 HEX:如果是 E4 BD A0 这类三字节一组,是 UTF-8 中文,本 App 能正确显示;如果是 C4 E3 这类两字节,是单片机按 GBK 发的,要改成 UTF-8 发送,或直接按 HEX 解析协议。 |
| 收到的数据只有前 20 个字节 | BLE 默认每包 20 字节。设备分包发送时会收到多次 BytesReceived,按协议里的结束符拼接;两端都支持 4.2 以上时可用 RequestMTU 调大。 |
用到的组件
相关教程:BLE 蓝牙组件文档 · WriteBytes 与 WriteStrings 的区别 · 十六进制与二进制转换
验证范围
最终 .aia 已通过:正式编辑器载入与保存重开 0 错误、后台导入、Android APK 编译,并用替身蓝牙设备执行了真实积木的 20 项业务检查(扫描解析、按地址连接、特征能力、订阅切换、HEX/中文解码、十六进制解析与非法输入、带响应写入回退、日志上限、断开与自动重连)。未在真实 BLE 设备上联调,射频与不同手机的兼容性以实测为准。详见验证记录。
扫码添加客服咨询