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

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

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

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

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

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

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

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

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

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

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

设备查找

设备查找(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 中按顺序校验:

  1. 状态校验:cmd.getStatus() != STATUS_SUCCESS 时区分 STATUS_UNKOWN_CMD(映射为 ERR_FUNC_NOT_SUPPORT)与其他错误状态(构造 ERR_RESPONSE_BAD_STATUS 错误);
  2. 响应非空校验:cmd.getResponse() == null 视为解析失败(ERR_PARSE_DATA);
  3. 结果码校验: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

流程要点:

  1. 路径 A 是典型的请求-响应模式,App 通过三层校验(状态 → 响应非空 → 结果码)把协议错误逐步收敛为具体错误类型,方便 UI 给出差异化提示。
  2. 路径 B 是设备主动推送 + App 被动应答模式,App 必须总是应答(无论是否播放),这是 RCSP 协议命令生命周期闭合的要求;应答参数 SearchDevResultParam(0) 中的 0 即「操作成功」。
  3. 两条路径共用 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_STATUSgetStatus() 非成功且非未知命令
ERR_PARSE_DATA响应为 null,解析失败
ERR_RESPONSE_BAD_RESULTresponse.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 协议」目录页
Prev
天气同步
Next
支付宝集成