BLE 低功耗蓝牙调试助手:扫描、连接、读写、订阅通知(附 .aia 源码)

BLE 调试助手:一个 App 完成低功耗蓝牙的扫描、连接、选特征、读取、订阅通知和发送,收到的每一包同时显示十六进制和 UTF-8 文本。

下载 BleDebugger.aia 返回搜索需求专题

这一篇回答的搜索:appinventor 的 ble、BLE 蓝牙接收数据、如何连接 BLE 蓝牙、BLE 扩展怎么用、BluetoothLE 总是获取空列表、不同的写 UUID 与读 UUID、使用 BLE 时出现 error 3300 9005、BLE 接收数据乱码、CH582 与 App Inventor 连接。

界面

BLE 调试助手界面布局预览

图为由最终 .aia 生成的布局预览(列表内容为示例),不是真机截图。

怎么用

准备:一块会广播的 BLE 设备(ESP32、nRF52、CH582、HM-10、micro:bit 等)。导入 .aia 后用 AI 伴侣或编译安装到手机,首次扫描时允许「附近设备」(Android 12+)或「位置信息」权限。

  1. 扫描:点「开始扫描」,15 秒后自动停止。附近设备按信号强弱排序,没有名字的显示为「(未命名设备)」。
    中途验证:列表里应能看到你的设备名和 信号 -40 ~ -90 dBm。看不到就查下面的常见问题。
  2. 连接:点一下设备。连上后标题栏显示「已连接 设备名」,② 里自动列出全部特征,并默认选中上次用过的那个。
  3. 选特征:卡片里会写出这个特征「可读 / 可写 / 可通知」。透传模块通常是两个特征:一个可写(手机→设备)、一个可通知(设备→手机),这就是「读写 UUID 不同」的原因,收发时要分别选。
  4. 收发:选可通知的特征点「开启通知」,设备上报的数据自动进日志;选可写的特征,输入文本或十六进制点「发送」。
    中途验证:日志里出现 收 你好↵ [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 设备上联调,射频与不同手机的兼容性以实测为准。详见验证记录。

文档反馈