设备查找
设备查找(Find Device)是 JL Health App 与智能手表之间的双向防丢查找能力:App 可发起命令让手表响铃以便定位设备,手表也可主动推送命令让手机响铃。该能力基于 RCSP 协议中的 SearchDevCmd(Command.CMD_SEARCH_DEVICE)实现,由 WatchManager 负责收发、RingHandler 负责铃声播放、HomeViewModel 负责业务编排与 UI 状态更新。
Purpose and Scope
本页完整讲解「设备查找」这一子系统能力的端到端实现,涵盖:
- 双向查找机制:App 查找设备(App → 手表)与设备查找手机(手表 → App)两条命令路径;
- 命令的构建、发送、响应与回调:
CommandBuilder.buildSearchDevCmd、WatchManager.sendRcspCommand、RcspCommandCallback; - 设备侧命令处理:
OnWatchCallback.onRcspCommand中的CMD_SEARCH_DEVICE分发、功能支持校验(isSupportSearchDevice)与应答; - 铃声播放与 UI 状态联动:
RingHandler.playAlarmRing / stopAlarmRing与 LiveData 状态发布; - 错误码与边界处理:
StateCode、RcspErrorCode相关的失败路径。
以下内容不属于本页边界,请参见对应目录页:蓝牙连接/断连管理与 WatchManager 完整生命周期(设备管理)、RCSP 协议栈其他命令(如设备信息、健康数据同步)、RingHandler 铃声资源管理(通知与闹钟)。
Overview
设备查找的本质是双向防丢:当用户找不到手表时,通过 App 触发手表响铃,循声定位;反之,当用户在手表上触发「查找手机」时,手表向 App 推送命令,手机播放铃声提醒用户手机位置。
从协议视角看,两条路径共用同一个 RCSP 命令 CMD_SEARCH_DEVICE,只是命令方向和播放端不同:
| 场景 | 命令方向 | 播放端 | 触发方式 |
|---|---|---|---|
| 查找设备 | App → 手表 | 手表扬声器 | CommandBuilder.buildSearchDevCmd(SEARCH_TYPE_DEVICE, timeout, ringWay, RING_PLAYER_DEVICE) |
| 查找手机 | 手表 → App | 手机铃声 | 手表按键,设备推送 SearchDevCmd 到 App |
| 停止查找 | App → 手表 | — | CommandBuilder.buildSearchDevCmd(RING_OP_CLOSE, 0) |
设计上,命令参数 op 区分打开(RING_OP_OPEN)与关闭(RING_OP_CLOSE)响铃;timeout 为超时秒数;ringWay 指定响铃方式(如全部响铃 RING_WAY_ALL);player 指定由哪一端播放(设备播放 RING_PLAYER_DEVICE)。这种参数化的命令模型让同一命令承载「开/关」「查找/停止」多种语义,协议扩展性好,App 端只需一套解析逻辑。
Architecture
flowchart TD
subgraph sg_Phone["手机侧 App"]
UI["HomeViewModel (UI 状态)"]
WM["WatchManager"]
CB["CommandBuilder"]
RH["RingHandler"]
UI --> WM
WM --> CB
end
subgraph sg_Protocol["RCSP 协议层 (jl_rcsp SDK)"]
CMD["SearchDevCmd (CMD_SEARCH_DEVICE)"]
PARAM["SearchDevParam / SearchDevResponse"]
end
subgraph sg_Watch["手表侧"]
DEV["手表固件 / 扬声器"]
end
CB -->|"buildSearchDevCmd"| CMD
WM -->|"sendRcspCommand 发送"| CMD
CMD -->|"BLE 传输"| DEV
DEV -->|"推送命令"| WM
WM -->|"onRcspCommand 回调"| RH
RH -->|"playAlarmRing / stopAlarmRing"| UI
架构要点:
WatchManager(WatchOpImpl子类,单例)是 RCSP 命令收发的门面:App 侧主动查找通过sendRcspCommand下发,设备侧推送的命令通过注册的OnWatchCallback.onRcspCommand回调进入 App。主应用中该回调由HomeViewModel中的WatchManagerCallback匿名类实现(HomeViewModel.java)。CommandBuilder负责将高层语义(查找类型、超时、响铃方式、播放端)组装为协议命令SearchDevCmd,屏蔽底层参数序列化细节。RingHandler(单例)统一管理铃声播放/停止,是「查找手机」路径的最终执行者;App 侧还会通过playAlarmRing(param.getType(), param.getTimeoutSec() * 1000L)将协议超时秒数转换为毫秒传给铃声播放器。- 功能开关:主应用在响铃前会检查
WatchConfigure.getFunctionOption().isSupportSearchDevice(),只有设备固件声明支持查找功能时才实际播放铃声,避免对低配设备做无效操作。
实现详解
双向命令流:谁发起、谁播放
设备查找子系统有两条方向相反但协议相同的命令路径:
flowchart LR
subgraph sg_A["路径 A:查找设备"]
A1["App 发起 buildSearchDevCmd"] --> A2["手表响铃"]
A2 --> A3["响应 result=0"]
end
subgraph sg_B["路径 B:查找手机"]
B1["手表推送 SearchDevCmd"] --> B2["App 校验 isSupportSearchDevice"]
B2 --> B3["RingHandler.playAlarmRing"]
B3 --> B4["回复 STATUS_SUCCESS"]
end
- 路径 A(查找设备):
CommandBuilder.buildSearchDevCmd构造命令 →WatchManager.sendRcspCommand发送 → 设备端响铃并返回SearchDevResponse,result == 0表示成功,App 在回调中据此提示用户「设备正在响铃」。 - 路径 B(查找手机):手表按键后向 App 推送
SearchDevCmd→ App 在onRcspCommand中解析SearchDevParam,若op == RING_OP_OPEN则播放闹铃并发布mRingPlayStatusMLD = true,否则停止响铃并发布false→ 无论是否实际播放,都会以STATUS_SUCCESS+SearchDevResultParam(0)应答设备。
命令构建与参数语义
CommandBuilder.buildSearchDevCmd 有两个重载:
buildSearchDevCmd(int op, int timeout):用于停止查找(RING_OP_CLOSE,timeout 传 0);buildSearchDevCmd(int op, int timeout, int ringWay, int player):用于启动查找,四个参数分别控制操作类型、超时秒数、响铃方式、播放端。
Demo 中启动查找的典型调用(SearchPhoneDemo.java):
//构建查找设备命令
//op --- 查找设备
//timeout --- 超时时间
//ringWay --- 全部响铃
//player --- 设备播放
CommandBase searchDevCmd = CommandBuilder.buildSearchDevCmd(RcspConstant.SEARCH_TYPE_DEVICE, 60,
RcspConstant.RING_WAY_ALL, RcspConstant.RING_PLAYER_DEVICE);
Source: SearchPhoneDemo.java
设计意图:将「响铃方式」「播放端」作为独立参数,使 SDK 无需为每种组合新增命令 ID;App 侧只需在 UI 上提供开关/时长选项即可组合出丰富行为。
发送命令与回调处理
命令通过 WatchManager.sendRcspCommand(device, command, callback) 发送,回调 RcspCommandCallback<SearchDevCmd> 提供 onCommandResponse 与 onErrCode 两个入口。onCommandResponse 中按顺序校验:
- 状态校验:
cmd.getStatus() != STATUS_SUCCESS时区分STATUS_UNKOWN_CMD(映射为ERR_FUNC_NOT_SUPPORT)与其他错误状态(构造ERR_RESPONSE_BAD_STATUS错误); - 响应非空校验:
cmd.getResponse() == null视为解析失败(ERR_PARSE_DATA); - 结果码校验:
response.getResult() != 0时构造ERR_RESPONSE_BAD_RESULT错误,result == 0才表示设备已进入/退出响铃状态。
Demo 中的完整校验链(SearchPhoneDemo.java):
watchManager.sendRcspCommand(watchManager.getConnectedDevice(), searchDevCmd, new RcspCommandCallback<SearchDevCmd>() {
@Override
public void onCommandResponse(BluetoothDevice device, SearchDevCmd cmd) {
final int status = cmd.getStatus();
if (status != StateCode.STATUS_SUCCESS) {
if (status == StateCode.STATUS_UNKOWN_CMD) {
onErrCode(device, new BaseError(RcspErrorCode.ERR_FUNC_NOT_SUPPORT)
.setOpCode(cmd.getId()).setSn(cmd.getOpCodeSn()));
return;
}
onErrCode(device, RcspErrorCode.buildJsonError(cmd.getId(), cmd.getOpCodeSn(), RcspErrorCode.ERR_RESPONSE_BAD_STATUS,
status, StateCode.printResponseStatus(status)));
return;
}
SearchDevResponse response = cmd.getResponse();
if (null == response) {
onErrCode(device, new BaseError(RcspErrorCode.ERR_PARSE_DATA, RcspErrorCode.getErrorDesc(RcspErrorCode.ERR_PARSE_DATA)));
return;
}
if (response.getResult() == 0) {
//请求成功, 说明设备在响铃
} else {
onErrCode(device, RcspErrorCode.buildJsonError(cmd.getId(), cmd.getOpCodeSn(), RcspErrorCode.ERR_RESPONSE_BAD_RESULT,
response.getResult(), RcspUtil.formatInt(response.getResult())));
}
}
@Override
public void onErrCode(BluetoothDevice device, BaseError error) {
//处理错误事件
}
});
Source: SearchPhoneDemo.java
设备侧命令处理与功能支持校验
主应用中,HomeViewModel 内部的 WatchManagerCallback 注册了 OnWatchCallback,在 onRcspCommand 中拦截 CMD_SEARCH_DEVICE。与纯 Demo 不同,生产代码在响铃前做了功能支持校验:读取 WatchConfigure.getFunctionOption().isSupportSearchDevice(),只有固件声明支持时才播放铃声;不支持的设备同样会收到成功应答,避免协议层重试(HomeViewModel.java):
@Override
public void onRcspCommand(BluetoothDevice device, CommandBase command) {
if (command.getId() == Command.CMD_SEARCH_DEVICE) {//查找设备的处理
SearchDevCmd searchDevCmd = (SearchDevCmd) command;
WatchConfigure configure = mWatchManager.getWatchConfigure(device);
boolean isAllowSearch = configure == null || (configure.getFunctionOption() != null
&& configure.getFunctionOption().isSupportSearchDevice());
if (isAllowSearch) {
SearchDevParam param = searchDevCmd.getParam();
if (param != null) {
if (param.getOp() == RcspConstant.RING_OP_OPEN) {
mRingHandler.playAlarmRing(param.getType(), param.getTimeoutSec() * 1000L);
mRingPlayStatusMLD.postValue(true);
} else {
mRingHandler.stopAlarmRing();
mRingPlayStatusMLD.postValue(false);
}
}
}
searchDevCmd.setStatus(StateCode.STATUS_SUCCESS);
searchDevCmd.setParam(new SearchDevParam.SearchDevResultParam(0));
mWatchManager.sendCommandResponse(device, searchDevCmd, null);
}
}
Source: HomeViewModel.java
关键设计点:
isAllowSearch的宽松默认:configure == null时默认为允许,避免配置尚未同步完成时漏掉查找请求;只有在明确拿到配置且FunctionOption明确不支持时才跳过播放。- UI 状态通过 LiveData 发布:
mRingPlayStatusMLD.postValue(true/false)驱动界面上的响铃状态指示,采用postValue而非setValue,因为回调可能来自非主线程(BLE 回调线程),postValue保证线程安全地切回主线程。 - 超时换算:协议单位为秒(
timeoutSec),铃声播放器单位为毫秒,因此乘以1000L。 - 应答与播放解耦:即使参数为 null 或功能不支持,仍以
STATUS_SUCCESS+SearchDevResultParam(0)应答,确保设备侧命令生命周期闭合、不会等待超时。
停止查找
停止查找复用同一命令,op 传 RING_OP_CLOSE、timeout 传 0(SearchPhoneDemo.java):
//构建停止查找设备命令
watchManager.sendRcspCommand(CommandBuilder.buildSearchDevCmd(RcspConstant.RING_OP_CLOSE, 0), new RcspCommandCallback<SearchDevCmd>() {
// ... 与启动查找相同的状态/响应/结果校验,result == 0 表示设备已停止响铃
});
Source: SearchPhoneDemo.java
核心流程
以下序列图展示「查找设备」完整生命周期:App 构建并发送命令 → 设备响铃 → 响应回传 → 结果校验;以及「查找手机」的推送路径。
sequenceDiagram
participant UI as HomeViewModel (UI)
participant WM as WatchManager
participant CB as CommandBuilder
participant DEV as 手表固件
participant RH as RingHandler
Note over UI,DEV: 路径 A:App 查找设备
UI->>CB: buildSearchDevCmd(SEARCH_TYPE_DEVICE, 60, RING_WAY_ALL, RING_PLAYER_DEVICE)
CB-->>WM: SearchDevCmd
UI->>WM: sendRcspCommand(device, cmd, callback)
WM->>DEV: 下发 CMD_SEARCH_DEVICE (BLE)
DEV-->>DEV: 扬声器响铃(最长 60s)
DEV-->>WM: SearchDevResponse(result)
WM-->>UI: onCommandResponse(status, response)
alt status == STATUS_SUCCESS && result == 0
UI-->>UI: 提示「设备正在响铃」
else 状态/响应/结果任一异常
UI-->>UI: onErrCode(ERR_FUNC_NOT_SUPPORT / ERR_RESPONSE_BAD_STATUS / ERR_PARSE_DATA / ERR_RESPONSE_BAD_RESULT)
end
Note over UI,DEV: 路径 B:手表查找手机
DEV->>WM: 推送 SearchDevCmd (op=RING_OP_OPEN)
WM->>UI: onRcspCommand(CMD_SEARCH_DEVICE)
UI->>UI: 校验 isSupportSearchDevice()
UI->>RH: playAlarmRing(type, timeoutSec*1000L)
RH-->>UI: 播放中,mRingPlayStatusMLD=true
UI->>WM: 应答 STATUS_SUCCESS + SearchDevResultParam(0)
WM-->>DEV: sendCommandResponse
流程要点:
- 路径 A 是典型的请求-响应模式,App 通过三层校验(状态 → 响应非空 → 结果码)把协议错误逐步收敛为具体错误类型,方便 UI 给出差异化提示。
- 路径 B 是设备主动推送 + App 被动应答模式,App 必须总是应答(无论是否播放),这是 RCSP 协议命令生命周期闭合的要求;应答参数
SearchDevResultParam(0)中的 0 即「操作成功」。 - 两条路径共用
RingHandler作为最终执行者,保证铃声资源的串行管理,避免「查找设备」与「查找手机」同时播放造成音频冲突。
API Reference
设备查找能力涉及的公开 API 主要来自 jl_rcsp SDK 与 App 侧封装,签名与语义如下(均基于上述源码验证):
CommandBuilder.buildSearchDevCmd(int op, int timeout)
构建「停止查找」命令。
参数:
op(int):操作类型,停止查找传RcspConstant.RING_OP_CLOSE。timeout(int):超时秒数,停止场景传 0。
返回: SearchDevCmd(CommandBase 子类),可直接交给 WatchManager.sendRcspCommand。
CommandBuilder.buildSearchDevCmd(int op, int timeout, int ringWay, int player)
构建「启动查找」命令。
参数:
op(int):操作类型,RcspConstant.RING_OP_OPEN打开响铃;SEARCH_TYPE_DEVICE表示查找设备语义。timeout(int):响铃持续秒数(如 60)。ringWay(int):响铃方式,如RcspConstant.RING_WAY_ALL(全部响铃)。player(int):播放端,如RcspConstant.RING_PLAYER_DEVICE(设备播放)。
返回: SearchDevCmd。
WatchManager.sendRcspCommand(BluetoothDevice device, CommandBase cmd, RcspCommandCallback<T> callback)
发送 RCSP 命令并注册回调。
参数:
device:目标设备,可为getConnectedDevice()。cmd:构建好的命令对象。callback:RcspCommandCallback<SearchDevCmd>,含onCommandResponse(BluetoothDevice, SearchDevCmd)与onErrCode(BluetoothDevice, BaseError)。
返回: void。
RingHandler.playAlarmRing(int type, long timeoutMs) / RingHandler.stopAlarmRing()
播放/停止手机闹铃。
参数:
type(int):铃声类型(取自SearchDevParam.getType())。timeoutMs(long):播放时长(毫秒),由协议秒数换算而来。
返回: void。
SearchDevCmd 关键访问器
getStatus():命令状态码,与StateCode.STATUS_SUCCESS/STATUS_UNKOWN_CMD比较。getResponse():SearchDevResponse,可能为 null。getParam():SearchDevParam,含getOp()/getType()/getTimeoutSec()。setStatus(int)/setParam(SearchDevParam.SearchDevResultParam):设置应答状态与结果。getOpCodeSn()/getId():用于错误上报的错误码构造。
错误码(RcspErrorCode)
| 错误码 | 触发条件 |
|---|---|
ERR_FUNC_NOT_SUPPORT | 设备返回 STATUS_UNKOWN_CMD,即固件不支持该命令 |
ERR_RESPONSE_BAD_STATUS | getStatus() 非成功且非未知命令 |
ERR_PARSE_DATA | 响应为 null,解析失败 |
ERR_RESPONSE_BAD_RESULT | response.getResult() != 0,操作被设备拒绝 |
失败模式、边界情况与并发
- 设备不支持查找:主应用中若
FunctionOption.isSupportSearchDevice()为 false,App 静默跳过播放但仍回复成功。注意 Demo 与主应用行为有差异——Demo 无条件播放,主应用受功能开关约束,集成时须以主应用行为为准。 - 未知命令:老固件可能不认识
CMD_SEARCH_DEVICE,回调中返回STATUS_UNKOWN_CMD,App 将其归一化为ERR_FUNC_NOT_SUPPORT提示用户升级固件。 - 空响应:
getResponse() == null说明回包不完整或解析失败,按ERR_PARSE_DATA处理。 - 非零结果码:设备侧执行失败(如扬声器忙)时
result != 0,App 应提示失败而非静默。 - 参数为 null:路径 B 中
searchDevCmd.getParam()可能为 null,主应用先判空再访问,避免 NPE;参数缺失时仍会应答成功。 - 线程安全:
onRcspCommand回调运行在 BLE 回调线程,主应用使用LiveData.postValue(线程安全)而非setValue(必须在主线程),这是多线程 UI 更新的关键点。 - 铃声冲突:两条路径共用
RingHandler单例,若「查找设备」响铃中又收到「查找手机」请求,playAlarmRing会被新的调用覆盖,须由上层 UI 保证互斥(源码中未发现显式互斥,属已知设计约束)。 - 超时换算:
timeoutSec * 1000L必须用长整型运算,避免 int 溢出(60 秒场景无碍,但超长 timeout 需注意)。
性能与运维考量
- BLE 低带宽:
SearchDevCmd负载极小(几个整型参数),对 BLE 通道开销可忽略;不建议高频重复发送,一次查找请求 + 一次停止请求即可。 - 超时兜底:设备端响铃受
timeout秒数限制,即使 App 停止命令丢失,设备也会自动停止,避免铃声无限播放耗电。 - 音频资源:手机侧
RingHandler管理铃声的启停,若查找手机期间用户接听电话,须由上层在onPause/电话状态回调中调用stopAlarmRing(源码中未覆盖该场景,属扩展点)。
扩展点
- 新增响铃方式/播放端:在
RcspConstant中扩展RING_WAY_*与RING_PLAYER_*常量,CommandBuilder.buildSearchDevCmd参数化设计无需改动命令结构。 - 铃声类型定制:
SearchDevParam.getType()透传给RingHandler.playAlarmRing,可依据 type 选择不同铃声资源。 - 功能开关联动:
FunctionOption.isSupportSearchDevice()是设备能力门控;新产品固件若新增查找能力,只需在配置下发中置位该选项,App 无需改动。 - 状态展示:
mRingPlayStatusMLD已暴露响铃状态,UI 层可基于该 LiveData 增加倒计时、停止按钮等交互。
测试
SearchPhoneDemo.java 位于 app/src/test 目录,以 JUnit 方式演示三类用例:searchPhoneDemo(处理设备推送的查找命令)、searchDeviceDemo(构建并发送查找设备命令)、stopSearch(停止查找)。它们同时充当协议用法契约:任何新集成方都应先复现这三个用例,验证命令构建、回调校验链与应答逻辑符合预期。
Related Links
- SearchPhoneDemo.java(查找设备/查找手机/停止查找 Demo)
- HomeViewModel.java(设备查找命令的 App 侧处理与状态发布)
- 设备管理(WatchManager 连接生命周期)—— 见「设备管理」目录页
- RCSP 协议命令体系(Command/SearchDevCmd 模型)—— 见「RCSP 协议」目录页