清泛IT社区

标题: App Inventor 2 BLE扩展源码分析 - WriteBytes vs WriteStrings 23字节硬编码问题 [打印本页]

作者: App Inventor 2     时间: 2026-05-19 08:39
标题: App Inventor 2 BLE扩展源码分析 - WriteBytes vs WriteStrings 23字节硬编码问题
一、问题背景

用户反馈: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 的问题清单

#问题影响严重程度
123字节截断超过23字节的字符串被静默截断,无警告严重
2Null终止符默认开启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));

优点:根本解决问题
缺点:需要修改源码并重新编译扩展

八、总结

对比项WriteStringsWriteBytes
长度限制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 分析整理,仅供学习参考。





欢迎光临 清泛IT社区 (https://bbs.tsingfun.com/) Powered by Discuz! X3.3