数据传输与自定义命令
本文档介绍 Flutter-JL_Home SDK 中与设备进行自定义数据收发相关的完整能力:通过 BleCustomCmdManager 向已连接的蓝牙设备发送自定义字节数据,通过 BleCustomCmdProcessor 与 BleEventStream 接收设备回传的自定义数据,以及官方示例应用 CustomCmdPage 中完整的 HEX/ASCII/UTF-8 编解码交互实现。
Purpose and Scope
本页覆盖「数据传输与自定义命令」这一能力的端到端实现:
- 发送路径:
BleCustomCmdManager.sendCustomCommand→BleBaseManager.invokeMethod→ 原生层sendCustomCommand方法通道; - 接收路径:原生事件 →
BleBaseEventProcessor过滤 →BleCustomCmdProcessor.customCommandData→ 对外统一流BleEventStream.customCommandData; - 事件与方法常量:
BleMethodConstants.methodSendCustomCommand、BleMethodConstants.argCustomData、BleEventConstants.typeCustomDataUpdate、BleEventConstants.keyCustomData; - 示例应用:
CustomCmdPage如何把文本编码为 UTF-8 字节、以 HEX/ASCII/UTF-8 三种视图预览数据,并订阅接收流。
与自定义命令无关的兄弟能力(如设备扫描连接、OTA 升级、音量控制、消息推送等)分别属于各自的目录页,本页不展开。SDK 顶层 API 入口(BleBaseManager、BleBaseEventProcessor、BleEventStream)仅在本页作为被调用的基础设施说明,其完整机制见对应文档。
Overview
JL(杰理)芯片生态的 BLE 设备除了标准服务外,通常还开放一条透传通道:App 可以把任意字节数组(如指令码、配置参数、私有协议帧)写入设备,设备也会异步回传自定义数据。SDK 为此提供了一对对称接口:
- 发送:
BleCustomCmdManager.sendCustomCommand(Uint8List data)—— 静态方法,将Uint8List通过 MethodChannel 交给原生层写入设备; - 接收:
BleEventStream.customCommandData—— 静态 Stream,订阅后即可持续收到设备回传的Uint8List数据帧。
设计上的关键决策是对称性与解耦:
- 发送与接收都基于
Uint8List原始字节,不规定业务协议——上层(App)自行决定如何组帧与解析,SDK 保持协议无关; - 接收端采用「事件总线 + 类型过滤 + 流映射」模式:原生层把所有事件汇入统一事件流,
BleBaseEventProcessor.filterByType只挑出typeCustomDataUpdate类型的事件,再通过getValueFromEvent取出载荷并转换为Uint8List; - 对外暴露统一的门面类
BleEventStream,App 不必感知事件内部结构。
典型使用场景:私有指令下发(如查询固件信息、设置参数)、透传调试(HEX 指令工具)、厂商自定义协议的数据交换。
Architecture
flowchart TD
subgraph sg_App["应用层 (example)"]
CustomCmdPage["CustomCmdPage<br/>HEX/ASCII/UTF-8 预览 + 发送"]
end
subgraph sg_Sdk["SDK 层 (jl_home)"]
SendMgr["BleCustomCmdManager<br/>sendCustomCommand"]
EventStream["BleEventStream<br/>customCommandData"]
CustomProc["BleCustomCmdProcessor<br/>filter + map"]
BaseMgr["BleBaseManager<br/>invokeMethod"]
BaseProc["BleBaseEventProcessor<br/>filterByType / getValueFromEvent"]
MethodConst["BleMethodConstants<br/>methodSendCustomCommand / argCustomData"]
EventConst["BleEventConstants<br/>typeCustomDataUpdate / keyCustomData"]
end
subgraph sg_Native["原生层 (Android/iOS)"]
Channel["MethodChannel / EventChannel"]
Device["BLE 设备"]
end
CustomCmdPage -->|"sendCustomCommand(Uint8List)"| SendMgr
SendMgr -->|"invokeMethod"| BaseMgr
BaseMgr -->|"方法名 + 参数"| MethodConst
BaseMgr -->|"MethodChannel"| Channel
Channel -->|"写入透传通道"| Device
Device -->|"回传数据事件"| Channel
Channel -->|"统一事件流"| BaseProc
BaseProc -->|"filterByType(typeCustomDataUpdate)"| EventConst
BaseProc -->|"getValueFromEvent → keyCustomData"| CustomProc
CustomProc -->|"Stream<Uint8List>"| EventStream
EventStream -->|"订阅"| CustomCmdPage
架构说明
- 发送侧是一条单向调用链:页面 → 管理器 → 基础管理器 → 原生通道 → 设备。
BleCustomCmdManager是薄封装,职责仅是拼装方法名与参数并委托给BleBaseManager.invokeMethod,真正的平台桥接发生在原生层。 - 接收侧是一条反向的数据流:设备 → 原生通道 → 统一事件流 → 类型过滤 → 载荷提取 → 字节转换 → 对外 Stream。
BleCustomCmdProcessor是纯 Dart 的转换器,不接触平台通道。 - 常量类(
BleMethodConstants/BleEventConstants)是发送与接收两侧的契约:方法名sendCustomCommand与参数键argCustomData构成发送协议,事件类型typeCustomDataUpdate与载荷键keyCustomData构成接收协议。两端必须与原生层保持一致,是防止「字符串魔法值」散落各处的关键设计。
发送路径:BleCustomCmdManager
发送侧的核心实现非常精简——BleCustomCmdManager 是典型的门面(Facade)+ 静态工具类,把复杂的平台桥接隐藏在一个方法后面:
class BleCustomCmdManager {
static Future<void> sendCustomCommand(Uint8List data) async {
await BleBaseManager.invokeMethod(
BleMethodConstants.methodSendCustomCommand,
arguments: {BleMethodConstants.argCustomData: data},
);
}
}
Source: ble_custom_cmd_manager.dart
关键设计点
- 静态方法,无状态:整个类只有一个静态方法,不维护任何实例状态。这意味着发送动作与连接状态、页面生命周期完全解耦——任何地方(页面、后台任务、定时器)都可以直接调用。
Uint8List作为统一载荷类型:data直接作为 MethodChannel 参数传入。Flutter 的Uint8List会被自动编码为原生层的字节数组(Android 为ByteArray,iOS 为NSData),无需手动序列化。- 委托给
BleBaseManager.invokeMethod:SDK 内所有平台方法调用都收敛到这一个基础方法,统一处理 MethodChannel 的创建、异常抛出与日志,避免每个管理器重复样板代码。 - 契约集中在常量类:方法名
sendCustomCommand与参数键argCustomData定义在BleMethodConstants中:
/// 发送自定义命令
static const String methodSendCustomCommand = "sendCustomCommand";
Source: ble_method_constants.dart
这样做的意义在于:原生侧(Android/iOS)只要按同一常量实现 sendCustomCommand 方法并读取 argCustomData 键,Dart 侧与原生侧就不会因字符串拼写不一致而静默失败。
发送时序
- App 构造
Uint8List(例如把文本utf8.encode(...)或把 HEX 字符串解析为字节); - 调用
BleCustomCmdManager.sendCustomCommand(data); BleBaseManager.invokeMethod通过 MethodChannel 调用原生方法sendCustomCommand,参数{argCustomData: data};- 原生层把字节写入 BLE 透传特征(写入响应由原生层内部处理);
Future完成即代表方法调用已被原生层接收(注意:并不代表设备已确认收到——确认语义由上层协议自行定义)。
接收路径:BleCustomCmdProcessor
接收侧采用「统一事件流 + 类型过滤 + 转换映射」的管道式设计:
/// Ble custom cmd processor
class BleCustomCmdProcessor{
static Stream<Uint8List> get customCommandData {
return BleBaseEventProcessor.filterByType(
BleEventConstants.typeCustomDataUpdate,
).map((event) {
try {
final data = BleBaseEventProcessor.getValueFromEvent(event);
final customData = data[BleEventConstants.keyCustomData];
if (customData == null) {
return Uint8List(0);
}
if (customData is List) {
if (customData is List<int>) {
return Uint8List.fromList(customData);
}
final List<int> result = [];
for (var element in customData) {
if (element is int) {
result.add(element);
} else if (element is num) {
result.add(element.toInt());
} else {
return Uint8List(0);
}
}
return Uint8List.fromList(result);
}
return Uint8List(0);
} catch (e) {
return Uint8List(0);
}
});
}
}
Source: ble_custom_cmd_processor.dart
管道各阶段详解
| 阶段 | 实现 | 职责 |
|---|---|---|
| 1. 事件过滤 | BleBaseEventProcessor.filterByType(BleEventConstants.typeCustomDataUpdate) | 从全局事件流中只保留自定义数据更新事件,其他类型事件(音量、OTA、消息推送等)全部丢弃 |
| 2. 载荷提取 | BleBaseEventProcessor.getValueFromEvent(event) | 把事件对象解包为 Map,得到携带业务数据的字典 |
| 3. 取值 | data[BleEventConstants.keyCustomData] | 按契约键取出原始数据字段 |
| 4. 类型归一 | 分支处理 List / List<int> / 非 int 元素 | 把原生层可能返回的各种列表形态统一转换为 Uint8List |
| 5. 容错 | null、非法元素、异常均返回 Uint8List(0) | 保证 Stream 永不因单帧脏数据而中断 |
为什么返回空数组而不是抛异常?
这是接收管道最重要的设计决策:订阅方拿到 Uint8List(0) 比收到异常更安全。理由如下:
- 自定义数据是高频、异步、不可控的外部输入,任何一帧数据解析失败都不应该影响后续帧;
- Stream 在
map内抛出异常会导致整个订阅链断开,App 需要重新订阅才能继续接收,这是不可接受的脆弱性; - 空数组是明确的「无有效载荷」信号,订阅方可以用
data.isEmpty快速判断并跳过。
代价是错误被静默吞掉:如果原生层长期返回格式不符的数据,问题难以定位。因此该设计依赖原生层严格遵守 typeCustomDataUpdate / keyCustomData 契约,调试时建议在原生侧先行校验。
对外门面:BleEventStream
SDK 把该 Stream 汇总到统一门面类 BleEventStream 中,与所有其他设备事件流并列:
// Get custom data
static Stream<Uint8List> get customCommandData =>
BleCustomCmdProcessor.customCommandData;
Source: ble_event_stream.dart
这保证了 API 一致性:App 无论订阅音量、电量还是自定义数据,都从同一个 BleEventStream 静态属性获取,学习成本低,也便于在门面层统一添加日志或调试能力。
Core Flow:一次完整的数据收发
sequenceDiagram
participant Page as CustomCmdPage
participant Mgr as BleCustomCmdManager
participant Base as BleBaseManager
participant Native as 原生层 (MethodChannel)
participant Dev as BLE 设备
participant Proc as BleCustomCmdProcessor
participant Stream as BleEventStream.customCommandData
Note over Page: initState 中订阅接收流
Page->>Stream: listen((data) => setState(...))
Stream-->>Page: 订阅建立
Note over Page: 用户点击发送
Page->>Page: utf8.encode(text) → Uint8List
Page->>Mgr: sendCustomCommand(data)
Mgr->>Base: invokeMethod("sendCustomCommand", {argCustomData: data})
Base->>Native: MethodChannel 调用
Native->>Dev: 写入透传特征
Native-->>Base: 方法调用返回
Base-->>Mgr: Future 完成
Mgr-->>Page: sendCustomCommand 返回
Note over Dev: 设备异步回传数据
Dev-->>Native: 透传特征通知
Native-->>Proc: 事件 typeCustomDataUpdate
Proc->>Proc: filterByType + getValueFromEvent + 转 Uint8List
Proc-->>Stream: Uint8List 数据帧
Stream-->>Page: onData 回调
Page->>Page: HEX / ASCII / UTF-8 三种视图展示
时序要点
- 订阅先于发送:页面在
initState阶段订阅customCommandData,保证设备回传的第一帧数据也不会丢失; - 发送是异步但不等设备确认:
sendCustomCommand的Future只表示原生方法调用已发出,回传数据完全由接收流异步送达,两者通过Uint8List字节流间接关联(无请求-响应配对,属于异步消息模型); - 回传经过纯 Dart 管道:从原生事件到页面回调之间没有任何平台通道参与,全部由
BleBaseEventProcessor与BleCustomCmdProcessor完成,便于单元测试。
示例应用:CustomCmdPage
官方示例中的 CustomCmdPage 是自定义命令能力的参考实现,展示了完整的交互闭环:文本输入 → UTF-8 编码 → 预览 → 发送 → 接收 → 三种视图解码展示。
页面状态与订阅
/// Custom command page for sending and receiving data
class CustomCmdPage extends StatefulWidget {
const CustomCmdPage({super.key});
State<CustomCmdPage> createState() => _CustomCmdPageState();
}
class _CustomCmdPageState extends State<CustomCmdPage> {
...
Uint8List? _receivedData;
StreamSubscription<Uint8List>? _dataSubscription;
final TextEditingController _textController = TextEditingController();
final FocusNode _textFocusNode = FocusNode();
Source: custom_cmd_page.dart 与 custom_cmd_page.dart
关键点:
_dataSubscription持有订阅句柄,页面销毁时(dispose)必须取消,防止内存泄漏与回调访问已销毁的 State;_receivedData是Uint8List?——null表示「尚未收到任何数据」,与「收到一帧空数据」在 UI 上区分开(_noDataReceivedText = 'No data received yet');- 输入框用
TextEditingController管理,配合FocusNode在点击发送时收起键盘(FocusScope.of(context).unfocus())。
发送按钮回调
FocusScope.of(context).unfocus();
_sendCustomCmdToDevice();
};
Source: custom_cmd_page.dart
页面把用户输入的文本按 UTF-8 编码为 Uint8List,再调用 BleCustomCmdManager.sendCustomCommand,并在成功后通过 SnackBar 提示 Sent N bytes successfully;若输入为空则提示 Please enter some text,编码失败则提示 Failed to convert text to bytes。这些文案常量集中在页面顶部(_sentSuccessPrefix、_pleaseEnterTextMessage、_failedToConvertMessage 等),便于后续国际化替换——事实上示例工程已通过 l10n 生成了多语言版本(如日文的 sendCustomCmd => 'カスタムコマンドを送信する')。
数据展示:HEX / ASCII / UTF-8 三视图
接收到的 Uint8List 在 UI 上以三种视角呈现,这是调试透传数据的标准做法:
| 视图 | 颜色常量 | 逻辑 | 用途 |
|---|---|---|---|
| HEX | _hexDataColor (blueGrey) | 每字节格式化为两位大写十六进制 | 查看原始帧结构、校验位 |
| ASCII | _asciiDataColor (green) | 可打印范围(32–126)直接显示字符,其余显示 [Non-ASCII characters] | 快速阅读可读文本 |
| UTF-8 | _utf8DataColor (orange) | utf8.decode,失败显示 [UTF-8 decode error] | 查看中文字符串等多字节文本 |
ASCII 范围常量 _asciiMinChar = 32、_asciiMaxChar = 126 定义了可打印区间;HEX 填充常量 _hexPadLeftLength = 2、_hexPadLeftChar = '0' 保证每字节固定两位输出。三种视图共用一份 Uint8List,互不干扰,体现了「原始字节单一数据源 + 多视图派生」的 UI 模式。
Usage Examples
基本用法:发送文本数据
将用户输入编码为 UTF-8 字节并发送(示例页面的核心交互):
final Uint8List data = Uint8List.fromList(utf8.encode(text));
await BleCustomCmdManager.sendCustomCommand(data);
Source: custom_cmd_page.dart(
dart:convert的utf8与dart:typed_data的Uint8List导入)
基本用法:订阅接收流
在 initState 中订阅,dispose 中取消——这是推荐的生命周期管理模式:
StreamSubscription<Uint8List>? _dataSubscription;
void initState() {
super.initState();
_dataSubscription = BleEventStream.customCommandData.listen((data) {
if (!mounted) return;
setState(() => _receivedData = data);
});
}
void dispose() {
_dataSubscription?.cancel();
super.dispose();
}
Sources:
- ble_event_stream.dart(流定义)
- custom_cmd_page.dart(订阅句柄模式)
高级用法:发送自定义二进制指令
发送任意原始字节(例如私有协议帧 0xAA 0x55 0x01 0x00 0x02):
final Uint8List frame = Uint8List.fromList([0xAA, 0x55, 0x01, 0x00, 0x02]);
await BleCustomCmdManager.sendCustomCommand(frame);
Source: ble_custom_cmd_manager.dart(方法签名即
Uint8List入参,天然支持二进制)
Configuration Options(协议常量)
SDK 通过常量类集中管理发送/接收两侧的协议契约。这些常量必须与原生层实现保持一致,属于协议配置而非运行时可调参数:
| 常量 | 值(按源码注释/命名推断) | 所属文件 | 用途 |
|---|---|---|---|
BleMethodConstants.methodSendCustomCommand | "sendCustomCommand" | ble_method_constants.dart | MethodChannel 方法名:发送自定义命令 |
BleMethodConstants.argCustomData | argCustomData(键名) | ble_method_constants.dart | 发送方法参数键:携带 Uint8List 载荷 |
BleEventConstants.typeCustomDataUpdate | typeCustomDataUpdate(事件类型) | ble_event_constants.dart | 事件类型:自定义数据更新 |
BleEventConstants.keyCustomData | keyCustomData(事件载荷键) | ble_event_constants.dart | 事件载荷键:取出自定义数据字段 |
说明:
argCustomData、typeCustomDataUpdate、keyCustomData的精确字符串值定义于对应常量类中,本页通过它们在 ble_custom_cmd_manager.dart 与 ble_custom_cmd_processor.dart 中的使用位置加以确认。
API Reference
BleCustomCmdManager.sendCustomCommand(Uint8List data): Future<void>
发送自定义命令数据到当前连接的设备。
参数:
data(Uint8List):要发送的原始字节载荷。可以是文本的 UTF-8 编码,也可以是任意二进制帧。
返回: Future<void>,在 MethodChannel 方法调用完成时完成。注意该 Future 只表示调用已送达原生层,不保证设备已消费数据。
抛出:
PlatformException:当设备未连接或原生层发送失败时,由BleBaseManager.invokeMethod向上传播(发送前建议先确认连接状态)。
源码: ble_custom_cmd_manager.dart
BleCustomCmdProcessor.customCommandData: Stream<Uint8List>
自定义数据接收流(处理器层)。
返回: Stream<Uint8List>。过滤 typeCustomDataUpdate 类型事件并转换为 Uint8List;解析失败或载荷为空时产出 Uint8List(0),不会抛出异常或中断流。
源码: ble_custom_cmd_processor.dart
BleEventStream.customCommandData: Stream<Uint8List>
对外统一门面属性,等价于 BleCustomCmdProcessor.customCommandData,App 应优先使用本入口以保持 API 一致性。
Failure Modes, Edge Cases & Concurrency
1. 接收数据的类型与值异常
处理器对回传载荷做了多层防御(ble_custom_cmd_processor.dart):
| 场景 | 行为 | 说明 |
|---|---|---|
customData == null | 返回 Uint8List(0) | 原生层事件缺少载荷键 |
载荷是 List<int> | Uint8List.fromList 直接转换 | 最常见的正常路径 |
载荷是含 num 元素的 List | 逐个 toInt() 归一 | 兼容原生层返回 double 型字节 |
| 载荷含非数值元素 | 返回 Uint8List(0) | 整帧视为无效 |
| 载荷不是 List | 返回 Uint8List(0) | 类型契约被破坏 |
| 转换过程抛异常 | catch (e) 返回 Uint8List(0) | 兜底,保证流不断 |
边界语义:Uint8List(0) 既是错误哨兵也是合法空帧,订阅方若需区分「错误」与「空数据」,应在协议层定义(例如约定帧头+长度校验),SDK 层刻意不做业务判定。
2. 发送失败与连接状态
sendCustomCommand 依赖当前 BLE 连接。若设备未连接或连接已断开,原生层会抛出 PlatformException。示例页面的 ConnectionStateManager(connection_state_manager.dart)专门维护连接状态,页面据此决定发送按钮可用性(_sendButtonDisabledColor)。最佳实践:发送前检查连接状态,并对 PlatformException 做 try/catch 或 catchError 处理,避免未捕获异常导致红屏。
3. 无请求-响应配对(异步消息模型)
发送与接收是两条独立的异步通道:一次 sendCustomCommand 调用后,设备可能回传零帧、一帧或多帧数据,SDK 不做关联。并发/一致性关注点:
- 高频连续发送时,回传帧的顺序由设备端决定,SDK 不保证与发送顺序一一对应;
- 若业务需要「请求→响应」配对(如查询指令),上层需在载荷中自行加入帧序号/事务 ID,并在订阅回调中匹配;
- 多个订阅者同时
listen同一BleEventStream.customCommandData时,各自独立收到完整事件流(取决于底层BleBaseEventProcessor的广播实现),上层不应假设「一人消费、他人不消费」。
4. 订阅生命周期
页面未取消订阅会导致:内存泄漏、setState 在 dispose 后调用(示例页面用 if (!mounted) return; 防御)、回调触发已销毁的 State。这是 Flutter 异步编程的经典坑,示例代码给出了正确范式。
5. 空帧/噪声数据的 UI 呈现
_receivedData 为 null 与 Uint8List(0) 在 UI 上分别显示「No data received yet」与空数据区。HEX 视图中空数组显示为空字符串而非错误,避免干扰调试。
Performance & Operational Notes
- 载荷编码开销:
Uint8List.fromList与utf8.encode均为 O(n) 操作,对典型指令帧(几十到几百字节)开销可忽略;应避免在回调中对大数据帧做重复解码(示例页面只解码一次,三种视图共享字节源)。 - 流的恒常性:接收管道是纯 Dart 转换,无平台通道往返,吞吐瓶颈在原生 BLE 层(MTU 大小、连接间隔),而非本页代码路径。
- 调试建议:先使用
CustomCmdPage的 HEX 视图验证帧结构,再编写上层协议解析;若长期收不到数据,优先检查原生层是否发出了typeCustomDataUpdate事件(可临时在map中打印event调试)。 - 错误静默的运维代价:处理器吞掉所有解析异常,线上问题难以直接观测。如有需要,可在
catch (e)分支加入日志埋点(SDK 当前版本未内置,属于扩展点)。
Extension Points
- 协议无关的载荷模型:SDK 只传输
Uint8List,上层可以自由定义帧格式(帧头/长度/CRC/序号)。这是本能力最大的扩展空间——例如在CustomCmdPage基础上扩展为指令面板、脚本编辑器等。 - 自定义解析器:可以在订阅
customCommandData后再map一层,把Uint8List解析为业务模型(如DeviceInfo、ConfigAck),保持页面层只面向强类型对象。 - 新增命令方法:若需要除
sendCustomCommand外的专用方法(如带确认回调的sendCommandWithAck),可在BleCustomCmdManager旁新增管理器,并沿用BleBaseManager.invokeMethod+ 常量类契约模式,与原生层协商新方法名。 - 多语言/UI 定制:示例页面将全部文案收敛为页面常量并接入
l10n,便于复制改造为生产级工具页。
Tests
示例工程包含集成测试入口 plugin_integration_test.dart,用于验证插件与真实/模拟设备的数据通路。由于自定义命令依赖实际 BLE 连接,纯 Dart 侧建议针对以下逻辑补充单元测试:
BleCustomCmdProcessor的map转换:构造含List<int>、List<num>、null、非法元素的伪事件,断言输出Uint8List与空数组分支;- 页面状态机的收发闭环(可借助
StreamController模拟customCommandData)。
说明:处理器与页面为静态/实例逻辑,但示例仓库中未发现针对自定义命令管道的独立单元测试文件;上述建议基于源码中可测的纯函数结构给出,属测试覆盖缺口,可作贡献点。
Related Links
- BleCustomCmdManager(发送接口)
- BleCustomCmdProcessor(接收处理器)
- BleEventStream(事件流门面)
- BleMethodConstants(方法常量)
- CustomCmdPage(示例页面)
- 相关目录页:设备连接管理(
BleBaseManager/BleBaseEventProcessor基础设施)、OTA 升级、设备控制(音量/消息推送等事件流的用法与本页接收模式一致)。