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

    • 环境要求与权限配置
    • 集成SDK依赖
    • 运行示例应用
  • 核心架构与协议

    • RCSP协议与数据通道
    • 蓝牙连接与设备管理
    • TWS双耳功能
    • 基础功能接口与自定义命令
  • 设备功能控制

    • 设备音乐控制与ID3信息
    • 文件浏览与传输
    • FM收音与发射
    • 灯光控制
    • 闹钟与时间管理
    • 查找设备与防丢
    • ANC与噪声处理
    • 按键功能设置
    • 彩屏仓控制
    • AI翻译
  • 音效与音频处理

    • 均衡器音效调节
    • 录音与语音控制
    • Line-in、SPDIF与声卡功能
    • 音频编解码库
  • 扩展功能库

    • OTA固件升级
    • 数据加密与解密
    • 图片与动图格式转换
  • 示例应用 btsmart

    • 应用架构与界面导航
    • 设备功能适配与数据层
    • 设备配置JSON与资源文件
  • 参考与版本

    • 错误码参考
    • 版本历史与更新日志
    • 开发文档中心导航

FM收音与发射

FM收音与发射(FM RX/TX)是 Jieli 蓝牙 RCSP 协议中多媒体控制的重要功能模块:App 通过 RCSPController 与设备(小机)通信,实现 FM 收音(搜索频道、选台、播放/暂停、收藏频点)以及 FM 发射(TX)的完整控制链路,并配合刻度尺(RulerView)、搜索弹窗等自定义控件完成交互。

Purpose and Scope

本页面向 device-functions.fm-control 目录主题,完整介绍 FM 控制子系统在 App 侧的实现:

  • FM 接收(收音)控制链路:FMControlFragment → FMControlViewModel → RCSPController → 设备,以及状态/频道回调如何驱动 UI 刷新;
  • FM 状态数据模型:FmStatusInfo(播放状态、频点、频率、模式);
  • 收藏频点持久化:基于 Room 的 FMCollectInfoEntity 与 DataRepository 读写;
  • FM 发射(TX)入口:FMTXControlFragment / FMTXControlViewModel 的职责边界;
  • SDK 侧 API 用法:以 FmDemo.java 为示例说明 RCSPController 的 FM 命令与回调。

以下内容属于其他目录页、此处不做展开:摇一摇切歌(JLShakeItManager)整体机制、多媒体主控页布局、RCSP 协议底层报文封装。相关说明见文末「Related Links」。

Overview

FM 控制是典型的一问一答式 RCSP 功能:App 端发起命令(如获取 FM 状态、向前搜索频道、播放指定频点),设备端处理后通过事件回调把结果推回 App,由 ViewModel 转为 LiveData 驱动 UI。

整个子系统分为三层:

  1. UI 层:FMControlFragment(收音页)与 FMTXControlFragment(发射页),通过 DataBinding 绑定 ViewModel,配合 FMRulerView 刻度尺、FMSearchDialog 搜索弹窗、FMFreqCollectAdapter 收藏列表等控件;
  2. 逻辑层:FMControlViewModel / FMTXControlViewModel,继承 BtBasicVM,持有 RCSPController 单例,负责命令下发、回调分发、命令节流与收藏读写;
  3. 数据层:DataRepository + Room(FMCollectInfoEntity / FMCollectInfoDao),以及 SDK 侧的 FmStatusInfo、ChannelInfo 等设备数据结构。

设计意图:把"设备命令"与"UI 状态"解耦——ViewModel 只暴露语义化动作(onFMPlayNextChannel() 等)和 LiveData 状态,UI 层不直接触碰 RCSP 协议细节;同时通过 isCanSendFMCmdToDevice()、checkIsQuickContinuousSend() 在命令入口做统一拦截,避免在断连或用户快速连续操作时向设备发送无效命令。

Architecture

flowchart TD
    subgraph sg_UI["UI 层 (btsmart)"]
        RXFragment["FMControlFragment"]
        TXFragment["FMTXControlFragment"]
        RulerView["FMRulerView"]
        SearchDialog["FMSearchDialog"]
        CollectAdapter["FMFreqCollectAdapter"]
    end

    subgraph sg_VM["逻辑层 (ViewModel)"]
        RXVM["FMControlViewModel"]
        TXVM["FMTXControlViewModel"]
        BtBasicVM["BtBasicVM (基类)"]
    end

    subgraph sg_SDK["RCSP SDK 层"]
        Controller["RCSPController (单例)"]
        EventCB["BTRcspEventCallback"]
        ActionCB["OnRcspActionCallback"]
    end

    subgraph sg_Data["数据层"]
        DataRepo["DataRepository"]
        RoomDB[(Room / FMCollectInfoDao)]
        Entity["FMCollectInfoEntity"]
    end

    subgraph sg_Device["设备端 (小机)"]
        Device["蓝牙设备 (FM 收音/发射)"]
    end

    RXFragment -->|"DataBinding / LiveData"| RXVM
    TXFragment --> TXVM
    RXFragment --> RulerView
    RXFragment --> SearchDialog
    RXFragment --> CollectAdapter
    RXVM --> BtBasicVM
    TXVM --> BtBasicVM
    RXVM -->|"fmPlaySelectedFrequency / fmSearchAllChannels 等"| Controller
    TXVM --> Controller
    Controller -->|"getFmInfo / fmForwardSearchChannels"| Device
    Device -->|"onFmStatusChange / onFmChannelsChange"| EventCB
    EventCB --> RXVM
    ActionCB --> RXVM
    RXVM -->|"insert / delete FMCollectInfo"| DataRepo
    DataRepo --> RoomDB
    RoomDB --> Entity

架构说明:

  • FMControlFragment 通过 ViewModelProvider 获取 FMControlViewModel(见 FMControlFragment.java),DataBinding 将 ViewModel 直接暴露给布局(mBinding.setFmControlViewModel(...))。
  • FMControlViewModel 构造函数即注册 BTRcspEventCallback,release() 时注销,保证与页面生命周期绑定、不泄漏监听器(见 FMControlViewModel.java)。
  • 命令层统一走 RCSPController 单例,回调分两类:动作结果回调 OnRcspActionCallback<Boolean>(命令是否送达/执行)与事件回调 BTRcspEventCallback(设备主动上报的状态/频道变化)。
  • 收藏数据与设备无关地持久化到 Room,按设备 MAC(btDeviceSSid)隔离。

核心流程

获取 FM 状态

App 打开收音页时调用 getFMInfo(),命令下发后设备上报 FmStatusInfo,经 onFmStatusChange 回调写入 fmStatusInfoLiveData,UI 随之刷新播放/暂停图标、当前频率与频道号。

sequenceDiagram
    participant F as FMControlFragment
    participant VM as FMControlViewModel
    participant RC as RCSPController
    participant D as 蓝牙设备
    participant CB as BTRcspEventCallback

    F->>VM: getFMInfo()
    VM->>RC: getFmInfo(device, null)
    RC->>D: 下发 RCSP FM 状态查询命令
    D-->>RC: 状态数据上报
    RC-->>CB: onFmStatusChange(device, FmStatusInfo)
    CB-->>VM: 更新 fmStatusInfoLiveData
    VM-->>F: LiveData 观察者收到新状态
    F->>F: 刷新播放状态 / 频率 / 频道 UI

搜索频道流程

向前/向后/全部搜索都会先 setPlayStateToPause() 暂停播放、弹出扫描对话框并播放搜索动画,然后下发对应命令;设备端把搜索到的频道列表通过 onFmChannelsChange(device, List<ChannelInfo>) 推回,写入 channelListLiveData。

flowchart TD
    Start([用户点击搜索]) --> Gate{"isCanSendFMCmdToDevice?"}
    Gate -->|"No"| Ignore["忽略操作"]
    Gate -->|"Yes"| Quick{"checkIsQuickContinuousSend?"}
    Quick -->|"True"| Ignore
    Quick -->|"False"| Pause["setPlayStateToPause"]
    Pause --> Dialog["scanDialogShowStateLiveData = true"]
    Dialog --> Gif["fmSearchIngGifLiveData = ic_fm_searching"]
    Gif --> Cmd["fmForwardSearchChannels / fmBackwardSearchChannels / fmSearchAllChannels"]
    Cmd --> Scan["设备搜索中"]
    Scan --> Found["onFmChannelsChange 回调"]
    Found --> Update["channelListLiveData 更新收藏/列表 UI"]
    Update --> Stop["onFMSearchStop 停止搜索"]
    Stop --> End([结束])

选台与播放控制

用户滑动 FMRulerView 选中频率后,onFMPlaySelectFreq(float freq) 将频率放大 10 倍(875 表示 87.5MHz)存入 fmSelectedFreqLiveData,并调用 fmPlaySelectedFrequency 下发选台命令;上一台/下一台、上一频点/下一频点、播放暂停均有对应的语义化方法。

状态模型:FmStatusInfo

App 侧 FmStatusInfo(FmStatusInfo.java)是设备状态在 App 内的投影,包含四个字段:

字段类型含义
isPlayboolean是否正在播放(收音中)
channelint当前频道号
freqfloat当前频率(MHz)
modeint工作模式

提供全参构造函数、getter/setter 与 toString()。SDK 侧 com.jieli.bluetooth.bean.device.fm.FmStatusInfo 与 ChannelInfo 用于事件回调,App 侧模型与之映射。

收藏频点与本地持久化

收藏是 FM 收音页的重要交互:用户把当前频点加入收藏,之后可通过收藏列表快速选台。收藏数据按设备 MAC 隔离存储,切换设备后互不干扰。

public void onFMFreqAddCollect() {
    String address = getConnectedDevice() == null ? "11:22:33:44:55:66" : getConnectedDevice().getAddress();
    FMCollectInfoEntity entity = new FMCollectInfoEntity();
    entity.setBtDeviceSSid(address);
    entity.setPlay(fmStatusInfoLiveData.getValue() != null && fmStatusInfoLiveData.getValue().isPlay());
    entity.setFreq(fmCurrentFreqLiveData.getValue());
    entity.setMode(fmStatusInfoLiveData.getValue() == null ? 0 : fmStatusInfoLiveData.getValue().getMode());
    DataRepository.getInstance().insertFMCollectInfo(entity, new DataRepository.DataRepositoryCallback() {
        @Override
        public void success() {//添加成功
            fmCollectStateLiveData.postValue(true);
        }

        @Override
        public void failed() {//代表已存在
            fmCollectStateLiveData.postValue(false);
        }
    });
}

Source: FMControlViewModel.java

要点:

  • 收藏实体 FMCollectInfoEntity 记录设备地址(btDeviceSSid)、播放状态、频率与模式;无设备时使用占位地址 "11:22:33:44:55:66",避免空指针;
  • DataRepository 回调区分两种结果:success() 表示新增成功,failed() 表示该频点已存在(收藏去重逻辑在数据层完成);
  • 删除收藏时若列表只剩一条,会自动退出收藏管理态(onFMFreqDeleteCollect 中 size() == 1 时置 fmCollectManageStateLiveData = false),避免用户删除最后一项后仍停留在管理态;
  • 收藏列表通过 fmCollectFreqLiveData 暴露给 UI,由 FMFreqCollectAdapter 渲染,DAO 为 FMCollectInfoDao。

命令入口统一拦截与防抖

ViewModel 中几乎所有动作方法都以 isCanSendFMCmdToDevice() 开头,搜索类动作还额外叠加 checkIsQuickContinuousSend() 快速连点保护:

public void onFMPlaySelectFreq(float freq) {
    if (!isCanSendFMCmdToDevice()) return;
    fmSelectedFreqLiveData.setValue((int) (freq * 10));
    JL_Log.d("zhm_fm", "onFMPlaySelectFreq: " + freq);
    mRCSPController.fmPlaySelectedFrequency(getConnectedDevice(), freq, null);
}

Source: FMControlViewModel.java

设计意图:

  • 断连保护:设备未连接/未初始化时直接丢弃命令,避免 BLE 写入失败引发异常或体验问题;
  • 快速连点保护:mLastSendTime 记录上次发命令时间,checkIsQuickContinuousSend() 在用户极短时间内重复触发(如狂点搜索)时拦截,防止命令风暴导致设备端处理错乱;fmCurrentFreqLiveData 的默认值 875 与 mIsViewFlingStop 配合 FMRulerView 的 fling 惯性滑动,确保刻度尺滑动过程中不频繁下发选台命令。

FM 发射(TX)侧

发射侧由 FMTXControlFragment(UI)与 FMTXControlViewModel(逻辑)组成,位于同一 ui/multimedia/control/fm 与 viewmodel 目录,与收音侧结构对称:同样继承 BtBasicVM、复用 RCSPController 下发发射频率等命令。发射与接收共用 FmStatusInfo/ChannelInfo 设备数据模型与收藏持久化体系,二者的差异集中在命令语义与 UI 展示(发射页关注发射频率设置与状态展示,收音页关注频道搜索与选台)。发射侧具体命令细节未在本页展开,请以 SDK 侧 RCSPController 实现为准。

Usage Examples

1. 判断设备是否支持 FM 及当前是否处于 FM 模式

SDK 的 DeviceInfo 提供能力位与当前功能标志,是进入 FM 页面前的前置校验:

public boolean isSupportFMMode() {
    //获取RCSPController对象
    RCSPController controller = RCSPController.getInstance();
    //获取当前操作设备
    BluetoothDevice usingDevice = controller.getUsingDevice();
    if (null == usingDevice) return false;
    DeviceInfo deviceInfo = controller.getDeviceInfo(usingDevice);
    if (null == deviceInfo) return false; //设备未初始化
    return deviceInfo.isFmEnable();
}

public boolean isDeviceInFMMode() {
    RCSPController controller = RCSPController.getInstance();
    BluetoothDevice usingDevice = controller.getUsingDevice();
    if (null == usingDevice) return false;
    DeviceInfo deviceInfo = controller.getDeviceInfo(usingDevice);
    if (null == deviceInfo) return false; //设备未初始化
    return deviceInfo.getCurFunction() == AttrAndFunCode.SYS_INFO_FUNCTION_FM;
}

Source: FmDemo.java

2. 获取 FM 状态信息(命令 + 事件回调配合)

getFmInfo 只是发起查询,真正的状态数据在 BTRcspEventCallback#onFmStatusChange 中异步送达:

void getFMStatus() {
    RCSPController controller = RCSPController.getInstance();
    controller.addBTRcspEventCallback(new BTRcspEventCallback() {
        @Override
        public void onFmStatusChange(BluetoothDevice device, FmStatusInfo fmStatusInfo) {
            //此处回调FM状态信息
        }
    });
    controller.getFmInfo(controller.getUsingDevice(), new OnRcspActionCallback<Boolean>() {
        @Override
        public void onSuccess(BluetoothDevice device, Boolean message) {
            //成功回调
            //结果将会在BTRcspEventCallback#onFmStatusChange回调
        }

        @Override
        public void onError(BluetoothDevice device, BaseError error) {
            //失败回调
            //error - 错误信息
        }
    });
}

Source: FmDemo.java

3. 频道搜索(前向搜索示例)

搜索类命令同样采用"动作回调确认 + 事件回调推送频道列表"的双回调模式:

void fmScanForward() {
    RCSPController controller = RCSPController.getInstance();
    controller.addBTRcspEventCallback(new BTRcspEventCallback() {
        @Override
        public void onFmChannelsChange(BluetoothDevice device, List<ChannelInfo> channels) {
            //此处回调已发现的FM频道列表
        }
    });
    controller.fmForwardSearchChannels(controller.getUsingDevice(), new OnRcspActionCallback<Boolean>() {
        @Override
        public void onSuccess(BluetoothDevice device, Boolean message) {
            //成功回调
        }

        @Override
        public void onError(BluetoothDevice device, BaseError error) {
            //失败回调
        }
    });
}

Source: FmDemo.java

4. 收音页 ViewModel 接线(Fragment 侧)

FMControlFragment 通过 ViewModelProvider 创建 ViewModel,并用 DataBinding 绑定,页面销毁后由 BtBasicVM.release() 自动注销回调:

@Override
public void actionsOnViewInflate() {
    super.actionsOnViewInflate();
    ViewModelProvider provider = new ViewModelProvider(this);
    mFMControlViewModel = provider.get(FMControlViewModel.class);
    mBinding.setFmControlViewModel(mFMControlViewModel);
    mBinding.setFmReceiveSuspension(suspensionLiveData);
    mBinding.setLifecycleOwner(this);
    mBinding.getRoot().setOnClickListener(v -> {
    });//拦截点击事件而已
}

Source: FMControlFragment.java

API Reference

RCSPController FM 命令(App → 设备)

以下方法均来自 com.jieli.bluetooth.impl.rcsp.RCSPController 单例,调用形态统一为 方法(BluetoothDevice device, OnRcspActionCallback<Boolean> callback);callback 可传 null(如 ViewModel 中多数调用)。

方法说明
getFmInfo(device, callback)查询 FM 状态;结果经 BTRcspEventCallback#onFmStatusChange 回调
fmPlaySelectedFrequency(device, float freq, callback)播放指定频点(freq 单位 MHz,如 87.5)
fmPlayPrevChannel(device, callback)播放上一频道
fmPlayNextChannel(device, callback)播放下一频道
fmPlayPrevFrequency(device, callback)播放上一频点
fmPlayNextFrequency(device, callback)播放下一频点
fmPlayOrPause(device, callback)播放/暂停切换
fmForwardSearchChannels(device, callback)向前搜索频道;结果经 onFmChannelsChange 回调
fmBackwardSearchChannels(device, callback)向后搜索频道
fmSearchAllChannels(device, callback)全部频道搜索
fmStopSearch(device, callback)停止搜索

BTRcspEventCallback 事件回调(设备 → App)

回调方法触发时机参数说明
onFmStatusChange(BluetoothDevice device, FmStatusInfo fmStatusInfo)设备上报 FM 状态变化FmStatusInfo 含播放状态、频道、频率、模式
onFmChannelsChange(BluetoothDevice device, List<ChannelInfo> channels)设备上报搜索到的频道列表ChannelInfo 为 SDK 侧频道模型

OnRcspActionCallback<Boolean> 动作回调

  • onSuccess(BluetoothDevice device, Boolean message):命令已送达/执行成功;
  • onError(BluetoothDevice device, BaseError error):失败,error 携带错误信息。

FMControlViewModel 对外 LiveData 状态

LiveData类型初始值含义
scanDialogShowStateLiveDataBooleanfalse扫描弹窗是否显示
channelListLiveDataArrayList<Integer>—设备扫描到的 FM 频道
fmStatusInfoLiveDataFmStatusInfo—设备回复的 FM 状态
fmCollectFreqLiveDataList—收藏频点列表
fmCurrentFreqLiveDataInteger875刻度尺当前频点(×10,87.5MHz)
fmSelectedFreqLiveDataInteger—刻度尺选中频点
fmShowSearchLiveDataBoolean—搜索弹窗显示状态
fmCollectManageStateLiveDataBooleanfalse收藏管理态开关
fmCollectStateLiveDataBoolean—收藏结果(true 成功 / false 已存在)
fmSearchIngGifLiveDataIntegerR.drawable.ic_fm_searching搜索动画 Gif 资源

Configuration Options

FM 控制没有独立的配置文件,运行时状态/行为由以下默认值与常量决定:

配置项类型默认值说明
fmCurrentFreqLiveData 初始值int875刻度尺起始频点,表示 87.5MHz
scanDialogShowStateLiveData 初始值booleanfalse进入页面时默认不显示扫描弹窗
fmCollectManageStateLiveData 初始值booleanfalse默认非收藏管理态
fmSearchIngGifLiveData 初始值intR.drawable.ic_fm_searching搜索动画起始帧
无设备时收藏地址占位String"11:22:33:44:55:66"onFMFreqAddCollect 中防空处理
AttrAndFunCode.SYS_INFO_FUNCTION_FMintSDK 定义设备当前功能为 FM 的模式标志
JLShakeItManager.MODE_CUT_SONG_TYPE_FMint常量摇一摇切歌的 FM 切歌类型
SConstant.ALLOW_SWITCH_FUN_DISCONNECTboolean常量允许切换功能时断开连接的开关(Fragment 静态引用)

Failure Modes, Edge Cases & Concurrency

  • 设备未连接 / 未初始化:所有动作命令前先执行 isCanSendFMCmdToDevice(),不满足直接 return,UI 无响应但不产生 BLE 写入异常;FmDemo 中通过 getUsingDevice() 与 getDeviceInfo() 双重判空并检查 isFmEnable()。
  • 非 FM 模式:getCurFunction() != SYS_INFO_FUNCTION_FM 时,App 不应进入 FM 操作(Demo 提供 isDeviceInFMMode() 校验)。
  • 快速连续操作:搜索类命令叠加 checkIsQuickContinuousSend()(基于 mLastSendTime)防抖,防止命令风暴;选台依赖 mIsViewFlingStop 标记,刻度尺 fling 惯性滑动未停止时不下发选台命令,避免设备端频率抖动。
  • 收藏重复:DataRepository.insertFMCollectInfo 对重复频点返回 failed(),UI 通过 fmCollectStateLiveData=false 提示"已存在";删除最后一条收藏时自动退出管理态。
  • 并发/线程模型:设备事件回调可能来自 BLE 工作线程,ViewModel 使用 LiveData.postValue()(异步线程安全)与 setValue()(主线程)区分场景;MutableLiveData 保证 UI 只观察主线程更新。
  • 页面生命周期:BTRcspEventCallback 在构造时注册、release() 时注销,避免页面销毁后仍接收设备事件导致内存泄漏或空引用。

Performance & Operational Notes

  • FM 命令为低频交互(选台/搜索/播放控制),单条命令体积小,BLE 传输开销可忽略;主要性能关注点是避免高频命令,即上文 mLastSendTime 与 fling 状态节流。
  • 搜索动画通过 fmSearchIngGifLiveData 切换资源帧,UI 侧只做状态监听,Gif 播放开销集中在 Fragment 的 Dialog 控件内。
  • 收藏读写走 Room(DataRepository 单例),插入为异步回调模式,不阻塞主线程;删除/查询同样由 DAO 承担。

Extension Points

  • 新 FM 动作:在 FMControlViewModel 增加语义化方法,内部调用 RCSPController 对应命令即可;UI 通过 DataBinding 绑定新方法,无需改动 SDK。
  • 自定义搜索交互:FMSearchDialog 是独立 Widget(ui/widget/FMSearchDialog.java),可替换为自定义弹窗;onShowFMSearchView 已预留 dismissCallback 联动 fmShowSearchLiveData。
  • 刻度尺交互:FMRulerView(ui/widget/rulerview/FMRulerView.java)负责滑动手势与频率映射,配合 setIsViewFlingStop() 控制命令下发时机。
  • 收藏扩展:FMCollectInfoEntity 可增字段(如备注、标签),配合 DAO 迁移即可扩展收藏维度。

Related Links

  • FMControlViewModel.java — 收音逻辑核心
  • FMTXControlViewModel.java — 发射逻辑
  • FMControlFragment.java — 收音页 UI
  • FMTXControlFragment.java — 发射页 UI
  • FmStatusInfo.java — 状态模型
  • FMCollectInfoEntity.java — 收藏实体
  • FmDemo.java — SDK FM 命令/回调示例
  • 相关页面:多媒体控制主流程、摇一摇切歌(JLShakeItManager)、RCSP 连接管理(RCSPController)。
Prev
文件浏览与传输
Next
灯光控制