杰理 SDK 文档中心
首页
首页
  • 概览与快速开始

    • 项目概述与能力总览
    • 快速开始与 SDK 集成
  • SDK 核心接口

    • 发送接口 BleMethod
    • 接收接口 BleEventStream
    • 数据模型与常量定义
  • 平台原生实现

    • Android 原生层
    • iOS 原生层架构
    • iOS 蓝牙管理与 SDK 运行
    • 辅助连接与广播音箱
  • OTA 升级功能

    • 升级流程与传输通道
    • 自动回连机制
    • 复用空间升级
    • 自定义命令
  • 示例应用

    • 页面结构与用户旅程
    • 设备扫描与连接管理
    • 固件文件管理
    • 升级执行与状态展示
    • 设置与调试
  • 文档与支持

    • 接口文档与收发说明
    • 调试与问题排查

自定义命令

自定义命令(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

解析策略说明:

  1. 类型过滤:只保留 Map 且 type == 'customDataUpdate' 的事件,其他事件(如连接状态、OTA 进度)不会混入;
  2. 空安全兜底:value 不是 Map、customData 字段缺失时,返回 Uint8List(0) 而不是抛异常,保证流订阅方不会因脏数据崩溃;
  3. 类型归一化:由于平台通道(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 为 Dart Stream 标准用法。

协议常量引用

发送与接收两侧均建议通过常量类引用,避免硬编码字符串:

// 发送:方法名与参数键
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 — 事件类型与数据键常量定义
Prev
复用空间升级