自定义命令
自定义命令(Custom Command)是 JL_OTA Flutter 插件提供的一项通用 BLE 数据通道能力:业务方可以通过 BleMethod.sendCustomCommand 向已连接的杰理(Jieli)设备发送任意字节数据,并通过 BleEventStream.customCommandData 监听设备侧主动上报的自定义数据。该能力不限定于 OTA 流程,而是作为设备与 App 之间透传业务协议的基础设施。
Purpose and Scope
本页面向开发者说明自定义命令在 Flutter 插件中的完整实现机制,包括:
- 发送链路:Dart 侧如何通过
MethodChannel把自定义数据交给原生层下发到 BLE 设备; - 接收链路:原生层上报的
customDataUpdate事件如何经过EventChannel进入 Dart 事件流,并被过滤、解析为Uint8List; - 协议常量:方法名、参数键、事件类型键的定义与取值;
- 数据解析算法、边界情况与失败模式。
本页聚焦于 Flutter/Dart 插件侧的自定义命令通道实现。原生(Android/iOS)BLE 驱动的具体写入与通知回调实现不属于本页范围;OTA 升级流程本身(如固件下发、升级进度事件)请参见「OTA 升级」相关页面。
Overview
在 BLE 应用中,除标准 OTA 升级外,设备往往还需要与 App 交互自定义业务数据(例如查询设备信息、下发配置、接收设备状态)。JL_OTA 插件为此提供了独立于 OTA 流程的自定义命令通道:
- 发送:
BleMethod.sendCustomCommand(Uint8List data)通过名为sendCustomCommand的方法通道调用原生层,原生层将字节数组写入 BLE 特征; - 接收:设备通过通知(Notification/Indication)上报的数据由原生层封装为
customDataUpdate事件,经事件通道推送至 Dart 侧,BleEventStream.customCommandData流负责过滤出该类型事件并解析为Uint8List。
这一设计把「字节透传」与「业务解析」解耦:插件只负责可靠搬运原始字节,数据内容的业务含义完全由上层应用定义。命令的语义(命令码、应答格式、校验方式)由设备固件协议决定,插件侧不解析、不校验自定义数据的内部结构。
Architecture
flowchart TD
subgraph sg_Flutter["Flutter 层 (Dart)"]
App["业务代码"]
BM["BleMethod.sendCustomCommand"]
ES["BleEventStream.customCommandData"]
CS["BleMethodConstants / BleEventConstants"]
end
subgraph sg_Channel["平台通道"]
MC["MethodChannel<br/>sendCustomCommand"]
EC["EventChannel<br/>事件流 baseStream"]
end
subgraph sg_Native["原生层 (Android / iOS)"]
NSend["BLE 特征写入"]
NNotify["BLE 通知回调"]
end
subgraph sg_Device["BLE 设备"]
Dev["杰理 OTA 设备"]
end
App -->|"Uint8List data"| BM
BM --> CS
BM -->|"invokeMethod"| MC
MC -->|"平台方法调用"| NSend
NSend -->|"BLE 写入"| Dev
Dev -->|"BLE 通知/指示"| NNotify
NNotify -->|"customDataUpdate 事件"| EC
EC -->|"事件推送"| ES
ES -->|"Uint8List 数据"| App
各角色职责:
| 组件 | 职责 |
|---|---|
BleMethod | 封装所有平台方法调用的静态工具类;sendCustomCommand 是其公开发送接口 |
BleEventStream | 封装平台事件流;customCommandData getter 从 baseStream 中过滤并解析自定义命令事件 |
BleMethodConstants | 定义方法名 sendCustomCommand 与参数键 customData,是 Dart 与原生层约定的发送协议 |
BleEventConstants | 定义事件类型键 customDataUpdate 与数据键 customData,是原生层与 Dart 约定的接收协议 |
| 原生层 | 实际执行 BLE 特征写入与通知监听,将字节数组在平台通道与 BLE 之间转换 |
实现机制详解
发送链路:BleMethod.sendCustomCommand
发送自定义命令的入口位于 BleMethod 静态方法中。它把调用参数包装成 Map,通过 MethodChannel 的 invokeMethod 交给原生层处理:
// 发送自定义命令
static Future<void> sendCustomCommand(Uint8List data) async {
try {
await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_SEND_CUSTOM_COMMAND,
{BleMethodConstants.ARG_CUSTOM_DATA: data},
);
} on PlatformException catch (e) {
print("Failed to send custom command: ${e.message}");
rethrow;
}
}
Source: ble_method.dart
要点:
- 返回类型为
Future<void>,即调用方只关心「是否成功投递到原生层」,不关心设备是否应答——设备应答通过接收链路(事件流)返回; - 参数
Uint8List直接放入参数 Map,键名为BleMethodConstants.ARG_CUSTOM_DATA; - 方法名与参数键全部来自常量类,避免魔法字符串在 Dart 与原生之间漂移;
- 失败时打印日志并
rethrow,由上层决定如何兜底。
接收链路:BleEventStream.customCommandData
设备侧上报的数据由原生层以 customDataUpdate 事件推送到 Dart 事件流。BleEventStream.customCommandData 是一个 getter,它基于 baseStream 做过滤与解析:
// 自定义命令数据流
static Stream<Uint8List> get customCommandData {
return baseStream
.where((event) =>
event is Map &&
event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_CUSTOM_COMMAND_DATA)
.map((event) {
try {
final data = event[BleEventConstants.KEY_VALUE] as Map?;
if (data == null) return Uint8List(0);
final customData = data[BleEventConstants.KEY_CUSTOM_DATA];
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);
}
}
...
Source: ble_event_stream.dart
解析策略说明:
- 类型过滤:只保留
Map且type == 'customDataUpdate'的事件,其他事件(如连接状态、OTA 进度)不会混入; - 空安全兜底:
value不是 Map、customData字段缺失时,返回Uint8List(0)而不是抛异常,保证流订阅方不会因脏数据崩溃; - 类型归一化:由于平台通道(
StandardMethodCodec/StandardMessageCodec)在 Android 上可能把字节数组解包为List<dynamic>(元素为int或num),这里先处理List<int>的快速路径,再对泛型List逐元素校验并toInt()归一化;一旦遇到非数值元素立即返回空数据,防止类型转换异常向上传播。
这一层解析的定位是「把平台通道的异构类型统一成 Uint8List」,业务方拿到字节后自行解析命令语义。
协议常量定义
发送与接收两侧的协议常量分别定义在两个常量类中:
| 常量 | 取值 | 用途 |
|---|---|---|
BleMethodConstants.METHOD_SEND_CUSTOM_COMMAND | 'sendCustomCommand' | 方法通道方法名(Dart → 原生) |
BleMethodConstants.ARG_CUSTOM_DATA | 'customData' | 发送参数的 Map 键 |
BleEventConstants.TYPE_CUSTOM_COMMAND_DATA | 'customDataUpdate' | 事件类型标识(原生 → Dart) |
BleEventConstants.KEY_CUSTOM_DATA | 'customData' | 事件负载中的数据键 |
Source: ble_method_constants.dart · ble_event_constants.dart
注意发送键名与接收键名都是 'customData',但两者分属不同通道(方法通道的参数 Map 与事件通道的负载 Map),互不冲突。
Core Flow
以下时序图展示一次「发送自定义命令 → 设备应答 → 应用接收」的完整交互:
sequenceDiagram
participant App as 业务代码
participant BM as BleMethod
participant MC as MethodChannel
participant Native as 原生层
participant Dev as BLE 设备
participant ES as BleEventStream.customCommandData
App->>BM: sendCustomCommand(Uint8List data)
activate BM
BM->>MC: invokeMethod('sendCustomCommand', {customData: data})
activate MC
MC->>Native: 平台方法调用
deactivate MC
Native->>Dev: BLE 特征写入
deactivate BM
Dev-->>Native: 通知/指示上报
Native-->>ES: 事件 customDataUpdate (含 customData)
activate ES
ES->>ES: 类型过滤 + 字节归一化
ES-->>App: Stream<Uint8List> 数据帧
deactivate ES
关键点:
- 发送与接收是两条独立通路:
sendCustomCommand的Future完成只代表写入请求已投递,不保证设备已应答; - 设备应答以异步事件形式进入
customCommandData流,与请求不强制一一对应,请求/应答配对逻辑由上层协议自行处理; - 过滤发生在事件进入流之前(
where),因此即使业务方没有订阅customCommandData,也不会影响其他事件流的消费。
Usage Examples
发送自定义命令
业务方在设备连接成功后,构造任意字节载荷并调用 sendCustomCommand:
// 组装业务自定义指令(示例:2 字节指令码 + 参数区)
final command = Uint8List.fromList([0x01, 0x02, 0xAA, 0xBB]);
try {
await BleMethod.sendCustomCommand(command);
// 已投递到原生层,等待设备应答(见下方接收示例)
} on PlatformException catch (e) {
// 原生层发送失败(如未连接、写入失败)
print("send failed: ${e.message}");
}
发送 API 来自 ble_method.dart;组装载荷的业务代码为基于公开 API 的用法示意,插件本身只透传字节。
接收设备上报数据
在应用初始化或设备连接后订阅自定义命令数据流,即可持续接收设备侧上报的字节帧:
StreamSubscription<Uint8List>? _sub;
void listenCustomData() {
_sub = BleEventStream.customCommandData.listen((Uint8List data) {
if (data.isEmpty) return; // 插件在解析失败时返回空数据
// 按设备协议解析业务命令
handleDeviceCommand(data);
});
}
void dispose() {
_sub?.cancel();
}
数据流 API 来自 ble_event_stream.dart;
listen/cancel为 DartStream标准用法。
协议常量引用
发送与接收两侧均建议通过常量类引用,避免硬编码字符串:
// 发送:方法名与参数键
await _methodChannel.invokeMethod(
BleMethodConstants.METHOD_SEND_CUSTOM_COMMAND, // 'sendCustomCommand'
{BleMethodConstants.ARG_CUSTOM_DATA: data}, // 'customData'
);
// 接收:事件类型与数据键
event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_CUSTOM_COMMAND_DATA // 'customDataUpdate'
Source: ble_method.dart · ble_event_stream.dart
API Reference
static Future<void> BleMethod.sendCustomCommand(Uint8List data)
通过方法通道将自定义命令字节数组发送到原生层,由原生层写入 BLE 设备。
参数:
data(Uint8List):要发送的原始字节数据,内容语义由设备协议决定。
返回: Future<void>,在方法通道调用完成时 resolve;仅表示请求已投递给原生层,不代表设备已应答。
Throws:
PlatformException:原生层调用失败(例如设备未连接、特征不可写、平台通道异常),此时会打印"Failed to send custom command: ..."后rethrow。
static Stream<Uint8List> get BleEventStream.customCommandData
只读事件流,订阅后持续接收设备上报的自定义命令数据。
返回: Stream<Uint8List>,每一帧对应一次 customDataUpdate 事件的解析结果;解析失败或字段缺失时返回空 Uint8List(0)。
说明: 该 getter 不会抛出异常;数据异常在流内部被归一化为空字节帧。
Failure Modes, Edge Cases & Concurrency
失败模式
| 场景 | 行为 |
|---|---|
| 设备未连接时发送 | 原生层方法调用抛 PlatformException,Dart 侧打印日志后 rethrow,由调用方捕获处理 |
事件 value 字段缺失或非 Map | 返回 Uint8List(0),不抛异常 |
事件 customData 字段缺失 | 返回 Uint8List(0) |
customData 元素含非数值类型 | 立即返回 Uint8List(0),避免 TypeError 污染事件流 |
原生层在 Android 上返回 List<num> | 逐元素 toInt() 归一化,保证跨平台行为一致 |
边界情况与解析决策
flowchart TD
E["原生事件 Map"] --> F{"type == customDataUpdate?"}
F -->|"否"| Drop["丢弃,不进入自定义流"]
F -->|"是"| M["取 value 字段"]
M --> N{"value 是 Map?"}
N -->|"否"| Empty1["返回 Uint8List(0)"]
N -->|"是"| D["取 customData 字段"]
D --> Null1{"customData 为空?"}
Null1 -->|"是"| Empty2["返回 Uint8List(0)"]
Null1 -->|"否"| L1{"是 List<int>?"}
L1 -->|"是"| Fast["Uint8List.fromList 快速路径"]
L1 -->|"否"| L2{"元素均为 num?"}
L2 -->|"是"| Conv["逐元素 toInt 转换"]
L2 -->|"否"| Empty3["返回 Uint8List(0)"]
并发与一致性
- 发送与接收走不同平台通道,互不阻塞;
sendCustomCommand的await不会影响事件流投递; - 同一时刻多次调用
sendCustomCommand时,调用顺序由平台通道串行保证,但设备侧应答顺序不保证与请求顺序一致——上层协议应自带序列号或请求/应答配对机制; customCommandData为冷流(每次 getter 访问基于baseStream派生),多个订阅者各自过滤同一事件源,互不影响。
Performance & Operational Notes
- 每次
sendCustomCommand都是一次方法通道调用 + 一次 BLE 写入;对高频小命令(如实时状态查询)建议由上层做节流或合并,避免 BLE MTU/写入队列拥塞; - 事件流中的解析为纯内存操作(类型检查 +
List<int>拷贝),开销可忽略,但业务方应及时消费事件,避免背压导致的事件积压; - 收到空
Uint8List(0)帧时上层应视为「无效帧」直接忽略,不建议重发,以免在设备异常时造成写入风暴。
Extension Points
- 自定义协议封装:可在
BleMethod/BleEventStream之上封装命令层(如sendCommand(code, payload)/onResponse(code)),内部维护序列号与应答配对表,插件本身不参与该层设计; - 原生行为定制:发送/接收的实际 BLE 通道(服务 UUID、特征 UUID、写入类型)由原生层决定,需要更换通道时只需修改原生实现,Dart 侧 API 无需变动;
- 镜像发布目录:仓库根目录
libs/下存在与code/JL_OTA/lib对应的插件源码副本(如 libs/ble_method.dart),打包/发布流程可依据libs/目录构建,改动时注意两处同步。
Related Links
- OTA 升级流程 — 标准固件升级流程与进度事件(与自定义命令通道相互独立)
- BleEventStream 事件体系 —
baseStream的构成与全部事件类型 - BleMethod 平台调用 — 方法通道的整体设计与其他平台方法
- BleMethodConstants — 方法名与参数键常量定义
- BleEventConstants — 事件类型与数据键常量定义