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

    • 项目简介与功能总览
    • 运行环境与快速开始
    • 工程结构与文档布局
  • 架构与核心机制

    • 插件架构与原生平台桥接
    • 基类管理器与常量体系
    • 事件流与接收通知机制
  • 蓝牙连接与设备管理

    • 蓝牙连接与状态管理
    • 设备信息、配置与按键设置
    • 双设备连接与多链路管理
    • 数据传输与自定义命令
  • 音乐与媒体控制

    • 设备音乐与手机音乐播放控制
    • 音量与音频输出管理
  • 音效与音频模式

    • 均衡器与音效调节
    • 音频模式与降噪(ANC)设置
    • Auracast 音频广播
  • 设备功能控制

    • 闹钟管理
    • FM 收音机控制
    • 灯光控制
    • 充电仓与彩屏仓管理
  • 示例应用:杰理之家 Demo

    • 应用框架与交互组件
    • 设置、多语言与调试
  • 接口参考与文档中心

    • 发送接口参考
    • 接收接口与事件参考
    • 官方文档与集成指南

FM 收音机控制

本文档介绍 Flutter-JL_Home 蓝牙 SDK 中 FM 收音机控制能力的完整实现:从 Flutter 侧发送指令(BleFmManager)、接收设备状态事件(fmInfoStream)到示例页面(FmPage)的完整端到端机制。

Purpose and Scope

本页面覆盖 FM 收音机控制这一完整能力链路:

  • 发送指令层:BleFmManager 提供的全部 FM 控制方法(模式切换、搜索、选台、播放/暂停、频率选择);
  • 事件接收层:BleEventStream.fmInfoStream → BleAudioProcessor.fmInfoStream 的事件过滤与数据映射;
  • 事件常量与数据模型:typeFmInfo、keyFmStatus、keyFmFrequency 等键定义及默认值;
  • UI 示例层:FmPage 如何初始化 FM 模式、订阅事件、处理断连与状态刷新。

以下主题属于其他页面,不在本文范围:蓝牙基础连接与设备发现、音频播放(ID3 音乐状态/进度)、LineIn 模式(lineInStatusStream)、Android 原生端 FMManager/FMProcessor 的具体实现细节(仅作为跨端桥接的引用提及)。

Overview

FM 收音机控制是 JieLi 蓝牙音箱/耳机设备的一项核心媒体功能。设备端具备完整的 FM 调谐器硬件,SDK 通过 BLE 通道将控制指令下发给设备,设备随后上报搜索进度、当前频率与播放状态。

整个功能采用指令(Command)与事件(Event)分离的架构模式:

  1. 指令方向(App → 设备):通过 BleBaseManager.invokeMethod 以 MethodChannel 方式调用原生层,再由原生层经 BLE 协议下发到设备。指令均为"发后即忘"(fire-and-forget),调用方不等待设备确认;
  2. 事件方向(设备 → App):设备状态变化通过 BLE 通知上报,经原生层转换为 Flutter 事件,最终由 BleAudioProcessor 按事件类型过滤,形成类型化的 Dart Stream 供 UI 订阅。

该设计的好处是:指令与状态解耦,UI 无需在每次操作后轮询设备,而是被动响应状态流;多个页面/模块可以同时订阅同一个 Stream 而互不干扰。

Architecture

flowchart TD
    subgraph sg_UI["UI 层 (example)"]
        FmPage["FmPage (StatefulWidget)"]
        JLFMStatus["JLFMStatus 枚举"]
        FMConstants["FMConstants"]
    end

    subgraph sg_Send["发送接口层 (libs/Send Interface)"]
        BleFmManager["BleFmManager"]
        BleBaseManager["BleBaseManager.invokeMethod"]
    end

    subgraph sg_Receive["接收接口层 (libs/Receive Interface)"]
        BleEventStream["BleEventStream.fmInfoStream"]
        BleAudioProcessor["BleAudioProcessor.fmInfoStream"]
        BleBaseEventProcessor["BleBaseEventProcessor.filterByType"]
    end

    subgraph sg_Const["常量与模型"]
        BleMethodConstants["BleMethodConstants"]
        BleEventConstants["BleEventConstants (typeFmInfo='fm_info')"]
    end

    subgraph sg_Native["原生桥接层 (Android)"]
        FMManager["FMManager.kt"]
        FMProcessor["FMProcessor.kt"]
        FmStatusInfo["FmStatusInfo.java"]
    end

    FmPage -->|"调用"| BleFmManager
    FmPage -->|"订阅"| BleEventStream
    BleFmManager --> BleBaseManager
    BleFmManager --> BleMethodConstants
    BleBaseManager -->|"MethodChannel"| FMManager
    FMManager -->|"BLE 协议"| FMProcessor
    FMProcessor --> FmStatusInfo
    FMManager -->|"事件回调"| BleBaseEventProcessor
    BleAudioProcessor --> BleBaseEventProcessor
    BleAudioProcessor --> BleEventConstants
    BleEventStream --> BleAudioProcessor

架构分层说明:

  • UI 层:FmPage 是示例工程中的 FM 收音机页面(fm_page.dart),负责展示频率、播放状态,并通过滑块与按钮触发指令;
  • 发送接口层:BleFmManager 是 FM 指令的唯一入口(ble_fm_manager.dart),所有方法都是静态方法,内部统一委托给 BleBaseManager.invokeMethod;
  • 接收接口层:BleEventStream.fmInfoStream 是公开的事件流入口(ble_event_stream.dart),它转发 BleAudioProcessor.fmInfoStream;后者负责按 typeFmInfo 过滤并映射出 Map<String, dynamic>;
  • 常量层:方法名集中在 BleMethodConstants,事件类型与数据键集中在 BleEventConstants,避免魔法字符串散落在业务代码中;
  • 原生桥接层:Android 端的 FMManager.kt、FMProcessor.kt、FmStatusInfo.java 负责 MethodChannel 到 BLE 协议之间的转换,属于本页的跨端边界,其内部细节请查阅 Android 原生文档。

发送接口层:BleFmManager

BleFmManager 是 FM 控制的命令门面(Facade),位于 libs/Send Interface/ble_fm_manager.dart。全部 9 个方法均为 static,无需实例化即可调用。每个方法只做一件事:把语义化方法名翻译成 BleMethodConstants 中的平台方法名,并转发给 BleBaseManager.invokeMethod。

/// FM Manager
class BleFmManager {
  /// Switch to FM mode
  static Future<void> changeFMMode() async {
    await BleBaseManager.invokeMethod(
      BleMethodConstants.methodChangeFmMode,
    );
  }

  /// FM global search
  static Future<void> fmSearchAll() async {
    await BleBaseManager.invokeMethod(
      BleMethodConstants.methodFmSearchAll,
    );
  }

  /// FM select frequency
  static Future<void> fmSelectFrequency(double frequency) async {
    await BleBaseManager.invokeMethod(
      BleMethodConstants.methodFmSelectFrequency,
      arguments: {BleMethodConstants.argFrequency: frequency},
    );
  }
}

Source: ble_fm_manager.dart

设计意图分析:

  • 静态门面:FM 指令无状态(设备端持有调谐器状态),因此使用静态方法而非实例方法,避免调用方管理对象生命周期,也方便在任何上下文(页面、服务、回调)中直接调用;
  • 参数传递:仅 fmSelectFrequency(double frequency) 携带参数,通过 arguments 字典以 BleMethodConstants.argFrequency 为键传递,其余指令均为无参调用——这反映了设备端 FM 调谐器的命令设计:频率选择需要显式传值,而搜索/切台/播放等动作由设备端自己维护内部状态;
  • "发后即忘"语义:方法返回 Future<void> 且不解析设备响应,意味着 UI 层必须依赖事件流(而非指令返回值)来感知操作结果。

完整指令清单

语义方法平台方法常量参数说明
changeFMMode()methodChangeFmMode无切换设备到 FM 模式(从音乐等模式进入)
fmSearchAll()methodFmSearchAll无全局搜台(全频段扫描)
fmStopSearch()methodStopSearch无停止搜索(注意常量名为通用 methodStopSearch,非 FM 专属)
fmChannelBefore()methodFmChannelBefore无上一个已存台
fmChannelNext()methodFmChannelNext无下一个已存台
fmSearchForward()methodFmSearchForward无向高频方向搜索
fmSearchNext()methodFmSearchNext无向低频方向搜索(命名与"下一台"语义相反,需注意)
fmPlayPause()methodFmPlayPause无播放/暂停切换
fmSelectFrequency(double)methodFmSelectFrequencyargFrequency直接指定频率(MHz)

注意 fmSearchForward 与 fmSearchNext 的命名易混淆:Forward 指向前(高频)搜索,Next 在代码注释中标注为 "FM backward search"(向后/低频搜索),实现时应以实际语义为准。

事件接收层:fmInfoStream

设备状态通过事件流回传。公开入口是 BleEventStream.fmInfoStream:

static Stream<Map<String, dynamic>> get fmInfoStream =>
    BleAudioProcessor.fmInfoStream;

Source: ble_event_stream.dart

真正完成过滤与映射的是 BleAudioProcessor.fmInfoStream:

/// FM info stream
static Stream<Map<String, dynamic>> get fmInfoStream {
  return BleBaseEventProcessor.filterByType(BleEventConstants.typeFmInfo)
      .map((event) {
    final data = BleBaseEventProcessor.getValueFromEvent(event);
    return {
      BleEventConstants.keyFmStatus: data[BleEventConstants.keyFmStatus] as int? ?? 0,
      BleEventConstants.keyFmFrequency: data[BleEventConstants.keyFmFrequency] as double? ?? 0.0,
    };
  });
}

Source: ble_audio_processor.dart

处理管线分两步:

  1. 过滤:filterByType(BleEventConstants.typeFmInfo) 从统一的底层事件总线中只挑出 type = 'fm_info' 的事件(该类型常量定义见 ble_event_constants.dart)。这种"单一总线 + 类型分发"的模式与音频(typeId3MusicStatus)、设备状态(typeDeviceStatus)等事件共用,避免为每种事件创建独立通道;
  2. 映射:把原始事件解包(getValueFromEvent)后提取两个字段,并为缺失字段提供安全默认值——keyFmStatus 缺省为 0(暂停),keyFmFrequency 缺省为 0.0。默认值策略保证了 UI 端即使收到不完整数据也不会崩溃。

事件流对外暴露为 Map<String, dynamic>,键为 BleEventConstants.keyFmStatus(int,0=暂停/1=播放)与 BleEventConstants.keyFmFrequency(double,单位 MHz)。

Core Flow:端到端控制流程

sequenceDiagram
    participant UI as FmPage
    participant FM as BleFmManager
    participant CH as BleBaseManager (MethodChannel)
    participant NAT as 原生层 FMManager
    participant DEV as 设备 FM 调谐器
    participant BP as BleBaseEventProcessor
    participant AP as BleAudioProcessor
    participant UI2 as FmPage 订阅者

    UI->>FM: changeFMMode()
    FM->>CH: invokeMethod(methodChangeFmMode)
    CH->>NAT: MethodChannel 调用
    NAT->>DEV: BLE 协议下发 FM 模式切换
    DEV-->>NAT: FM 状态通知 (fm_info)
    NAT-->>BP: 事件回调入总线
    BP-->>AP: filterByType(typeFmInfo) 过滤
    AP-->>UI2: fmInfoStream 映射为 {keyFmStatus, keyFmFrequency}
    UI2->>UI2: setState 刷新频率与播放状态

    UI->>FM: fmSelectFrequency(97.5)
    FM->>CH: invokeMethod(methodFmSelectFrequency, {argFrequency: 97.5})
    CH->>NAT: MethodChannel 调用
    NAT->>DEV: BLE 协议设置频率
    DEV-->>NAT: 新频率状态通知
    NAT-->>BP: 事件回调
    BP-->>AP: 过滤 fm_info
    AP-->>UI2: 新频率事件
    UI2->>UI2: 更新滑块与显示

流程要点:

  1. 进入 FM 模式:UI 初始化时先调用 changeFMMode(),确保设备处于 FM 模式(设备若在音乐/LineIn 模式,FM 指令可能无效);
  2. 订阅事件:changeFMMode() 之后立即订阅 fmInfoStream,避免模式切换瞬间的状态丢失——这是示例页 _initializeFm() 中两个操作的固定顺序(先命令后订阅,见下文代码);
  3. 指令下发:所有指令经 BleBaseManager.invokeMethod 走 MethodChannel 到 Android 原生层,原生 FMManager 转 BLE 协议发送;
  4. 状态回传:设备主动上报 fm_info 事件(非指令应答),经事件总线 → 类型过滤 → 字段映射后推送给所有订阅者;
  5. UI 刷新:订阅回调中通过 setState 更新 _currentFrequency 与 _currentPlayStatus。

示例页面实现:FmPage

FmPage 展示了完整的 FM 控制最佳实践(初始化、订阅、断连处理、释放):

/// Initializes FM mode and starts listening to events
void _initializeFm() async {
  await BleFmManager.changeFMMode();
  _fmSubscription = BleEventStream.fmInfoStream.listen(_handleFmInfoUpdate);
}

/// Handles FM information updates from the stream
void _handleFmInfoUpdate(Map<String, dynamic> fmInfo) {
  if (!mounted) return;

  setState(() {
    _currentPlayStatus = _convertStatusToFmStatus(fmInfo[BleEventConstants.keyFmStatus]);
    _currentFrequency = fmInfo[BleEventConstants.keyFmFrequency];
    _displayFrequency = _currentFrequency;
  });
}

Source: fm_page.dart

页面同时定义了频率边界与状态码常量,这些数值构成了 FM 功能的隐式契约:

class FMConstants {
  // FM frequency constants
  static const double fmMinFrequency = 87.5;
  static const double fmMaxFrequency = 108.0;
  static const int sliderDivisions = 205;

  // FM status codes
  static const int fmStatusPause = 0;
  static const int fmStatusPlay = 1;
  ...
}

/// FM player status enumeration
enum JLFMStatus {
  pause,    // Paused state
  play,     // Playing state
  unknown,  // Unknown status
}

Source: fm_page.dart

关键设计点:

  • 频率边界:87.5 ~ 108.0 MHz 是 FM 广播标准频段(中国/欧洲制式),滑块划分数 205 对应 87.5 到 108.0 之间每 0.1 MHz 一步((108.0−87.5)×10 = 205),确保滑块只能选择合法频率;
  • 状态映射:keyFmStatus 的 int 值(0/1)被映射为 JLFMStatus 枚举(pause/play),unknown 用于设备未就绪或字段缺失的场景,体现了"显式表达未知状态"而不是用魔法数字;
  • 生命周期管理:_fmSubscription 在 dispose() 中调用 cancel(),防止页面销毁后 Stream 泄漏导致的内存问题与回调触发;
  • 断连处理:页面通过 context.watch<ConnectionStateManager>() 监听连接状态(fm_page.dart),当 connectState == connectionDisconnected(0)时自动 Navigator.maybePop 返回上一页——这是设备功能页的通用行为模式:蓝牙断开时设备功能全部失效,留在页面只会让用户面对一个无响应的 UI;
  • 搜索动画:搜索状态(_isFmSearching)配合 searchFmGif 资源展示动态搜索效果,按钮圆角在搜索中/正常状态间切换(buttonBorderRadiusSearching/buttonBorderRadiusNormal),给用户即时的操作反馈。

API Reference

BleFmManager(发送指令)

所有方法均为 static Future<void>,通过 BleBaseManager.invokeMethod 走 MethodChannel。

changeFMMode()

  • 作用:将设备切换到 FM 模式。进入 FM 页面/功能前的必要前置步骤。
  • 参数:无。返回:Future<void>。抛出:MethodChannel 调用失败时由 invokeMethod 传播异常。

fmSearchAll()

  • 作用:触发设备全频段全局搜台(自动扫描并保存电台)。
  • 参数:无。返回:Future<void>。

fmStopSearch()

  • 作用:停止当前搜索。注意底层方法常量为通用的 methodStopSearch。
  • 参数:无。返回:Future<void>。

fmChannelBefore() / fmChannelNext()

  • 作用:切换到上一个/下一个已保存的电台(频道步进,非频率扫描)。
  • 参数:无。返回:Future<void>。

fmSearchForward() / fmSearchNext()

  • 作用:向高频方向(Forward)/低频方向(Next,注释为 "FM backward search")手动搜索下一个信号。
  • 参数:无。返回:Future<void>。

fmPlayPause()

  • 作用:切换 FM 播放/暂停状态。
  • 参数:无。返回:Future<void>。

fmSelectFrequency(double frequency)

  • 作用:直接指定频率调台。
  • 参数:frequency(double)— 目标频率,单位 MHz;应位于 87.5 ~ 108.0 范围内(与 FMConstants.fmMinFrequency/fmMaxFrequency 对应),通过 BleMethodConstants.argFrequency 键传递。
  • 返回:Future<void>。

BleEventStream / BleAudioProcessor(接收事件)

BleEventStream.fmInfoStream → Stream<Map<String, dynamic>>

  • 公开的 FM 状态事件流,转发自 BleAudioProcessor.fmInfoStream。
  • 事件载荷键:BleEventConstants.keyFmStatus(int,0=暂停,1=播放)、BleEventConstants.keyFmFrequency(double,MHz)。
  • 缺省值:status 缺省 0,frequency 缺省 0.0(见 ble_audio_processor.dart)。

事件常量(BleEventConstants)

常量值用途
typeFmInfo'fm_info'FM 状态事件类型标识(ble_event_constants.dart)
keyFmStatus—事件载荷中的播放状态键
keyFmFrequency—事件载荷中的频率键(double,MHz)

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

设备未处于 FM 模式

在音乐/LineIn 模式下直接调用 FM 搜索或频率选择指令可能被设备忽略。正确做法是先 changeFMMode() 并等待 fm_info 事件确认模式生效,再执行后续操作(示例页 _initializeFm() 即采用此顺序)。

状态字段缺失或类型不符

事件映射层为 keyFmStatus/keyFmFrequency 提供了 ?? 0 / ?? 0.0 兜底,且 as int? / as double? 可空强转——但若设备上报的字段类型与预期不符(例如频率以字符串返回),强转会抛 TypeError。当前实现假定原生端始终按约定类型上报。

蓝牙断连

设备断开后事件流不再产生数据,但 UI 不会自动感知。示例页通过 ConnectionStateManager 监听连接状态并在断开时自动退出页面(fm_page.dart)。自定义实现应同样监听连接事件并清理 FM 状态。

指令与事件之间的竞态

fmSelectFrequency 是异步下发的,事件回传需要若干 BLE 周期。若用户在滑块上连续快速拖动,会累积多条未确认指令,设备端按到达顺序执行,最终事件频率可能与最后一次滑块位置一致,也可能存在中间帧。UI 应以最终事件为准,而不是以本地期望值直接覆盖显示。

搜索期间的状态

搜索过程中设备可能不响应频道切换指令,或上报中间频率。页面用 _isFmSearching 标志切换按钮样式(圆角 14.0 vs 9.0),提示用户当前处于搜索态。业务实现应在搜索期间禁用或降级相关操作,等待 fmStopSearch() 或搜索完成事件。

性能与运维注意点

  • Stream 订阅必须取消:FmPage.dispose() 中调用 _fmSubscription?.cancel(),否则页面退出后回调仍会触发 setState(尽管有 mounted 保护)并造成订阅泄漏;
  • 指令频率控制:BLE 通道带宽有限,应避免高频连续调用 fmSelectFrequency;UI 滑块场景建议在 onChangeEnd 时才真正下发指令(或做节流),而非每次 onChanged 都发送;
  • 事件流是广播式的:filterByType 从统一事件总线分发,多个页面同时订阅会各自收到完整事件;每个订阅者各自维护状态,互不影响,但应避免重复监听造成多余开销。

扩展点

  • 新增 FM 指令:在 BleFmManager 增加静态方法并对应扩展 BleMethodConstants 的平台方法名;无需改动事件层。原生端需同步实现新方法(FMManager.kt);
  • 扩展状态字段:如需上报信号强度、电台名称等更多信息,需同时修改原生端 FmStatusInfo 模型、BleEventConstants 新键、BleAudioProcessor.fmInfoStream 的映射逻辑三处,并保持缺省值策略一致;
  • 自定义 UI:fmInfoStream 是公开且无 UI 绑定的纯数据流,任何页面/组件都可以独立订阅实现自定义收音机界面,无需复用 FmPage。

Related Links

  • 蓝牙事件流 BleEventStream — FM 事件流入口
  • BleFmManager 源码 — FM 指令门面
  • BleAudioProcessor 源码 — FM 事件过滤与映射
  • FmPage 示例页面 — FM 收音机 UI 完整实现
  • Android 原生 FMManager — 原生桥接层(跨端边界)
  • 音频播放与音乐状态(ID3)属于设备媒体能力的另一部分,见对应页面
Prev
闹钟管理
Next
灯光控制