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

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

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

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

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

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

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

接收接口 BleEventStream

BleEventStream 是 JL_OTA_Flutter 蓝牙插件(com.jieli.ble_plugin)在 Dart 侧的统一接收接口,通过 EventChannel 监听原生 Android 侧推送的全部事件,并以类型化 Stream 的形式暴露给上层业务(扫描、连接、OTA 升级、日志回传等)。

Purpose and Scope

本页面完整讲解 SDK 接收侧的核心类 BleEventStream:

  • 它与原生 Android 侧之间通过 EventChannel 通信的机制;
  • 底层单例广播流 baseStream 的初始化与惰性单例设计;
  • 全部 15 条类型化事件流的职责、过滤条件、返回类型与数据转换逻辑;
  • 配套的常量定义(事件类型 / 字段键)与数据模型(ScanDevice、DeviceConnection);
  • 订阅示例、失败模式与扩展点。

本页面不涉及发送侧接口(扫描、连接、OTA 等命令的发起方法),相关内容属于发送接口的独立目录页;本页面也不展开 ScanDevice / DeviceConnection 模型的字段细节,这些属于数据模型页。事件流常量类的具体取值定义在 BleEventConstants 中,本页面仅列出 BleEventStream 实际引用到的键名。

概述

在 OTA 升级 SDK 中,原生 Android 侧持续产生大量异步事件:蓝牙开关状态变化、扫描状态与设备列表刷新、设备连接状态、OTA 连接与升级进度、日志文件列表、下载进度、强制升级提醒、自定义命令回包、错误信息等。这些事件无法通过"请求-响应"式的发送接口同步返回,因此 SDK 采用了 EventChannel 广播 + Dart Stream 过滤分发 的接收架构:

  1. 原生侧将事件打包为 Map(含 type 与 value 字段),通过 EventChannel('com.jieli.ble_plugin/events') 推送;
  2. Dart 侧 BleEventStream 将 EventChannel 的原始广播流缓存为单例 baseStream;
  3. 每条对外公开的静态 getter 在 baseStream 上执行 where(按 KEY_TYPE 过滤事件类型)与 map(把 KEY_VALUE 转换为强类型对象/Map);
  4. 业务层只需 BleEventStream.xxxStream.listen(...) 即可收到对应事件。

这种"单一原始流 + 多路派生流"的设计让原生侧只需一个 channel,Dart 侧按需派生,避免为每条事件创建独立 channel,也天然实现了按事件类型的解耦订阅。

架构

flowchart TD
    subgraph sg_Native["原生 Android 侧 (com.jieli.ble_plugin)"]
        BT["蓝牙开关事件"]
        SCAN["扫描状态 / 设备列表"]
        CONN["设备连接状态"]
        OTA["OTA 连接 / 状态 / 进度"]
        LOG["日志文件列表 / 详情"]
        DL["下载状态"]
        CMD["自定义命令回包"]
        ERR["错误 PlatformException"]
    end

    subgraph sg_Channel["EventChannel"]
        CH["com.jieli.ble_plugin/events<br/>(receiveBroadcastStream)"]
    end

    subgraph sg_Dart["Dart 侧 BleEventStream (libs/ble_event_stream.dart)"]
        BASE["baseStream 单例<br/>Stream&lt;dynamic&gt;"]
        FILTER["按 KEY_TYPE 过滤 (where)"]
        MAP["按 KEY_VALUE 转换 (map)"]
        TYPED["15 条类型化 Stream getter"]
    end

    subgraph sg_UI["业务层 (Flutter UI / Service)"]
        SUB["listen / onError 订阅"]
    end

    BT --> CH
    SCAN --> CH
    CONN --> CH
    OTA --> CH
    LOG --> CH
    DL --> CH
    CMD --> CH
    ERR --> CH
    CH --> BASE
    BASE --> FILTER --> MAP --> TYPED
    TYPED --> SUB

架构要点:

  • 单一 EventChannel:类内只定义一个静态 EventChannel,channel 名为 com.jieli.ble_plugin/events,所有事件类型共用这一条通道,降低了原生侧注册成本与 Dart 侧的内存占用。
  • 惰性单例 baseStream:_baseStream ??= _eventChannel.receiveBroadcastStream() 保证 receiveBroadcastStream() 只被调用一次,后续所有 getter 复用同一原始流。
  • 管道式派生:每条类型化流都是 baseStream.where(...).map(...) 的派生流,各自持有独立订阅者,互不影响。
  • 强类型转换:能转换为模型的事件(设备列表、设备连接)直接映射为 ScanDevice / DeviceConnection;其余事件以 Map<String, dynamic> 或标量形式透传。

实现来源:ble_event_stream.dart

事件流机制详解

baseStream:原始广播流与单例保证

BleEventStream 的一切派生流都建立在 baseStream 之上。该 getter 使用 Dart 的 ??= 惰性初始化模式:只有第一次访问时才真正调用 receiveBroadcastStream(),之后所有调用方共享同一个 Stream<dynamic> 实例:

static const EventChannel _eventChannel = EventChannel('com.jieli.ble_plugin/events');

// 核心广播流
// 单例模式:确保_baseStream只被初始化一次
static Stream<dynamic>? _baseStream;

// 提供一个公共的访问方法
static Stream<dynamic> get baseStream {
  _baseStream ??= _eventChannel.receiveBroadcastStream();
  return _baseStream!;
}

来源:ble_event_stream.dart

设计意图:receiveBroadcastStream() 是 EventChannel 的原生广播流,若每条 getter 各自调用一次,会向原生侧重复注册监听器,可能造成事件重复或资源泄漏。单例化之后,无论业务层订阅多少条派生流,底层始终只有一条原生连接;同时 Stream 的 where/map 派生天然支持多订阅者,各条流之间互不干扰。

事件信封(Envelope)结构

原生侧推送的每个原始事件都是一个 Map,至少包含两个键(常量定义见 BleEventConstants):

键含义
KEY_TYPE事件类型标识,用于路由(如 TYPE_SCAN_DEVICE_LIST、TYPE_OTA_STATE)
KEY_VALUE事件载荷,通常是嵌套 Map,包含 KEY_STATE、KEY_LIST、KEY_PROGRESS 等业务字段

每条派生流的实现模式高度一致:先用 where 判断 event is Map && event[KEY_TYPE] == TYPE_XXX 做路由过滤,再用 map 从 event[KEY_VALUE] 中提取并转换数据。以扫描设备列表为例:

// 扫描设备列表流
static Stream<List<ScanDevice>> get scanDeviceListStream {
  return baseStream
      .where((event) => event is Map && event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_SCAN_DEVICE_LIST)
      .map((event) {
    final list = event[BleEventConstants.KEY_VALUE][BleEventConstants.KEY_LIST] as List? ?? [];
    return list
        .whereType<Map>()
        .map((deviceMap) => ScanDevice.fromMap(deviceMap))
        .toList();
  });
}

来源:ble_event_stream.dart

注意这里的健壮性处理:as List? ?? [] 允许缺失 KEY_LIST;whereType<Map>() 过滤掉原生侧可能混入的非 Map 元素,避免类型转换异常导致整条流 onError。

全部派生流一览

静态 getter返回类型过滤的 TYPE_* 常量载荷转换要点
baseStreamStream<dynamic>—原始广播流(单例)
bluetoothStateStreamStream<bool>TYPE_BLUETOOTH_STATEKEY_VALUE[KEY_STATE] 强转为 bool
scanStateStreamStream<String>TYPE_SCAN_DEVICE_LISTKEY_VALUE[KEY_STATE],缺省为空串 ''
scanDeviceListStreamStream<List<ScanDevice>>TYPE_SCAN_DEVICE_LISTKEY_VALUE[KEY_LIST] 逐项 ScanDevice.fromMap
deviceConnectionStreamStream<DeviceConnection>TYPE_DEVICE_CONNECTIONDeviceConnection.fromMap(KEY_VALUE)
otaConnectionStreamStream<Map<String, dynamic>>TYPE_OTA_CONNECTION提取 KEY_STATE、KEY_DEVICE_TYPE
logFilesStreamStream<List<Map<String, String>>>TYPE_LOG_FILESKEY_FILES 列表,每项保留 KEY_NAME
logDetailFilesStreamStream<String>TYPE_LOG_DETAIL_FILESKEY_FILES.first,缺省 ''
downloadStatusStreamStream<Map<String, dynamic>>TYPE_DOWNLOAD_STATUS提取 KEY_STATUS、KEY_PROGRESS、KEY_MESSAGE
otaFileListStreamStream<List<Map<String, String>>>TYPE_OTA_FILE_LISTKEY_VALUE[KEY_LIST],每项保留 KEY_NAME、KEY_PATH
selectedFilePathsStreamStream<List<String>>TYPE_SELECTED_FILE_PATHSKEY_VALUE[KEY_LIST] 过滤为 String
mandatoryUpgradeStreamStream<bool>TYPE_MANDATORY_UPGRADEKEY_VALUE[KEY_IS_REQUIRED] 强转为 bool
otaStateStreamStream<Map<String, dynamic>>TYPE_OTA_STATE提取 KEY_STATE、KEY_SUCCESS、KEY_CODE、KEY_TYPE、KEY_MESSAGE;当 state == KEY_STATE_WORKING 时附加 KEY_PROGRESS
customCommandDataStream<Uint8List>TYPE_CUSTOM_COMMAND_DATA将 KEY_CUSTOM_DATA 转换为字节数组,见下文
errorStreamStream<Map<String, String>>PlatformException仅透传 code == ERROR 的错误,其余重新抛出

实现来源:ble_event_stream.dart;类型常量定义于 ble_event_constants.dart

特殊流实现细节

otaStateStream 的条件字段:OTA 状态流在 state == KEY_STATE_WORKING(工作中)时,才会把 KEY_PROGRESS(升级进度)放入结果 Map;非工作状态(如空闲、完成、失败)不携带进度字段,业务层需要以 result.containsKey(...) 或空值判断,避免假设每个事件都有进度:

// OTA状态流
static Stream<Map<String, dynamic>> get otaStateStream {
  return baseStream
      .where((event) =>
  event is Map && event[BleEventConstants.KEY_TYPE] == BleEventConstants.TYPE_OTA_STATE)
      .map((event) {
    final data = event[BleEventConstants.KEY_VALUE];
    final state = data[BleEventConstants.KEY_STATE];
    final result = {
      BleEventConstants.KEY_STATE: state,
      BleEventConstants.KEY_SUCCESS: data[BleEventConstants.KEY_SUCCESS],
      BleEventConstants.KEY_CODE: data[BleEventConstants.KEY_CODE],
      BleEventConstants.KEY_TYPE: data[BleEventConstants.KEY_TYPE],
      BleEventConstants.KEY_MESSAGE: data[BleEventConstants.KEY_MESSAGE],
    };
    if (state == BleEventConstants.KEY_STATE_WORKING) {
      result[BleEventConstants.KEY_PROGRESS] = data[BleEventConstants.KEY_PROGRESS];
    }
    return result;
  });
}

来源:ble_event_stream.dart

customCommandData 的容错转换:自定义命令回包在原生侧可能是 List<int>、List<num> 或包含非整数元素的 List,SDK 做了多级降级:List<int> 直接 Uint8List.fromList;List<num> 逐个 toInt();任一元素既非 int 也非 num、或数据缺失、或整体抛异常,一律返回空 Uint8List(0) 而不是中断流:

// 自定义命令数据流
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);
          }
        }
        return Uint8List.fromList(result);
      }
      return Uint8List(0);
    } catch (e) {
      return Uint8List(0);
    }
  });
}

来源:ble_event_stream.dart

errorStream 的特殊过滤:与其他流不同,错误流不是按 KEY_TYPE 过滤,而是按运行时类型 PlatformException 过滤,且仅放行 code == BleEventConstants.ERROR 的错误;其他 PlatformException 会被原样 throw,从而把"SDK 定义的错误事件"与"意外异常"区分开,业务层可只监听已知错误码,异常则走流级 onError:

// 错误流
static Stream<Map<String, String>> get errorStream {
  return baseStream
      .where((event) => event is PlatformException)
      .map((event) {
    final error = event as PlatformException;
    if (error.code == BleEventConstants.ERROR) {
      return {
        BleEventConstants.KEY_CODE: error.code,
        BleEventConstants.KEY_MESSAGE: error.message ?? 'Unknown log error',
      };
    }
    throw error;
  });
}

来源:ble_event_stream.dart

数据模型转换

两条流会转换为 SDK 预定义模型(模型字段细节见对应数据模型页):

  • scanDeviceListStream:将 KEY_LIST 中每个 Map 交给 ScanDevice.fromMap(deviceMap) 构造 ScanDevice;
  • deviceConnectionStream:将整个 KEY_VALUE 交给 DeviceConnection.fromMap(data) 构造 DeviceConnection。
import 'model/device_connection.dart';
import 'model/scan_device.dart';

来源:ble_event_stream.dart

选择模型化而非 Map 透传,是为了让业务层获得编译期类型安全:字段拼写错误、类型不符等问题在编译期即可暴露,而不需要等到运行时调试。

核心流程

下图展示了一次典型的"原生事件 → Dart 订阅者"的完整链路,以扫描设备列表事件为例:

sequenceDiagram
    participant N as 原生 Android 侧
    participant EC as EventChannel<br/>(com.jieli.ble_plugin/events)
    participant BS as baseStream (单例)
    participant F as where 过滤<br/>(KEY_TYPE == TYPE_SCAN_DEVICE_LIST)
    participant M as map 转换<br/>(ScanDevice.fromMap)
    participant S as 业务层订阅者

    N->>EC: 推送 Map{type: TYPE_SCAN_DEVICE_LIST, value: {list: [...]}}
    EC-->>BS: receiveBroadcastStream 事件
    BS->>F: 事件到达 (broadcast 流)
    alt type 匹配
        F->>M: 通过过滤
        M->>M: 逐项 ScanDevice.fromMap(deviceMap)
        M-->>S: Stream<List<ScanDevice>> 发射
        S->>S: setState / 业务处理
    else type 不匹配
        F->>S: 静默忽略 (不下发)
    end
    Note over S: 订阅时需处理 onError 与 cancel

流程说明:

  1. 原生侧把业务数据打包成 Map{KEY_TYPE, KEY_VALUE} 信封,通过唯一的 EventChannel 推送;
  2. baseStream 单例收到原始事件(Stream<dynamic>),由于是 receiveBroadcastStream() 广播流,每条派生流都能收到同一事件副本;
  3. 每条派生流的 where 谓词独立判断事件类型:不匹配的事件静默丢弃,匹配的事件进入 map;
  4. map 阶段完成载荷提取与强类型转换(模型、Map、标量、Uint8List);
  5. 业务层 listen 回调收到转换结果,并在 onError 中处理异常;不再需要时调用 StreamSubscription.cancel() 释放资源(官方示例中每个订阅都保存了 StreamSubscription 句柄)。

使用示例

以下示例均取自官方接入文档 Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md。

基本用法:订阅扫描状态流

最典型的订阅模式:保存 StreamSubscription 句柄、在回调中先做 mounted 检查、通过 onError 兜底日志:

StreamSubscription<String>? _scanStateSubscription;

void _subscribeToScanStateStream() {
  _scanSubscription = BleEventStream.scanStateStream.listen(
    (state) {
      if (!mounted) return;

      setState(() {
        if (state == BleEventConstants.SCAN_STATE_SCANNING) {
          // 当前正在扫描中,做UI层的相应的处理
        } else if (state == BleEventConstants.SCAN_STATE_IDLE) {
          // 当前扫描结束,做UI层的相应的处理
        }
      });
    },
    onError: (error) {
      log("Scan state stream error: $error");
    },
  );
}

来源:Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md

要点:SCAN_STATE_SCANNING / SCAN_STATE_IDLE 等取值来自 BleEventConstants;回调内 setState 前先判断 mounted,避免 Widget 销毁后更新 UI 引发异常——这是 Flutter 中订阅事件流的标准防护写法。

进阶用法:订阅扫描设备列表并兼容类型

设备列表流发射 List<ScanDevice>,但某些场景下事件携带的是原始 Map,官方示例提供了 convertToScanDeviceList 兜底转换:

List<ScanDevice> _devices = [];
StreamSubscription<List<ScanDevice>>? _scanSubscription;

List<ScanDevice> convertToScanDeviceList(List<dynamic> list) {
  return list.map((item) {
    if (item is ScanDevice) {
      return item;
    } else if (item is Map) {
      return ScanDevice.fromMap(item);
    } else {
      throw Exception('无法转换的类型: ${item.runtimeType}');
    }
  }).toList();
}

void _subscribeToScanListStream() {
  _scanSubscription = BleEventStream.scanDeviceListStream.listen((devices) {
    setState(() => _devices = convertToScanDeviceList(devices));
  });
}

来源:Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md

其他流订阅模式

官方文档中 deviceConnectionStream、otaConnectionStream、logFilesStream、logDetailFilesStream 均采用同一模式:BleEventStream.xxxStream.listen((data) { if (!mounted) return; setState(...); }, onError: ...),其中:

  • deviceConnectionStream 回调参数为 DeviceConnection 对象;
  • otaConnectionStream 回调参数为含 KEY_STATE、KEY_DEVICE_TYPE 的 Map;
  • logFilesStream 回调参数为文件信息 Map 列表;
  • logDetailFilesStream 回调参数为日志详情字符串。

来源:Jieli OTA Upgrade (Flutter) Send:Receive Interface Introduction.md

API 参考

BleEventStream 是一个纯静态类:没有实例方法,全部对外能力都是静态 getter(返回类型化的 Stream),外部通过 BleEventStream.xxx 直接访问,无需实例化。所有 getter 均为只读派生流,订阅语义遵循 Dart Stream 标准(broadcast 流,支持多订阅者)。

Stream<dynamic> get baseStream

底层原始广播流(惰性单例)。一般不需要直接订阅,由内部各派生流复用;仅在需要原始事件调试时使用。

首次访问:调用 _eventChannel.receiveBroadcastStream() 并缓存;后续访问:直接返回缓存实例。

Stream<bool> get bluetoothStateStream

蓝牙开关状态流。发射 bool(true 表示蓝牙已开启)。过滤 TYPE_BLUETOOTH_STATE。

Stream<String> get scanStateStream

扫描状态流。发射扫描状态字符串(如 SCAN_STATE_SCANNING / SCAN_STATE_IDLE,取值见 BleEventConstants)。缺失时发射 ''。与设备列表流共用 TYPE_SCAN_DEVICE_LIST 类型——两条流分别从同一事件的 KEY_STATE 与 KEY_LIST 提取不同字段,可同时订阅。

Stream<List<ScanDevice>> get scanDeviceListStream

扫描设备列表流。发射 ScanDevice 列表,内部对每个设备 Map 调用 ScanDevice.fromMap。

Stream<DeviceConnection> get deviceConnectionStream

设备连接状态流。发射 DeviceConnection 模型,内部对整个 KEY_VALUE 调用 DeviceConnection.fromMap。

Stream<Map<String, dynamic>> get otaConnectionStream

OTA 连接状态流。发射 Map,包含 KEY_STATE(连接状态)与 KEY_DEVICE_TYPE(设备类型)。

Stream<List<Map<String, String>>> get logFilesStream

日志文件列表流。发射文件信息 Map 列表,每个 Map 仅含 KEY_NAME(文件名)。

Stream<String> get logDetailFilesStream

日志文件详情流。发射 KEY_FILES 列表中的第一个元素(字符串);列表为空或缺失时发射 ''。

Stream<Map<String, dynamic>> get downloadStatusStream

下载状态流。发射 Map,包含 KEY_STATUS、KEY_PROGRESS、KEY_MESSAGE。

Stream<List<Map<String, String>>> get otaFileListStream

OTA 文件列表流。发射文件信息 Map 列表,每个 Map 含 KEY_NAME 与 KEY_PATH。

Stream<List<String>> get selectedFilePathsStream

选中文件路径流。发射路径字符串列表,非 String 元素被过滤。

Stream<bool> get mandatoryUpgradeStream

强制升级流。发射 bool,表示当前固件是否要求强制升级(KEY_IS_REQUIRED)。

Stream<Map<String, dynamic>> get otaStateStream

OTA 状态流。发射 Map,包含 KEY_STATE、KEY_SUCCESS、KEY_CODE、KEY_TYPE、KEY_MESSAGE;当 state == KEY_STATE_WORKING 时额外包含 KEY_PROGRESS。

Stream<Uint8List> get customCommandData

自定义命令数据流。发射原始命令回包字节(Uint8List)。任何解析失败均返回空 Uint8List(0),不会中断流。

Stream<Map<String, String>> get errorStream

错误事件流。仅发射 code == ERROR 的 PlatformException,转为 {KEY_CODE, KEY_MESSAGE};其他 PlatformException 原样抛出,由订阅方 onError 处理。

配置与常量

BleEventStream 本身没有运行时配置项;所有"配置"都体现在 BleEventConstants 常量类中(定义于 code/JL_OTA/lib/constant/ble_event_constants.dart)。本页面按 BleEventStream 源码中的实际引用列出:

常量类别常量名(本类引用到的)用途
信封键KEY_TYPE、KEY_VALUE事件路由:KEY_TYPE 决定走哪条派生流,KEY_VALUE 是载荷
载荷键KEY_STATE、KEY_LIST、KEY_FILES、KEY_NAME、KEY_PATH状态、列表、文件、名称、路径提取
载荷键KEY_STATUS、KEY_PROGRESS、KEY_MESSAGE下载/OTA 状态、进度、消息
载荷键KEY_DEVICE_TYPE、KEY_SUCCESS、KEY_CODE、KEY_IS_REQUIRED、KEY_CUSTOM_DATA设备类型、成功标志、错误码、强制升级标志、自定义数据
状态值KEY_STATE_WORKINGOTA 工作中状态,触发 KEY_PROGRESS 附加
扫描值SCAN_STATE_SCANNING、SCAN_STATE_IDLE扫描中 / 扫描结束(示例代码中使用)
事件类型TYPE_BLUETOOTH_STATE、TYPE_SCAN_DEVICE_LIST、TYPE_DEVICE_CONNECTION、TYPE_OTA_CONNECTION、TYPE_LOG_FILES、TYPE_LOG_DETAIL_FILES、TYPE_DOWNLOAD_STATUS、TYPE_OTA_FILE_LIST、TYPE_SELECTED_FILE_PATHS、TYPE_MANDATORY_UPGRADE、TYPE_OTA_STATE、TYPE_CUSTOM_COMMAND_DATA与派生流的 where 过滤一一对应
错误码ERROR与 errorStream 中 PlatformException.code 比对

注意:libs/ble_event_stream.dart 通过 import 'constant/ble_event_constants.dart' 引用常量类;仓库中该类的实现位于 code/JL_OTA/lib/constant/ble_event_constants.dart(同构副本可能随 SDK 目录布局不同而存在)。各 TYPE_* 常量的具体字符串取值以该文件为准,业务层应始终通过常量引用而非硬编码字符串,以兼容原生侧的事件协议演进。

失败模式、边界情况与并发

失败模式

  1. 原生侧异常:EventChannel 上的 PlatformException 会进入原始流。errorStream 只放行 code == ERROR 的"业务错误"并转换为 {KEY_CODE, KEY_MESSAGE};其余 PlatformException 被重新抛出,最终到达各订阅者的 onError 回调——因此业务层必须为每条订阅提供 onError(官方示例统一使用 log("... error: $error") 记录)。
  2. 数据缺失:所有载荷提取都带有 ?? [] / ?? '' / as String? ?? '' 兜底,单个字段缺失不会导致流中断,而是发射空值(空列表、空串)。
  3. 类型不匹配:scanDeviceListStream 用 whereType<Map>() 过滤非 Map 元素;selectedFilePathsStream 用 whereType<String>() 过滤非字符串。不会因个别坏元素整体抛错。
  4. 自定义数据解析失败:customCommandData 在数据为空、元素类型非法或抛异常时返回 Uint8List(0),业务层需自行区分"合法空数据"与"解析失败"。

边界情况

  • scanStateStream 与 scanDeviceListStream 共用 TYPE_SCAN_DEVICE_LIST:同一原生事件会同时驱动两条流。若某次扫描事件只携带 KEY_STATE 而无 KEY_LIST,设备列表流会发射空列表,属预期行为。
  • logDetailFilesStream 只取 KEY_FILES.first:原生侧必须保证该列表至少含一个元素,否则发射 ''。
  • otaStateStream 的 KEY_PROGRESS 仅在工作状态存在:订阅方读取进度前应先判断 state,避免把 null 进度当作"0% 或已完成"。

并发与订阅生命周期

  • 多订阅者安全:baseStream 是 receiveBroadcastStream() 广播流,任意数量的 listen 都合法,各订阅者互不影响;派生流由 where/map 创建,同样支持多订阅。
  • 单例初始化竞态:_baseStream ??= ... 在 Dart 单线程事件循环下是原子的(首个访问在同一个微任务内完成赋值),不存在竞态条件;但注意不要在 receiveBroadcastStream() 尚未完成原生注册时过早依赖事件,事件在订阅后才开始投递。
  • 资源释放:官方示例将每个订阅句柄保存为 StreamSubscription 字段(_scanStateSubscription、_scanSubscription 等),应在 dispose() 中 cancel(),否则 Widget 销毁后回调仍会执行(示例用 if (!mounted) return; 防护)。
  • 不要重复订阅同一 getter:由于所有 getter 都是派生流,多次 listen 同一 getter 会产生多个订阅者,每次 listen 都会收到完整事件流。若业务层误在 build/initState 中重复订阅,会导致回调重复执行。

性能与运维

  • 单一 EventChannel 是性能关键设计:所有事件共用一条原生通道,Dart 侧只做 where(O(1) 类型比对)与 map(轻量 Map 提取),单事件处理开销极小,可支撑高频事件(如设备列表逐条刷新、OTA 进度节拍)。
  • 惰性初始化:baseStream 直到首次访问才建立原生连接。若业务层从未订阅任何流,SDK 不会产生 EventChannel 开销;反之,一旦任一流被访问,原生连接即建立并保持(由 receiveBroadcastStream 生命周期管理)。
  • 日志排查:官方示例在每个订阅的 onError 中 log(...)。排查事件未到达的问题时,可临时订阅 baseStream 观察原始事件信封,确认 KEY_TYPE 取值与 BleEventConstants 是否一致(版本升级后最容易出现的事件协议不匹配)。

扩展点

  • 新增事件类型:原生侧新增事件时,只需在 BleEventConstants 增加新的 TYPE_* 与载荷键,然后在 BleEventStream 中仿照现有模式新增一条 where(KEY_TYPE == TYPE_XXX).map(...) 的静态 getter。SDK 的"单通道 + 类型路由"架构使新增流无需改动原生侧 channel 注册。
  • 自定义命令:customCommandData 已提供双向自定义通道的接收侧(发送侧见发送接口),业务层可在此流之上实现私有协议。
  • 强类型模型:如需要更丰富的设备/连接信息,可扩展 ScanDevice / DeviceConnection 的 fromMap 解析字段,或参照它们为其他事件建立新模型后替换 map 逻辑。
  • 注意保持常量一致:新增事件类型时,Dart 侧常量值必须与原生侧(Android 插件)的事件协议字符串完全一致,否则 where 过滤将永远无法命中。

相关链接

  • 发送接口文档(发送侧:扫描/连接/OTA 命令) —— 与接收侧配套的命令发起接口(目录页按 SDK 文档结构对应)
  • BleEventStream 源码
  • BleEventConstants 常量定义
  • 官方接入文档:发送/接收接口介绍
  • 数据模型页:ScanDevice、DeviceConnection(ScanDevice.fromMap / DeviceConnection.fromMap 的字段说明)
Prev
发送接口 BleMethod
Next
数据模型与常量定义