|
|
一、问题背景
用户反馈:WriteBytes 发送字符串硬件能收到,WriteStrings 发送却收不到。
同样的字符串数据,通过 WriteBytes 方法发送时硬件正常接收,但通过 WriteStrings 方法发送时硬件收不到或收到截断数据。
源码仓库: mit-cml/appinventor-extensions (extension/bluetoothle 分支)
关键文件:
- BluetoothLE.java(2975行)-- 公开API层
- BluetoothLEint.java(3165行)-- 内部实现层
二、核心发现:23字节硬编码限制
2.1 WriteStrings 的写入路径
位于 BLEWriteOperation.write() 方法第678-690行:
if (mClass == String.class) {
byte[] str = ((String) data.get(0)).getBytes();
// 23字节硬限制!
final int len = Math.min(23, str.length + (nullTerminateStrings ? 1 : 0));
byte[] buffer = new byte[len];
System.arraycopy(str, 0, buffer, 0, len - (nullTerminateStrings ? 1 : 0));
if (nullTerminateStrings) {
buffer[len - 1] = 0; // 默认追加 null 终止符
}
characteristic.setValue(buffer);
}
问题核心:Math.min(23, ...) 直接将发送数据截断为最多23字节,没有任何警告或异常。
2.2 WriteBytes 的写入路径
位于同一方法第703-712行:
} else {
byte[] contents = new byte[size * data.size()]; // 没有长度限制!
long value;
int i = 0;
for (Number n : (List<? extends Number>) data) {
value = n.longValue();
for (int j = 0; j < size; j++) {
contents[i++] = (byte)(value & 0xFF);
value >>= 8;
}
}
characteristic.setValue(contents);
}
关键差异:Integer/Number 路径没有硬编码长度限制,分配的缓冲区大小等于实际数据大小。
三、23字节的来源
| 项目 | 值 | 说明 | | BLE 4.0 ATT MTU 默认值 | 23 字节 | 规范规定的最小值 | | ATT 头部开销 | 3 字节 | 操作码 + 句柄 | | 实际有效载荷 | 20 字节 | 23 - 3 = 20 | | NullTerminateStrings 开销 | 1 字节 | 默认追加 \0 | | WriteStrings 实际可用 | 22 字节 | 23 - 1(null终止符) |
为什么23字节不应硬编码:
1. MTU 是可协商的:BLE 4.2+ 支持协商更大的 MTU(最大512字节)
2. Android BLE 栈自动处理分片:gatt.writeCharacteristic() 会根据协商后的 MTU 自动分片
3. 硬编码过时了:这个值是 BLE 4.0 的最小 MTU,现代设备普遍支持更大的 MTU
4. 正确做法:应使用 characteristic.getWriteType() 和协商后的 MTU 值
四、WriteStrings 的问题清单
| # | 问题 | 影响 | 严重程度 | | 1 | 23字节截断 | 超过23字节的字符串被静默截断,无警告 | 严重 | | 2 | Null终止符默认开启 | nullTerminateStrings = true,占用1字节,实际可用仅22字节 | 中等 | | 3 | 只取 data.get(0) | 只写第一个字符串,忽略列表中的其他字符串 | 中等 | | 4 | 无 MTU 协商感知 | 不查询当前连接的 MTU 大小 | 中等 |
五、WriteBytes 为什么能工作
5.1 字符串到字节的转换
toIntList() 方法将字符串的每个字节转为 Integer:
字符串 "Hello" --> 字节 [72, 101, 108, 108, 111]
--> Integer列表 (72, 101, 108, 108, 111)
5.2 Integer 路径无限制
Integer/Number 路径在写入时:
- 缓冲区大小 = size x data.size()(实际数据大小)
- 没有硬编码长度限制
- Android BLE 栈自动处理 MTU 和分片
这就是为什么用户发现 WriteBytes 能工作而 WriteStrings 不行的根本原因。
六、NullTerminateStrings 属性
| 属性 | 值 | | 属性名 | NullTerminateStrings | | 默认值 | true(在 BluetoothLEint.java 第1243行) | | 效果 | 在字符串末尾追加 \0 字节 | | 占用 | 1字节 | | Designer 可设置 | 是(可改为 false) |
当 NullTerminateStrings = true 时:实际有效载荷 = 23 - 1 = 22字节
七、解决方案
方案1:继续用 WriteBytes 发送字符串(推荐)
将字符串转为字节列表后使用 WriteBytes 发送:
// 将字符串转为字节列表
定义 字符串转字节列表(文本)
初始化局部变量 结果 = 创建空列表
对于 初始化局部变量 i = 1 到 文本长度(文本)
追加列表项(结果, 取字符的Unicode码(选择文本(文本, i)))
结束对于
返回 结果
// 发送时
当 按钮_发送.被点击 时
初始化局部变量 字节列表 = 字符串转字节列表(文本输入框_命令.文本)
调用 蓝牙LE1.WriteBytes(字节列表)
优点:无需修改源码,立即可用
缺点:需要额外的转换步骤
方案2:修改 NullTerminateStrings + 限制长度
如果坚持使用 WriteStrings:
1. 在 Designer 中将 NullTerminateStrings 设为 false
2. 确保每次发送的字符串不超过 23 字节
优点:直接使用 WriteStrings
缺点:仍有23字节限制
方案3:修改源码(根本修复)
// 修复前
final int len = Math.min(23, str.length + (nullTerminateStrings ? 1 : 0));
// 修复后 - 使用协商后的 MTU
final int mtu = gatt.getMtu();
final int headerSize = 3; // ATT header
final int maxPayload = mtu - headerSize;
final int len = Math.min(maxPayload, str.length + (nullTerminateStrings ? 1 : 0));
优点:根本解决问题
缺点:需要修改源码并重新编译扩展
八、总结
| 对比项 | WriteStrings | WriteBytes | | 长度限制 | 23字节硬编码(含null终止符仅22字节) | 无硬编码限制 | | Null终止符 | 默认追加 \0 | 不追加 | | 截断行为 | 静默截断,无警告 | 不截断 | | 多数据支持 | 只取第一个 | 支持列表 | | MTU感知 | 无 | 无(但Android栈自动处理) | | 推荐度 | 短字符串可用 | 推荐 |
一句话结论:WriteStrings 的 23 字节硬编码是 BLE 4.0 最小 MTU 的过时简化,不应硬编码。WriteBytes 走 Integer 路径无此限制,推荐使用 WriteBytes 发送字符串作为 workaround。
参考资料
- MIT App Inventor BLE 扩展源码
- BLE 4.0 规范 - ATT MTU
- Android BluetoothGatt 文档
- App Inventor 中文网
文档版本:2026.05 | 分析日期:2026-05-17 | 作者:App Inventor 2 中文网 www.fun123.cn
源码采用 Apache 2.0 授权。本文档由 ai2claw 分析整理,仅供学习参考。 |
|