运动数据同步
运动数据同步是「宜动健康」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 命令主动读取。因此整个同步机制建立在两条互补的通道上:
- 状态同步(一次性拉取):进入运动页时,
DeviceSportsServiceImpl.start()调用HealthOpImpl.readSportsInfo()从固件拉取当前运动信息——运动类型(室外/室内)、状态、运动 ID、开始时间、实时数据同步间隔、心率模式——填充到SportsInfo模型,并据此更新 UI 与全局运动模式。 - 实时数据同步(周期性轮询):
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 |
HealthOpImpl | jl_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
执行顺序要点
- 先状态、后实时:
DeviceSportsServiceImpl先完成readSportsInfo状态同步,再启动组合的实时同步服务——因为实时同步依赖SportsInfo.readRealDataInterval(轮询间隔由固件下发)。 - 单飞轮询:
readTask的每次执行都独占队列(执行完才调度下一次),保证任意时刻至多一个在途的readRealTimeSportsData请求,避免 BLE 命令交错。 - 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 可解析为开始时间 |
useMap | App 推导 | boolean | false | type == SPORTS_TYPE_OUTDOOR 时启用 GPS 地图 |
HealthApplication.sportMode | 运动服务更新 | int | SPORT_MODE_IDLE(0) | 全局运动模式镜像,初始为不在运动 |
| 编译期开关 | 类型 | 默认值 | 说明 |
|---|---|---|---|
BuildConfig.DEBUG | boolean | debug 构建为 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)