杰理 SDK 文档中心
首页
首页
  • 项目概述

    • 项目简介与核心能力
    • 运行环境与SDK版本
  • 快速开始

    • 工程导入与依赖配置
    • 权限配置与示例运行
  • 平台架构

    • SDK分层架构与RCSP协议
    • 蓝牙连接库
    • 健康SDK核心库 JL_Watch
    • 健康服务器与云端服务
  • 健康与运动数据

    • 健康数据同步
    • 运动数据同步
    • 本地数据持久化
  • 设备管理功能

    • 表盘管理
    • 闹钟与健康提醒
    • 消息与联系人同步
    • 天气同步
    • 设备查找
    • 支付宝集成
  • 传输与媒体处理

    • 文件传输与文件管理
    • 音乐传输与播放控制
    • 图像转换库
    • 音频编解码与解密
  • OTA 升级

    • 固件空中升级流程
    • 4G模块与差分升级
  • AI 能力

    • AI表盘与云服务
    • AI语音助手
  • 示例应用

    • HealthAide 健康助手应用
    • WatchTestTool 测试工具
  • 开发者指南

    • 自定义命令扩展
    • 调试技巧与问题排查
    • 版本历史与兼容性

运动数据同步

运动数据同步是「宜动健康」App 与杰理智能手表/手环固件之间的核心数据通道,覆盖运动状态信息(类型、状态、ID、开始时间、心率模式)的读取同步、运动实时数据(心率、配速等)的周期性轮询同步,以及运动记录与轨迹数据的请求下发,全部基于 jl_rcsp SDK 的 RCSP 蓝牙命令体系实现。

Purpose and Scope

本文档深入介绍运动数据同步能力(Catalog: 4-health-sports / 4.2-sport-data)的实现机制,包括:

  • 运动数据同步的整体架构与两条同步通道(状态同步 + 实时数据轮询同步);
  • DeviceSportsServiceImpl 固件运动控制服务与 DeviceSyncRealDataServiceImpl 实时数据同步服务的实现细节;
  • SportsInfo 运动信息模型与 HealthApplication 全局运动模式状态;
  • 运动记录/轨迹请求入口(IRequestRecordHandler / DeviceRequestRecordHandler)及其与运动数据的关联;
  • 失败重试、并发调度、性能特征与扩展点。

以下主题属于同目录下的兄弟页面,本文只做交叉引用、不展开:运动页面的 UI 交互与地图渲染(ui.sports.map 包)、室内/室外跑步的差异化服务实现(DeviceIndoorRunningServiceImpl / DeviceOutdoorRunningServiceImpl)、运动通知发送(ui.sports.notify 包)、蓝牙连接与 RCSP 协议底层(jl_rcsp SDK)。

Overview

运动数据同步解决一个核心问题:App 与设备固件之间如何可靠、低延迟地保持运动状态与运动数据的一致。

在运动场景中,设备(手表/手环)是运动数据的采集方(心率、步频、配速、GPS 轨迹等),App 是数据的消费方与展示方。由于 BLE 通道带宽有限、且设备端运动状态由固件自主管理,App 无法"推送"数据,只能通过 RCSP 命令主动读取。因此整个同步机制建立在两条互补的通道上:

  1. 状态同步(一次性拉取):进入运动页时,DeviceSportsServiceImpl.start() 调用 HealthOpImpl.readSportsInfo() 从固件拉取当前运动信息——运动类型(室外/室内)、状态、运动 ID、开始时间、实时数据同步间隔、心率模式——填充到 SportsInfo 模型,并据此更新 UI 与全局运动模式。
  2. 实时数据同步(周期性轮询):DeviceSyncRealDataServiceImpl 以固件返回的 readRealDataInterval 为间隔,通过主线程 Handler 反复调用 HealthOpImpl.readRealTimeSportsData() 拉取 RealTimeSportsData,转换为 DeviceRealData 后通过 RealDataListener 推送给 UI;失败时(ERR_RESPONSE_BAD_RESULT)按相同间隔退避重试。

设计意图:用"固件规定节奏的主动轮询"替代"事件推送",一是因为 RCSP 协议栈以请求-应答模型为主,二是把同步节奏的控制权交给固件(由 readRealDataInterval 决定),避免 App 端过度请求拖垮 BLE 链路或造成设备端功耗问题。全局运动模式(HealthApplication.sportMode)则是 App 侧对设备运动状态的一份镜像,供多个页面(地图、心率、运动详情)共享判断。

Architecture

flowchart TD
    subgraph sg_App["应用层 com.jieli.healthaide"]
        HealthApp["HealthApplication<br/>(全局运动模式 sportMode)"]
        SportsUI["运动页面 / 地图 UI"]
        RecordHandler["DeviceRequestRecordHandler<br/>(运动记录/轨迹请求)"]
    end

    subgraph sg_Service["运动服务层 ui.sports.service"]
        SportsSvc["SportsService 接口"]
        AbsServer["AbstractSportsServerImpl<br/>(抽象基类)"]
        DS["DeviceSportsServiceImpl<br/>(固件运动控制/状态同步)"]
        SDR["DeviceSyncRealDataServiceImpl<br/>(实时数据轮询同步)"]
    end

    subgraph sg_SDK["杰理 RCSP SDK (jl_rcsp)"]
        HealthOp["HealthOpImpl"]
        Cmd["SportsInfoStatusSyncCmd / RealTimeSportsData"]
    end

    subgraph sg_Device["蓝牙设备"]
        Watch["手表/手环固件"]
    end

    SportsUI --> DS
    SportsUI --> SDR
    DS --> SDR
    DS --> AbsServer
    SDR --> AbsServer
    DS -.实现.-> SportsSvc
    SDR -.实现.-> SportsSvc
    HealthApp --> DS
    DS -->|"readSportsInfo / pauseSports / resumeSports"| HealthOp
    SDR -->|"readRealTimeSportsData"| HealthOp
    RecordHandler -->|"记录数据请求"| HealthOp
    HealthOp --> Cmd
    Cmd <-->|"BLE 命令 / 应答"| Watch

各组件职责

组件职责关键证据
HealthApplication维护全局运动模式(SPORT_MODE_IDLE/OUTDOOR/INDOOR),提供 getSportMode() / setSportMode()HealthApplication.java
SportsService 接口运动服务的统一契约(start/pause/resume/stop 等生命周期方法)由两个服务实现
AbstractSportsServerImpl<T>抽象基类,泛型参数 T 为实时数据类型(如 DeviceRealData)ui.sports.service 包内基类
DeviceSportsServiceImpl固件运动控制服务:拉取运动信息、暂停/恢复运动,并组合实时同步服务DeviceSportsServiceImpl.java
DeviceSyncRealDataServiceImpl设备实时数据同步:按固件规定间隔轮询 readRealTimeSportsData,通过监听器推送DeviceSyncRealDataServiceImpl.java
HealthOpImpljl_rcsp SDK 的健康操作封装,向下发送 RCSP 命令jl_rcsp 库(AAR 依赖)
IRequestRecordHandler / DeviceRequestRecordHandler运动记录(含轨迹)请求的处理接口与设备实现ui.sports.record 包
RealDataListener<T>实时数据回调接口,UI 通过它订阅数据更新ui.sports.listener 包

两个服务都继承 AbstractSportsServerImpl<DeviceRealData> 并实现 SportsService,且 DeviceSportsServiceImpl 在构造时组合创建 DeviceSyncRealDataServiceImpl——这是典型的"模板方法 + 组合"结构:生命周期由基类/接口统一管理,实时同步逻辑以组合方式复用。

说明:SportsService 接口与 AbstractSportsServerImpl 的完整方法清单位于 ui.sports.service 包内,本文基于两个实现类的源码描述其行为;接口细节以源码为准。

同步机制详解

1. 运动状态同步(一次性拉取)

运动状态同步由 DeviceSportsServiceImpl.start() 驱动。start() 首先调用 addListener() 注册 RCSP 监听,然后向固件发起 readSportsInfo 请求:

@Override
public void start() {
    addListener();
    mHealthOp.readSportsInfo(mWatchManager.getConnectedDevice(), new OnOperationCallback<com.jieli.jl_rcsp.model.device.health.SportsInfo>() {
        @Override
        public void onSuccess(com.jieli.jl_rcsp.model.device.health.SportsInfo result) {
            if (BuildConfig.DEBUG && sportsInfo.type != result.getMode()) {
                ToastUtil.showToastLong(R.string.inconsistent_motion_state + result.getMode());
            }
            sportsInfo.type = result.getMode();//运动类型
            sportsInfo.useMap = result.getMode() == SportsInfoStatusSyncCmd.SPORTS_TYPE_OUTDOOR; //运动类型
            sportsInfo.status = result.getState(); //运动状态
            sportsInfo.id = result.getId(); //运动id
            sportsInfo.startTime = RcspUtil.intToTime(result.getId()); //运动开始时间
            sportsInfo.readRealDataInterval = result.getReadRealTimeDataInterval(); //同步运动实时数据的时间间隔
            sportsInfo.heartRateMode = result.getHeartRateMode();//运动的心率模式
            JL_Log.d(tag, "readSportsInfo", sportsInfo + ",\n" + result);
            changeStatus(sportsInfo.status);
        }

        @Override
        public void onFailed(BaseError error) {
            JL_Log.w(tag, "readSportsInfo", "获取运动数据失败: " + error);
        }
    });
}

Source: DeviceSportsServiceImpl.java

这段代码揭示了状态同步的几个关键设计决策:

  • 一次回调完成全量状态迁移:固件返回的 SportsInfo 同时携带类型、状态、ID、时间、间隔、心率模式六类信息,App 一次性将其映射进自己的 SportsInfo 模型,避免多次 BLE 往返。这正是"同步"的本质——以固件状态为唯一事实来源(source of truth),App 只做镜像。
  • 运动类型决定是否使用地图:useMap = (mode == SPORTS_TYPE_OUTDOOR),室外运动启用 GPS 地图(配合 ui.sports.map 包),室内运动不启用。
  • 运动 ID 即开始时间:RcspUtil.intToTime(result.getId()) 将运动 ID 直接解析为开始时间,说明固件侧以时间戳作为运动记录的唯一标识,App 侧无需单独同步时间字段。
  • 同步节奏由固件下发:readRealDataInterval 由固件返回,App 不自行决定轮询频率——这是对 BLE 链路与设备功耗的双重保护。

pause() 与 resume() 则是对固件运动状态的主动控制,通过 mHealthOp.pauseSports / mHealthOp.resumeSports 下发 RCSP 命令,失败时仅记录 JL_Log.w 日志、不中断 UI:

@Override
public void pause() {
    mHealthOp.pauseSports(mWatchManager.getConnectedDevice(), new OnOperationCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) { }

        @Override
        public void onFailed(BaseError error) {
            JL_Log.w(tag, "pauseSports", "主动暂停失败: " + error);
        }
    });
}

Source: DeviceSportsServiceImpl.java

2. 实时数据同步(周期性轮询)

实时数据同步是运动数据同步中频率最高、对稳定性要求最严的通道。DeviceSyncRealDataServiceImpl 用主线程 Handler 驱动一个自调度 Runnable:

private final Handler handler = HandlerManager.getInstance().getMainHandler();
private final Runnable readTask = new Runnable() {
    @Override
    public void run() {
        HealthOpImpl healthOp = WatchManager.getInstance().getHealthOp();
        healthOp.readRealTimeSportsData(healthOp.getConnectedDevice(), new OnOperationCallback<RealTimeSportsData>() {
            @Override
            public void onSuccess(RealTimeSportsData result) {
                realDataListener.onRealDataChange(new DeviceRealData(result));
                handler.removeCallbacks(readTask);
                handler.postDelayed(readTask, sportsInfo.readRealDataInterval);
            }

            @Override
            public void onFailed(BaseError error) {
                JL_Log.w("DeviceSyncRealDataServiceImpl", "onFailed", "主动获取实时运动数据:" + error);
                if (error.getSubCode() == RcspErrorCode.ERR_RESPONSE_BAD_RESULT) {
                    handler.postDelayed(readTask, sportsInfo.readRealDataInterval);
                }
            }
        });
    }
};

Source: DeviceSyncRealDataServiceImpl.java

该实现的关键特征:

  • 自调度轮询(self-rescheduling polling):每次 readTask 执行完成后,在 onSuccess 里才安排下一次执行(postDelayed(readTask, interval))。这意味着轮询间隔是执行间隔而非启动间隔,天然避免了任务堆积;如果上次请求未返回,不会叠加新的请求。
  • 成功回调才驱动下一次:onSuccess 中先 removeCallbacks 再 postDelayed,保证同一时刻队列里只有一个 readTask 实例,杜绝并发 BLE 请求。
  • 选择性失败重试:仅当子错误码为 ERR_RESPONSE_BAD_RESULT 时按原间隔重试;其他错误(如设备断开)不重试,避免在链路不可用时空转。注释中保留的 handler.postDelayed(readTask, 0) 展示了"立即重试"备选方案被刻意弃用——避免失败风暴。
  • 数据转换边界:SDK 返回的 RealTimeSportsData 在 onRealDataChange(new DeviceRealData(result)) 处被包装为 App 层模型 DeviceRealData(ui.sports.model 包),UI 层只依赖 App 模型、不感知 SDK 类型。

start() / stop() 管理轮询的生命周期,并额外注册屏幕亮灭广播:

@Override
public void start() {
    handler.removeCallbacks(readTask);
    handler.post(readTask);
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_SCREEN_OFF);
    filter.addAction(Intent.ACTION_SCREEN_ON);
    context.registerReceiver(broadcastReceiver, filter);
    broadcastReceiver.isRegister = true;
}

@Override
public void stop() {
    handler.removeCallbacks(readTask);
    if (broadcastReceiver.isRegister) {
        context.unregisterReceiver(broadcastReceiver);
        broadcastReceiver.isRegister = false;
    }
}

Source: DeviceSyncRealDataServiceImpl.java

ScreenBroadcastReceiver(同文件内部类)监听屏幕亮/灭,用于在灭屏时调整轮询行为(例如暂停高频轮询以省电),start() 先 removeCallbacks 再 post 保证重复调用幂等。

3. SportsInfo 模型

SportsInfo(ui.sports.model 包)是状态同步与实时同步共享的数据载体,其运动类型常量直接复用 RCSP 协议定义:

public static final int TYPE_OUTDOOR = SportsInfoStatusSyncCmd.SPORTS_TYPE_OUTDOOR;
public static final int TYPE_INDOOR =  SportsInfoStatusSyncCmd.SPORTS_TYPE_INDOOR;

Source: SportsInfo.java

SportsInfo 的字段(type、useMap、status、id、startTime、readRealDataInterval、heartRateMode)全部由 readSportsInfo 回调一次性填充(见上文 DeviceSportsServiceImpl.start())。该模型同时被 HealthApplication 的全局运动模式引用(SPORT_MODE_OUTDOOR = SportsInfo.TYPE_OUTDOOR),是 App 层与 SDK 层类型映射的枢纽。

4. 全局运动模式状态

HealthApplication 保存当前设备的运动模式镜像,供整个 App 共享:

/**
 * 不在运动模式
 */
public static final int SPORT_MODE_IDLE = 0;
/**
 * 室外运动模式
 */
public static final int SPORT_MODE_OUTDOOR = SportsInfo.TYPE_OUTDOOR;
/**
 * 室内运动模式
 */
public static final int SPORT_MODE_INDOOR = SportsInfo.TYPE_INDOOR;
...
private int sportMode = SPORT_MODE_IDLE;

public int getSportMode() {
    return sportMode;
}

public void setSportMode(int sportMode) {
    this.sportMode = sportMode;
}

Source: HealthApplication.java

设计意图:运动模式是跨页面共享的上下文(运动页、地图页、心率页都要判断"当前是否在室外跑步"),若各自向固件查询会造成重复 BLE 流量。将其提升为 Application 级单例状态,由运动服务在同步完成后通过 setSportMode 更新,其他页面通过 getSportMode 读取,实现"一次同步、多处消费"。

5. 运动记录与轨迹同步入口

历史运动记录(含 GPS 轨迹)的同步走独立通道:ui.sports.record 包定义了请求处理接口 IRequestRecordHandler 及其设备实现 DeviceRequestRecordHandler,配合数据模型 SportsRecordAndLocation(运动记录 + 位置点的组合模型)组织同步结果。该通道负责在运动结束后向固件批量拉取记录与轨迹,是"实时同步"之外的事后同步通道;其命令基于 jl_rcsp SDK 的 QueryFileTask(小文件传输任务,SportsInfo 中可见其 import),用于较大体积的轨迹数据分块传输。详细的分页/分块流程以 DeviceRequestRecordHandler 源码为准,本文不展开。

核心流程

下面以时序图完整展示一次运动数据同步的端到端执行过程(从进入运动页到持续实时刷新):

sequenceDiagram
    participant UI as 运动页面
    participant DS as DeviceSportsServiceImpl
    participant SD as DeviceSyncRealDataServiceImpl
    participant HO as HealthOpImpl
    participant W as 手表固件
    participant L as RealDataListener

    UI->>DS: start()
    DS->>DS: addListener() 注册 RCSP 监听
    DS->>HO: readSportsInfo(device)
    HO->>W: RCSP 读取运动信息命令
    W-->>HO: 应答(类型/状态/id/间隔/心率模式)
    HO-->>DS: onSuccess(result)
    DS->>DS: 填充 SportsInfo + changeStatus(status)
    DS->>SD: 构造并注入 SportsInfo(组合)
    UI->>SD: start()
    SD->>SD: handler.removeCallbacks + post(readTask)
    Note over SD: 注册屏幕亮灭广播
    loop 每 readRealDataInterval 毫秒(自调度)
        SD->>HO: readRealTimeSportsData(device)
        HO->>W: RCSP 读取实时运动数据命令
        W-->>HO: RealTimeSportsData
        HO-->>SD: onSuccess(result)
        SD->>L: onRealDataChange(new DeviceRealData(result))
        SD->>SD: removeCallbacks + postDelayed(readTask, interval)
    end
    UI->>SD: stop()
    SD->>SD: removeCallbacks(readTask)
    SD->>SD: 注销屏幕广播
    UI->>DS: pause() / resume()(用户操作时)
    DS->>HO: pauseSports / resumeSports

执行顺序要点

  1. 先状态、后实时:DeviceSportsServiceImpl 先完成 readSportsInfo 状态同步,再启动组合的实时同步服务——因为实时同步依赖 SportsInfo.readRealDataInterval(轮询间隔由固件下发)。
  2. 单飞轮询:readTask 的每次执行都独占队列(执行完才调度下一次),保证任意时刻至多一个在途的 readRealTimeSportsData 请求,避免 BLE 命令交错。
  3. UI 驱动生命周期:start/stop 由运动页面生命周期控制;pause/resume 由用户操作控制,两者作用对象不同(一个管轮询,一个管固件运动状态)。

使用示例

示例一:启动运动服务并订阅实时数据

运动页面创建 DeviceSportsServiceImpl,注入 SportsInfo 并设置实时数据监听:

public DeviceSportsServiceImpl(Context context, @NonNull SportsInfo sportsInfo) {
    // Context mContext = context.getApplicationContext();
    this.sportsInfo = sportsInfo;
    mHealthOp = mWatchManager.getHealthOp();
    syncRealDataService = new DeviceSyncRealDataServiceImpl(context);
}

@Override
public void setRealDataListener(RealDataListener<DeviceRealData> realDataListener) {
    syncRealDataService.setRealDataListener(realDataListener);
}

Source: DeviceSportsServiceImpl.java

要点:setRealDataListener 被委托给组合的 syncRealDataService,对外表现为一个统一的运动服务门面(Facade),UI 无需感知实时同步子服务的存在。

示例二:实时数据回调的转换与推送

@Override
public void onSuccess(RealTimeSportsData result) {
    realDataListener.onRealDataChange(new DeviceRealData(result));
    handler.removeCallbacks(readTask);
    handler.postDelayed(readTask, sportsInfo.readRealDataInterval);
}

Source: DeviceSyncRealDataServiceImpl.java

要点:SDK 类型 RealTimeSportsData 在进入 App 领域层之前被包装为 DeviceRealData;随后用 removeCallbacks + postDelayed 原子地重排下一次轮询。

配置选项

运动数据同步的配置主要来自固件下发(而非本地静态配置),由 SportsInfo 承载:

配置项来源类型默认值说明
readRealDataInterval固件 readSportsInfo 应答int固件决定实时数据轮询间隔(毫秒),App 严格遵循
heartRateMode固件 readSportsInfo 应答int固件决定运动心率模式
type / status固件 readSportsInfo 应答int固件决定运动类型(室外/室内)与运动状态
id / startTime固件 readSportsInfo 应答int / String固件决定运动记录 ID,ID 可解析为开始时间
useMapApp 推导booleanfalsetype == SPORTS_TYPE_OUTDOOR 时启用 GPS 地图
HealthApplication.sportMode运动服务更新intSPORT_MODE_IDLE(0)全局运动模式镜像,初始为不在运动
编译期开关类型默认值说明
BuildConfig.DEBUGbooleandebug 构建为 true状态不一致时弹出 toast 提示(inconsistent_motion_state)

API Reference

DeviceSportsServiceImpl

extends AbstractSportsServerImpl<DeviceRealData> implements SportsService —— 固件运动控制服务。

方法说明
DeviceSportsServiceImpl(Context context, SportsInfo sportsInfo)构造:注入运动信息模型,组合创建 DeviceSyncRealDataServiceImpl
void start()注册监听 + 拉取固件运动状态(readSportsInfo)填充 SportsInfo
void pause()下发 pauseSports RCSP 命令暂停固件运动
void resume()下发 resumeSports RCSP 命令恢复运动(源码第 96-100 行起)
void setRealDataListener(RealDataListener<DeviceRealData>)委托给内部 syncRealDataService 设置实时数据监听

DeviceSyncRealDataServiceImpl

extends AbstractSportsServerImpl<DeviceRealData> implements SportsService —— 设备实时数据同步服务。

方法说明
DeviceSyncRealDataServiceImpl(Context context)构造:持有 context 用于注册屏幕广播
void setSportInfo(SportsInfo sportsInfo)注入运动信息(提供轮询间隔)
void start()启动轮询(post(readTask))并注册屏幕亮灭广播
void stop()取消轮询回调并注销广播
void setRealDataListener(RealDataListener<DeviceRealData>)设置实时数据回调监听

HealthApplication 全局运动模式

方法/常量说明
SPORT_MODE_IDLE = 0不在运动模式(默认)
SPORT_MODE_OUTDOOR = SportsInfo.TYPE_OUTDOOR室外运动模式
SPORT_MODE_INDOOR = SportsInfo.TYPE_INDOOR室内运动模式
int getSportMode()读取当前全局运动模式
void setSportMode(int sportMode)更新全局运动模式

底层 SDK 操作(jl_rcsp HealthOpImpl)

方法说明错误处理
readSportsInfo(device, OnOperationCallback<SportsInfo>)拉取运动状态信息onFailed 记录日志,不重试
readRealTimeSportsData(device, OnOperationCallback<RealTimeSportsData>)拉取实时运动数据ERR_RESPONSE_BAD_RESULT 时按间隔重试
pauseSports(device, OnOperationCallback<Boolean>)暂停运动onFailed 记录日志
resumeSports(device, OnOperationCallback<Boolean>)恢复运动onFailed 记录日志

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

失败模式与容错

失败场景源码行为影响评估
readSportsInfo 失败onFailed 仅记录 JL_Log.w,不重试、不弹窗状态同步失败时运动页仍可进入,但 SportsInfo 保持初始值,实时同步可能使用无效间隔——属于静默降级
readRealTimeSportsData 返回 ERR_RESPONSE_BAD_RESULT按 readRealDataInterval 间隔重新调度 readTask瞬时坏结果(如固件忙)被平滑跳过,不影响轮询连续性
readRealTimeSportsData 其他错误记录日志后不重试避免在设备断开等不可恢复场景下空转;恢复依赖页面重新 start()
运动状态与本地不一致(DEBUG 构建)ToastUtil.showToastLong(inconsistent_motion_state)仅 debug 构建提示,release 静默以固件为准
暂停/恢复命令失败记录 JL_Log.w不中断 UI 流程,固件状态以实际执行为准

并发与调度约束

  • 单线程串行化:readTask 运行在 HandlerManager 提供的主线程 Handler 上,所有 BLE 请求回调与 UI 回调同线程,天然规避了数据竞争;代价是轮询回调与 UI 绘制共享主线程,单次 readRealTimeSportsData 往返期间主线程被占用(回调内仅做模型包装与 postDelayed,无阻塞操作)。
  • 队列单实例保证:onSuccess 内先 removeCallbacks(readTask) 再 postDelayed,配合 start() 开头的 removeCallbacks,保证任意时刻队列中至多一个 readTask——这是防止 BLE 请求叠加的核心机制。
  • 幂等的 start/stop:重复 start() 不会堆积任务(先清理再投递);重复 stop() 因 isRegister 标志不会重复注销广播,避免 IllegalArgumentException。
  • 跨线程状态共享:HealthApplication.sportMode 为主线程读写(由主线程上的运动服务回调更新、UI 主线程读取),无需额外同步。

性能与运维考量

  • 功耗控制:轮询节奏完全由固件下发的 readRealDataInterval 决定,App 不自行加速,避免高频 BLE 请求导致设备端与手机端双端耗电;屏幕亮灭广播为灭屏降频预留了扩展位(ScreenBroadcastReceiver)。
  • 失败退避:坏结果按正常间隔重试而非立即重试(源码注释中被弃用的 postDelayed(readTask, 0) 印证了设计取舍),防止失败风暴打爆 BLE 队列。
  • 日志成本:轮询路径每周期都有 JL_Log 输出,长时间运动会产生大量日志;线上排查可关注 DeviceSyncRealDataServiceImpl 标签下的 onFailed 频率,作为链路健康度的信号。
  • 单飞(single-flight)语义:由于轮询是"执行完再调度",天然避免请求积压——即使 BLE 往返慢于间隔,也不会出现排队,实际刷新率只会低于名义间隔而不会堆叠。

扩展点

  • SportsService 接口 + AbstractSportsServerImpl 抽象基类:新运动模式(如骑行、游泳)可继承基类实现接口,复用统一的 start/pause/resume/stop 生命周期与实时数据通道。仓库中已有 DeviceOutdoorRunningServiceImpl 与 DeviceIndoorRunningServiceImpl 两个具体实现,验证了该扩展路径。
  • RealDataListener<T> 监听器:UI 通过 setRealDataListener 订阅实时数据,可自由替换消费端(心率卡片、配速、地图轨迹),而无需改动同步服务。
  • IRequestRecordHandler 接口:运动记录/轨迹请求可插拔,DeviceRequestRecordHandler 是设备侧实现;未来可增加云端记录同步实现而不影响协议层。
  • SportsNotifySender 系列:DeviceOutdoorRunningNotifySender / DeviceIndoorRunningNotifySender 负责按运动类型发送通知,与同步服务解耦。

测试覆盖

仓库测试目录包含 ReceiveHealthDataCmdTest.java(app/src/test),用于验证健康/运动数据命令的接收与解析路径:

}, sport -> {
    System.out.println("sport:" + sport);

Source: ReceiveHealthDataCmdTest.java

该测试以回调方式断言真实数据(real)与运动数据(sport)的解析输出,覆盖 SDK 命令 → App 模型的转换链路,是同步通道数据解析正确性的回归保障。实时轮询的间隔调度、广播注册/注销等时序行为属于设备联调场景,源码中未发现对应单元测试,建议在扩展同步逻辑时补充。

Related Links

  • HealthApplication.java(全局运动模式)
  • DeviceSportsServiceImpl.java(固件运动控制/状态同步)
  • DeviceSyncRealDataServiceImpl.java(实时数据轮询同步)
  • SportsInfo.java(运动信息模型)
  • IRequestRecordHandler.java(记录请求处理接口)
  • DeviceRequestRecordHandler.java(记录请求设备实现)
  • SportsRecordAndLocation.java(记录与轨迹模型)
  • ReceiveHealthDataCmdTest.java(同步解析测试)
  • 相关兄弟页面:室内/室外跑步服务(DeviceIndoorRunningServiceImpl / DeviceOutdoorRunningServiceImpl)、运动地图与轨迹(ui.sports.map)、运动通知(ui.sports.notify)
Prev
健康数据同步
Next
本地数据持久化