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

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

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

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

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

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

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

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

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

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

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

天气同步

天气同步是 JL Health 生态中将实时天气信息从云端(高德地图服务)下发到智能手表设备的完整能力链路,覆盖 HealthAide 应用中的自动同步任务(WeatherSyncTask)、JL RCSP SDK 的设备指令通道(PushInfoDataToDeviceCmd.Weather / syncWeatherInfo),以及 WatchTestTool 测试工具中的重复压测实现(SyncWeatherTask)。

Purpose and Scope

本页面向"天气同步"这一完整业务能力,说明它如何在设备同步框架(SyncTaskManager/DeviceSyncTask)中被调度,如何完成"定位城市 → 获取实时天气 → 字符串到字节编码映射 → 下发设备 → 失败重试"的端到端流程,并给出 SDK 层的直接调用方式与测试工具中的压测实现。

本页不覆盖以下内容(属于兄弟页面范畴):

  • 设备连接、OTA 状态管理与 BLE 传输层细节(见"设备管理"相关页面)
  • 通用的同步任务调度框架 SyncTaskManager 的全部任务类型(本页只说明天气任务如何接入)
  • 测试工具中天气数据的本地数据库持久化(WeatherDao/WeatherEntity)与 UI 操作细节(SyncWeatherFragment/AddWeatherDialog),仅提及它们的存在

Overview

智能手表屏幕空间有限,无法直接展示天气 App 的完整数据,因此需要 App 侧从天气服务商获取结构化数据,映射为手表固件约定的紧凑字节编码后通过蓝牙指令下发。WeatherSyncTask 是整个流程的编排者:

  1. 由 SyncTaskManager 在设备同步流程中按任务类型调度启动;
  2. 通过高德地图定位 SDK 获取当前城市(单次高精度定位,失败时回退到 IP 定位,再失败则使用默认城市"北京市");
  3. 通过高德地图天气服务(WeatherSearch,实时天气类型)获取省份、城市、天气现象、温度、湿度、风向、风力、报告时间;
  4. 将中文天气现象/风向字符串映射为手表固件约定的字节编码(天气编码 0~38、风向编码 0~9),构造 PushInfoDataToDeviceCmd.Weather;
  5. 调用 WatchManager.syncWeatherInfo() 经 JL RCSP SDK 将数据下发至手表;
  6. 根据结果回调决定下一次同步的调度间隔:成功 15 分钟、失败 2 分钟,体现了"失败快速重试、成功低频刷新"的节流设计。

天气编码表的匹配逻辑支持高德返回的复合天气描述(如"小雨-中雨"),通过包含 / 的列表项做子串匹配,是保证编码正确性的关键细节。

Architecture

flowchart TD
    subgraph sg_App["HealthAide 应用层"]
        SyncTaskManager["SyncTaskManager"]
        WeatherSyncTask["WeatherSyncTask"]
        WatchManager["WatchManager"]
    end

    subgraph sg_Service["高德地图服务"]
        AMapLoc["AMapLocationClient 单次定位"]
        AMapIP["restapi.amap.com IP 定位"]
        AMapWeather["WeatherSearch 实时天气"]
    end

    subgraph sg_SDK["JL RCSP SDK / 设备"]
        PushCmd["PushInfoDataToDeviceCmd.Weather"]
        WatchOp["WatchOpImpl.syncWeatherInfo"]
        Device["手表设备"]
    end

    SyncTaskManager -->|"调度 TASK_TYPE_SYNC_DEVICE_WEATHER"| WeatherSyncTask
    WeatherSyncTask -->|"checkEnv 通过后"| AMapLoc
    AMapLoc -->|"定位失败(权限错误)"| AMapIP
    AMapLoc -->|"定位成功返回城市"| AMapWeather
    AMapIP -->|"失败兜底 北京市"| AMapWeather
    AMapWeather -->|"LocalWeatherLive 数据"| WeatherSyncTask
    WeatherSyncTask -->|"字符串转字节编码"| PushCmd
    PushCmd -->|"构造 Weather 对象"| WatchOp
    WatchOp -->|"BLE 指令下发"| Device
    WeatherSyncTask -->|"onFinish 结果回调"| SyncTaskManager

架构要点说明:

  • WeatherSyncTask(源码)继承 DeviceSyncTask,属于同步任务框架的一员,通过 getType() 返回 TASK_TYPE_SYNC_DEVICE_WEATHER 标识自身类型,由 SyncTaskManager 统一调度。
  • 高德定位与天气服务是外部依赖:定位负责拿到城市名,天气服务负责拿到实时天气字段;两者都通过回调异步返回结果。
  • PushInfoDataToDeviceCmd.Weather 是 App 与设备之间的数据载体,包含 8 个字段(省份、城市、天气编码、温度、湿度、风力、风向编码、时间戳),是 SDK 层 syncWeatherInfo 的入参。
  • WatchManager(WatchOpImpl 子类) 是 App 侧对 JL RCSP SDK 的封装入口,syncWeatherInfo() 将 Weather 对象打包为蓝牙指令发送给手表。
  • 测试工具中的 SyncWeatherTask(com.jieli.watchtesttool)走同一条 SDK 指令通道,但由 UI 触发、可配置测试次数与随机天气列表,用于验证设备端表现。

主流程实现详解

1. 同步框架接入与启动

WeatherSyncTask 继承自 DeviceSyncTask,构造函数接收 SyncTaskManager 与 SyncTaskFinishListener,这样任务完成后既能通过 finishListener 通知框架,又能调用 syncTaskManager 安排下一次调度:

public class WeatherSyncTask extends DeviceSyncTask {
    private final String tag = getClass().getSimpleName();
    private final SyncTaskManager syncTaskManager;

    public WeatherSyncTask(SyncTaskManager manager, SyncTaskFinishListener finishListener) {
        super(finishListener);
        this.syncTaskManager = manager;
    }

    @Override
    public int getType() {
        return TASK_TYPE_SYNC_DEVICE_WEATHER;
    }

Source: WeatherSyncTask.java

设计意图:将"天气同步"实现为同步框架中的一个可插拔任务,框架无需关心任务内部细节,只需按类型调度并接收完成回调;同时任务自身持有 SyncTaskManager 引用,以便在结束时注册下一次执行(即自调度重试)。

start() 是任务入口,先做两个前置检查:

@Override
public void start() {
    if (mWatchManager.isFirmwareOTA()) {
        JL_Log.w(tag, "start", "device's ota is in progress.");
        if (finishListener != null) finishListener.onFinish();
        return;
    }
    if (checkEnv()) {
        refreshCity(HealthApplication.getAppViewModel().getApplication());
    } else {
        onFinish(false);
    }
}

private boolean checkEnv() {
    if (!mWatchManager.isConnected()) return false;
    NetWorkStateModel netWorkStateModel = NetworkStateHelper.getInstance().getNetWorkStateModel();
    //网络状态
    return netWorkStateModel != null && netWorkStateModel.isAvailable();
}

Source: WeatherSyncTask.java

两个检查的含义:

  • OTA 进行中直接跳过:固件升级期间蓝牙信道被占用,任何业务指令都不应下发,因此直接回调 onFinish() 结束本轮,且不注册重试(避免干扰升级流程)。
  • checkEnv() 双条件:手表必须已连接(isConnected()),且系统网络可用(NetworkStateHelper 的 NetWorkStateModel.isAvailable())。天气数据来自云端,无网络时获取必然失败,提前失败可以省掉无谓的定位与 HTTP 请求。环境不满足时走 onFinish(false),触发 2 分钟后的快速重试。

2. 城市定位:单次高精度定位 + 权限回退

refreshCity() 使用高德定位 SDK,配置为"单次定位 + 最新一次定位结果",超时 20 秒,定位模式为高精度(GPS + 网络):

private void refreshCity(Context context) {
    AMapLocationClient locationClient = null;
    try {
        locationClient = new AMapLocationClient(context);
        locationClient.setLocationListener(mAMapLocationListener);
        AMapLocationClientOption option = new AMapLocationClientOption();
        option.setOnceLocation(true);
        option.setOnceLocationLatest(true);
        option.setHttpTimeOut(20000);
        option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
        locationClient.setLocationOption(option);
        //设置场景模式后最好调用一次stop,再调用start以保证场景模式生效
        locationClient.stopLocation();
        locationClient.startLocation();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Source: WeatherSyncTask.java

定位结果在 mAMapLocationListener 中处理,这是整个链路中第一个分支点:

private final AMapLocationListener mAMapLocationListener = aMapLocation -> {
    if (aMapLocation != null) {
        if (aMapLocation.getErrorCode() == 0) {
            refreshWeather(aMapLocation.getCity());
        } else {
            if (aMapLocation.getErrorCode() == AMapLocation.ERROR_CODE_FAILURE_LOCATION_PERMISSION) {
                refreshCityByIP();
            } else {
                onFinish(false);
            }
            JL_Log.d(tag, "onLocationChanged", "location Error, ErrCode:"
                    + aMapLocation.getErrorCode() + ", errInfo:"
                    + aMapLocation.getErrorInfo());
        }
    }
};

Source: WeatherSyncTask.java

分支策略的设计意图:定位成功(errorCode == 0)直接使用 getCity();只有权限失败才尝试 IP 定位兜底——因为 IP 定位精度低,仅作为"拿不到定位权限"时的最后手段;其余定位错误(超时、无信号等)直接判定同步失败,交给 2 分钟重试机制处理,避免在失败路径上叠加更多网络请求。

IP 定位兜底使用 OkHttp 异步请求高德 IP 定位接口,解析失败或网络失败时硬编码回退到"北京市":

private void refreshCityByIP() {
    String url = "http://restapi.amap.com/v3/ip?key=2824c5044e6cd020c3af5e5d923cb883";
    OkHttpClient okHttpClient = new OkHttpClient();
    final Request request = new Request.Builder()
            .url(url)
            .get()
            .build();
    Call call = okHttpClient.newCall(request);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NotNull Call call, @NotNull IOException e) {
            JL_Log.w(tag, "refreshCityByIP", "onFailure: " + e);
            refreshWeather("北京市");
        }

        @Override
        public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
            if (!response.isSuccessful() || null == response.body()) return;
            String res = response.body().string();
            String city = null;
            try {
                Gson gson = new GsonBuilder().create();
                City cityObj = gson.fromJson(res, City.class);
                if (cityObj != null) {
                    city = cityObj.getCity();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            refreshWeather(city != null ? city : "北京市");
        }
    });
}

Source: WeatherSyncTask.java

City 是内嵌的 Gson 模型类,对应高德 IP 接口 JSON 中的 status/info/infocode/province/city/adcode/rectangle 字段。注意 onResponse 中若 HTTP 非成功或响应体为空会直接 return 而不继续,此时任务既不会成功也不会触发 onFinish——这是源码中一个实际存在的行为边界(见"失败模式"一节)。

3. 实时天气获取:AMap WeatherSearch

拿到城市名后,refreshWeather() 构造高德天气查询(WEATHER_TYPE_LIVE 实时天气类型),异步搜索:

private void refreshWeather(String city) {
    try {
        WeatherSearchQuery query = new WeatherSearchQuery(city, WeatherSearchQuery.WEATHER_TYPE_LIVE);
        WeatherSearch search = new WeatherSearch(HealthApplication.getAppViewModel().getApplication());
        search.setQuery(query);
        search.setOnWeatherSearchListener(new WeatherSearch.OnWeatherSearchListener() {
            @Override
            public void onWeatherLiveSearched(LocalWeatherLiveResult localWeatherLiveResult, int code) {
                if (code != 1000) {
                    JL_Log.w(tag, "refreshWeather", "获取天气失败");
                    onFinish(false);
                    return;
                }
                if (localWeatherLiveResult == null || localWeatherLiveResult.getLiveResult() == null) {
                    JL_Log.w(tag, "refreshWeather", "没有天气数据");
                    onFinish(false);
                    return;
                }
                LocalWeatherLive result = localWeatherLiveResult.getLiveResult();
                String sb = "实时天气数据:" + "\n" +
                        "省份:" + result.getProvince() + "\t\t" +
                        "城市:" + result.getCity() + "\t\t" +
                        "城市编码:" + result.getAdCode() + "\n" +
                        "天气:" + result.getWeather() + "\t\t" +
                        "温度:" + result.getTemperature() + "\t\t" +
                        "湿度:" + result.getHumidity() + "\n" +
                        "方向:" + result.getWindDirection() + "\t\t" +
                        "风力:" + result.getWindPower() + "\n" +
                        "时间:" + result.getReportTime() + "\n\n";
                JL_Log.d(tag, "refreshWeather", sb);
                syncWeatherToDevice(result);
            }

            @Override
            public void onWeatherForecastSearched(LocalWeatherForecastResult localWeatherForecastResult, int code) {
                onFinish(false);
            }
        });
        search.searchWeatherAsyn();
    } catch (AMapException e) {
        e.printStackTrace();
    }
}

Source: WeatherSyncTask.java

关键校验点:

  • 高德搜索回调的 code 必须为 1000(成功码),否则直接失败;
  • 结果对象或 getLiveResult() 为空视为失败("没有天气数据");
  • 只有实时天气监听(onWeatherLiveSearched)被使用;预报监听(onWeatherForecastSearched)直接回调失败——本任务只同步实时天气,不涉及多日预报;
  • 获取到的原始字段先以 JL_Log.d 打印便于排查,再进入编码映射阶段。

4. 数据编码:字符串 → 字节映射

设备固件无法直接理解中文天气描述,syncWeatherToDevice() 负责把高德的字符串字段转换为 byte 编码,并解析时间戳:

private void syncWeatherToDevice(LocalWeatherLive result) {
    byte weatherCode = getWeatherCode(result.getWeather());
    byte windDirectionCode = getWindDirectionCode(result.getWindDirection());
    byte temperature = Byte.parseByte(result.getTemperature());
    byte humidity = Byte.parseByte(result.getHumidity());
    byte windPower = (result.getWindPower().equals("≤3")) ? 3 : Byte.parseByte(result.getWindPower());
    @SuppressLint("SimpleDateFormat")
    SimpleDateFormat format = CustomTimeFormatUtil.dateFormat("yyyy-MM-dd hh:mm:ss");
    long time = 0;
    try {
        Date date = format.parse(result.getReportTime());
        if (date != null) {
            time = date.getTime();
        }
    } catch (ParseException e) {
        e.printStackTrace();
    }

    PushInfoDataToDeviceCmd.Weather weather = new PushInfoDataToDeviceCmd.Weather(
            result.getProvince(), result.getCity(),
            weatherCode, temperature,
            humidity, windPower, windDirectionCode,
            time
    );
    mWatchManager.syncWeatherInfo(weather, new OnWatchOpCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) {
            onFinish(true);
        }

        @Override
        public void onFailed(BaseError error) {
            JL_Log.w(tag, "syncWeatherToDevice", "天气同步失败, " + error);
            onFinish(false);
        }
    });
}

Source: WeatherSyncTask.java

编码细节与设计意图:

  • 天气编码:getWeatherCode() 维护一张 38 项的映射表(0~37),默认值 38 表示"未知/未匹配";匹配规则是 weather.equals(tmp) || tmp.contains("/" + weather) || tmp.contains(weather + "/"),即支持高德返回的复合描述(如"小雨-中雨"在列表项"雨/小雨/毛毛雨/细雨/小雨-中雨"中通过 contains("/" + weather) 命中编码 14)。这样用字符串包含关系代替精确匹配,容忍了高德不同时期返回措辞的差异。
  • 风向编码:getWindDirectionCode() 维护 10 项映射(无风向 0、东 1、南 2、西 3、北 4、东南 5、东北 6、西北 7、西南 8、旋转不定 9),使用精确匹配,未命中默认 0。
  • 温度/湿度:直接 Byte.parseByte() 解析为单字节,若高德返回异常字符串会抛 NumberFormatException(见"失败模式")。
  • 风力"≤3"特判:高德在微风场景可能返回"≤3"这样的非数字字符串,代码将其映射为 3,避免解析崩溃。
  • 报告时间:按 "yyyy-MM-dd hh:mm:ss" 格式解析为毫秒时间戳;解析失败时 time 保持 0,设备端按无效时间处理。

5. 设备下发与结果收口

mWatchManager.syncWeatherInfo() 是 SDK 指令入口,OnWatchOpCallback<Boolean> 的 onSuccess/onFailed 分别对应设备应答成功与失败。无论成功失败,最终都汇聚到统一的 onFinish() 收口函数,该函数负责决定下一次调度的间隔:

private void onFinish(boolean success) {
    int min = 1000 * 60;
    syncTaskManager.addWeatherTask(success ? 15 * min : 2 * min);//同步成功,间隔15分钟,同步失败,间隔2分钟
    finishListener.onFinish();
}

Source: WeatherSyncTask.java

这是整个任务的节流核心:成功时 15 分钟后再次同步,失败时仅 2 分钟后重试。天气变化相对缓慢,15 分钟刷新足够;而失败路径缩短到 2 分钟,可以在网络/定位恢复后尽快补上数据,同时避免失败时无限快速重试拖垮电量与流量。

6. 测试工具中的实现:SyncWeatherTask

WatchTestTool 中另有一个面向测试场景的 SyncWeatherTask(com.jieli.watchtesttool.tool.test.message),它不依赖定位与高德服务,而是直接由 UI 传入(或随机挑选)PushInfoDataToDeviceCmd.Weather 对象,对 SDK 指令通道做重复压测:

private void startTestSyncWeather() {
    if (!isTestRunning) return;
    PushInfoDataToDeviceCmd.Weather weather = getRandomWeather();
    if (null == weather) {
        callbackError(TestError.ERR_INVALID_PARAM);
        return;
    }
    onTestLog("开始同步天气:" + weather + ",\n 测试次数: " + (count + 1));
    mWatchOp.syncWeatherInfo(weather, new OnWatchOpCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) {
            count++;
            if (count >= mTestCount) {
                callbackError(TestError.ERR_SUCCESS);
            } else {
                mUIHandler.postDelayed(() -> startTestSyncWeather(), DELAY_TIME);
            }
        }

        @Override
        public void onFailed(BaseError error) {
            String text = String.format(Locale.getDefault(), "code:%d, %s", error.getCode(), error.getMessage());
            callbackError(TestError.ERR_FAILED, text);
        }
    });
}

Source: SyncWeatherTask.java

与 App 侧实现的关键差异:

  • 继承 AbstractTestTask,使用 startTest()/stopTest()/callbackError() 测试框架协议;开始前校验 mWatchOp.isWatchSystemOk(),失败返回 ERR_DEVICE_NOT_CONNECT。
  • 支持传入天气列表并按配置次数循环发送,两次发送之间通过主线程 Handler 延时 2000ms(DELAY_TIME)。
  • getRandomWeather() 保证连续两次不选同一项(lastIndex 去重),避免重复测试同一编码。
  • 停止时若正在运行,回 ERR_SUCCESS 终止循环;参数为空回 ERR_INVALID_PARAM;设备应答失败回 ERR_FAILED 并携带 BaseError 的 code/message。
  • 配套的 UI/数据层(SyncWeatherFragment、SyncWeatherViewModel、WeatherAdapter、AddWeatherDialog、WeatherDao/WeatherEntity、WeatherUtil)负责用户录入天气数据与本地持久化,测试列表从数据库读取后交给该任务执行。

核心流程

以下时序图展示一次完整的天气同步从调度到收口的过程,包含定位失败时的回退分支:

sequenceDiagram
    participant SM as SyncTaskManager
    participant WST as WeatherSyncTask
    participant LOC as AMapLocationClient
    participant WS as WeatherSearch
    participant WM as WatchManager
    participant DEV as 手表设备

    SM->>WST: start() 调度天气同步任务
    WST->>WST: 检查 OTA 进行中? isFirmwareOTA()
    WST->>WST: checkEnv(): isConnected + 网络可用
    WST->>LOC: refreshCity(): 单次高精度定位
    LOC-->>WST: onLocationChanged(城市)
    alt 定位权限失败 (errorCode == 12)
        WST->>WST: refreshCityByIP() OkHttp 查询
        WST-->>WST: 解析失败/网络失败 -> "北京市"
    else 其他定位错误
        WST->>WST: onFinish(false) -> 2 分钟后重试
    end
    WST->>WS: refreshWeather(城市) 实时天气查询
    WS-->>WST: onWeatherLiveSearched(code==1000)
    WST->>WST: 天气/风向/风力字符串 -> byte 编码
    WST->>WM: syncWeatherInfo(Weather 对象)
    WM->>DEV: 打包 PushInfoDataToDeviceCmd 蓝牙下发
    DEV-->>WM: 设备应答
    WM-->>WST: onSuccess / onFailed(BaseError)
    alt 成功
        WST->>SM: addWeatherTask(15 分钟) + onFinish
    else 失败
        WST->>SM: addWeatherTask(2 分钟) + onFinish
    end

流程关键节点回顾:

  1. 入口与前置门禁:start() 先排除 OTA 场景,再通过 checkEnv() 双条件门禁(连接 + 网络),任一不满足立即以失败收口。
  2. 定位分支:定位成功 → 天气查询;权限失败 → IP 兜底(再失败 → 默认"北京市");其他错误 → 直接失败。
  3. 天气数据门禁:搜索 code 必须为 1000,结果非空,才进入编码阶段。
  4. 编码与下发:字符串映射为 4 个字节编码 + 时间戳,构造 PushInfoDataToDeviceCmd.Weather 后走 WatchManager.syncWeatherInfo()。
  5. 节流收口:onFinish(boolean) 统一处理,成功 15 分钟 / 失败 2 分钟注册下一次 addWeatherTask,并通知框架本轮结束。

整个流程中除 refreshWeather 的构造外几乎全部异步(定位回调、OkHttp 回调、天气搜索回调、设备指令回调),任务不阻塞任何线程;状态流转靠回调串联,因此不存在显式状态机,而是"回调链 + 收口函数"的编排风格。

天气编码表

getWeatherCode() 内置的映射表是 App 与手表固件约定的协议,共 38 个编码(0~37),未命中返回 38:

编码天气现象(高德返回可能值)
0晴
1少云
2晴间多云
3多云
4阴
5有风/和风/清风/微风
6平静
7大风/强风/劲风/疾风
8飓风/狂爆风
9热带风暴/风暴
10霾/中度霾/重度霾/严重霾
11阵雨
12雷阵雨
13雷阵雨并伴有冰雹
14雨/小雨/毛毛雨/细雨/小雨-中雨
15中雨/中雨-大雨
16大雨/大雨-暴雨
17暴雨/暴雨-大暴雨
18大暴雨/大暴雨-特大暴雨
19特大暴雨
20强阵雨
21强雷阵雨
22极端降雨
23雨夹雪/阵雨夹雪/冻雨/雨雪天气
24雪
25阵雪
26小雪/小雪-中雪
27中雪/中雪-大雪
28大雪/大雪-暴雪
29暴雪
30浮尘
31扬沙
32沙尘暴
33强沙尘暴
34龙卷风
35雾/轻雾/浓雾/强浓雾/特强浓雾
36热
37冷
38默认(未匹配)

匹配算法为包含式匹配:命中条件 weather.equals(tmp) || tmp.contains("/" + weather) || tmp.contains(weather + "/"),因此高德返回"小雨-中雨"时,第 14 项"雨/小雨/毛毛雨/细雨/小雨-中雨"中 tmp.contains(weather + "/") 不成立但 tmp.contains("/" + weather) 成立,正确命中 14。

Source: WeatherSyncTask.java

使用示例

示例 1:手动构造天气数据并同步(SDK 层直接调用)

绕过自动任务,直接调用 WatchManager.syncWeatherInfo() 是接入天气同步的最简方式,适合需要自定义天气来源(如本地数据库、测试数据)的场景:

@Test
public void syncWeatherInformation() {
    //WatchManager是WatchOpImpl的子类,须在1.3配置好sdk
    WatchManager watchManager = WatchManager.getInstance();
    String province = "省份";
    String city = "城市";
    byte weatherCode = 0;//天气编码,参考天气编码表
    byte temperature = 0;//温度
    byte humidity = 0; //湿度
    byte windPower = 0;//风力等级
    byte windDirectionCode = 0; //方向编码,参考方向编码表
    long time = System.currentTimeMillis(); //时间
    //构建天气信息
    PushInfoDataToDeviceCmd.Weather weather = new PushInfoDataToDeviceCmd.Weather(province, city,
            weatherCode, temperature,
            humidity, windPower, windDirectionCode,
            time
    );
    //执行同步天气信息功能并等待结果回调
    watchManager.syncWeatherInfo(weather, new OnWatchOpCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) {
            //天气同步成功
        }

        @Override
        public void onFailed(BaseError error) {
            //天气同步失败
            //error - 错误信息
        }
    });
}

Source: WeatherSyncDemo.java

该示例同时是单元测试(@Test),揭示了两个关键契约:Weather 构造参数的顺序(省份、城市、天气编码、温度、湿度、风力、风向编码、时间戳),以及回调中 Boolean result 成功恒为 true、失败携带 BaseError(含 getCode() 与 getMessage())。

示例 2:测试工具中的循环压测(多组天气随机发送)

WatchTestTool 支持传入一组天气列表并循环测试,连续两次不重复:

public SyncWeatherTask(WatchOpImpl watchOp, int testCount, PushInfoDataToDeviceCmd.Weather... weatherList) {
    mWatchOp = watchOp;
    if (testCount < 1) testCount = 1;
    mTestCount = testCount;
    mWeatherList = weatherList;
}

@Nullable
private PushInfoDataToDeviceCmd.Weather getRandomWeather() {
    if (null == mWeatherList || mWeatherList.length == 0) return null;
    if (mWeatherList.length == 1) return mWeatherList[0];
    int random = getRandom(mWeatherList.length);
    while (random == lastIndex) {
        random = getRandom(mWeatherList.length);
    }
    lastIndex = random;
    return mWeatherList[random];
}

Source: SyncWeatherTask.java

lastIndex 去重逻辑保证两次相邻测试使用不同天气编码,从而在压测中覆盖更多编码组合;testCount < 1 时被钳制为 1,避免死循环。

配置选项

天气同步的配置以内置常量与硬编码为主,未使用独立配置文件:

配置项位置值说明
同步成功调度间隔WeatherSyncTask.onFinish()15 分钟(15 * 1000 * 60 ms)成功后通过 syncTaskManager.addWeatherTask() 注册下一次
同步失败调度间隔WeatherSyncTask.onFinish()2 分钟(2 * 1000 * 60 ms)失败后快速重试
定位 HTTP 超时refreshCity()20000 msAMapLocationClientOption.setHttpTimeOut
定位模式refreshCity()Hight_Accuracy高精度定位(GPS+网络)
定位次数refreshCity()setOnceLocation(true) + setOnceLocationLatest(true)单次定位并取最新结果
天气查询类型refreshWeather()WEATHER_TYPE_LIVE仅实时天气,不使用预报
天气搜索成功码refreshWeather()code == 1000高德天气搜索成功判定
IP 定位接口refreshCityByIP()http://restapi.amap.com/v3/ip?key=...定位权限失败时兜底;key 硬编码
默认回退城市refreshCityByIP()"北京市"IP 定位失败时的最终兜底
压测发送间隔SyncWeatherTask(测试工具)DELAY_TIME = 2000 ms两次压测发送之间的延时
压测次数下限SyncWeatherTask(测试工具)testCount 最小值 1小于 1 时钳制为 1

Source: WeatherSyncTask.java

API 参考

WeatherSyncTask(SyncTaskManager manager, SyncTaskFinishListener finishListener)

构造自动天气同步任务。

参数:

  • manager(SyncTaskManager):同步任务管理器,用于在收口时注册下一次调度;
  • finishListener(SyncTaskFinishListener):任务完成监听,收口时被调用。

int getType()

返回任务类型标识,恒为 TASK_TYPE_SYNC_DEVICE_WEATHER,供框架识别与分发。

void start()

任务入口。OTA 进行中则直接回调 onFinish() 结束;checkEnv() 通过则开始定位,否则以失败收口。

boolean checkEnv()

环境检查:手表已连接且系统网络可用,返回 true。

返回: true 表示可执行同步;false 表示环境不满足。

PushInfoDataToDeviceCmd.Weather 构造器

Weather(String province, String city,
        byte weatherCode, byte temperature,
        byte humidity, byte windPower, byte windDirectionCode,
        long time)

设备天气数据载体,字段与固件协议一一对应。

参数:

  • province/city(String):省份与城市名;
  • weatherCode(byte):天气编码(0~38,见编码表);
  • temperature(byte):温度;
  • humidity(byte):湿度;
  • windPower(byte):风力等级;
  • windDirectionCode(byte):风向编码(0~9);
  • time(long):天气报告时间戳(毫秒),由 "yyyy-MM-dd hh:mm:ss" 解析而来。

WatchManager.syncWeatherInfo(PushInfoDataToDeviceCmd.Weather weather, OnWatchOpCallback<Boolean> callback)

下发天气信息到手表(WatchOpImpl 的封装方法)。

参数:

  • weather:待下发的天气数据;
  • callback:操作回调。

回调:

  • onSuccess(Boolean result):设备应答成功,result 为 true;
  • onFailed(BaseError error):失败,error 携带 getCode() 与 getMessage()。

测试工具:SyncWeatherTask(WatchOpImpl watchOp, int testCount, Weather... weatherList)

构造压测任务。

参数:

  • watchOp(WatchOpImpl):SDK 操作实例,startTest() 前会校验 isWatchSystemOk();
  • testCount:测试总次数,小于 1 时钳制为 1;
  • weatherList:天气数据列表,为空时 startTest() 直接报 ERR_INVALID_PARAM。

错误码(TestError): ERR_DEVICE_NOT_CONNECT(设备未就绪)、ERR_INVALID_PARAM(天气列表为空)、ERR_FAILED(设备应答失败)、ERR_SUCCESS(全部完成或手动停止)。

失败模式与边界情况

失败/边界场景处理方式源码位置
固件 OTA 进行中start() 直接回调 onFinish() 结束本轮,不注册重试WeatherSyncTask.start()
手表未连接 / 网络不可用checkEnv() 返回 false,onFinish(false) 触发 2 分钟重试WeatherSyncTask.checkEnv()
定位权限被拒绝回退 refreshCityByIP();IP 查询也失败则默认"北京市"mAMapLocationListener
定位其他错误(超时、无信号等)直接 onFinish(false),不叠加网络请求mAMapLocationListener
天气搜索 code != 1000onFinish(false),2 分钟后重试onWeatherLiveSearched
天气结果或 getLiveResult() 为空onFinish(false)onWeatherLiveSearched
高德返回未知天气现象getWeatherCode() 返回默认编码 38(未匹配)getWeatherCode()
高德返回未知风向getWindDirectionCode() 返回默认 0(无风向)getWindDirectionCode()
温度/湿度非数字字符串Byte.parseByte() 抛 NumberFormatException,任务未捕获,可能导致回调链中断(源码边界)syncWeatherToDevice()
风力返回"≤3"特判映射为 3,避免解析异常syncWeatherToDevice()
报告时间解析失败time 保持 0,设备端按无效时间处理syncWeatherToDevice()
IP 兜底 HTTP 非成功或响应体为空onResponse 直接 return,不调用 onFinish,任务既无成功也无失败回调(源码边界)refreshCityByIP()
设备应答失败onFailed(BaseError) 记录日志后 onFinish(false)syncWeatherInfo 回调
测试工具:设备未就绪startTest() 返回 ERR_DEVICE_NOT_CONNECTSyncWeatherTask.startTest()
测试工具:天气列表为空返回 ERR_INVALID_PARAMstartTestSyncWeather()

需要特别指出的两个源码中实际存在的边界(实现者应在扩展时留意):

  1. IP 兜底的静默失败:onResponse 中 !response.isSuccessful() || null == response.body() 时直接 return,此时既不 refreshWeather 也不 onFinish,任务停留在"无结果"状态,直到 SyncTaskManager 的其他机制介入。
  2. 数值解析异常未捕获:Byte.parseByte(result.getTemperature()) 等调用不在 try-catch 内,高德异常返回可能导致 NumberFormatException 沿 OkHttp/搜索回调线程抛出。

并发与一致性

  • 整个流程均为异步回调驱动,无共享可变状态,不存在并发写冲突;WeatherSyncTask 实例每次同步只产生一个定位客户端、一次天气搜索、一条设备指令。
  • 重试调度通过 SyncTaskManager.addWeatherTask() 的定时机制实现,同一时刻至多一个待执行的天气任务,天然串行,不会出现多个同步并发下发。
  • 测试工具中的 SyncWeatherTask 使用主线程 Handler.postDelayed 串联压测,count 与 lastIndex 仅在主线程回调中读写,无并发问题;stopTest() 通过 isTestRunning 标志终止后续轮次。

性能与运维注意

  • 网络请求开销:每次同步最多产生 2 次 HTTP 类请求(定位 + 天气搜索,权限失败时再加 1 次 IP 查询)。成功间隔 15 分钟、失败间隔 2 分钟的节流策略控制了流量与电量开销。
  • OkHttp 客户端按次创建:refreshCityByIP() 每次 new 一个 OkHttpClient,未复用连接池;仅出现在定位权限失败的兜底路径,频次低,影响有限。
  • 日志可观测性:关键节点均有 JL_Log 输出(定位错误码/errInfo、IP 响应、实时天气全字段、设备同步失败原因),线上问题可按 tag WeatherSyncTask 检索。
  • 密钥硬编码:高德 IP 定位 key 直接写在源码中,属于公开客户端 key 的常见做法,但升级/轮换 key 时需要同步修改代码。
  • 时间格式大小写:"yyyy-MM-dd hh:mm:ss" 中 hh 为 12 小时制,若高德返回 24 小时制时间(如 13:00),SimpleDateFormat 会按 12 小时制解析导致时间戳偏差——这是潜在精度隐患。

扩展点

  • 天气编码表可扩展:getWeatherCode() 的 weathers 列表即协议表,新增天气现象只需追加列表项并分配编码;匹配逻辑自动支持带 / 的复合描述。
  • 同步框架插拔:WeatherSyncTask 实现 DeviceSyncTask 契约(getType() + start()),可被 SyncTaskManager 与其他任务任意组合调度;新增同步类型可参照其结构与 TASK_TYPE_* 常量扩展。
  • 自定义天气来源:SDK 层 syncWeatherInfo(Weather, callback) 接受任意构造的 PushInfoDataToDeviceCmd.Weather,不依赖高德服务,可替换为自研天气源(测试工具即为此模式)。
  • 压测参数化:SyncWeatherTask 的 (watchOp, testCount, weather...) 构造器支持传入任意天气列表与次数,可扩展为从数据库/文件批量导入。

测试覆盖

  • WeatherSyncDemo(app/src/test):以 JUnit 测试形式演示 WatchManager.syncWeatherInfo() 的最小可用路径,验证 Weather 构造与回调契约(成功 onSuccess(true)、失败 onFailed(BaseError))。见 WeatherSyncDemo.java。
  • SyncWeatherTask(WatchTestTool):提供可重复执行的设备侧压测,覆盖多编码随机下发、连续去重、失败上报(ERR_DEVICE_NOT_CONNECT/ERR_INVALID_PARAM/ERR_FAILED)、手动停止等场景。见 SyncWeatherTask.java。
  • 测试工具配套的 WeatherDao/WeatherEntity 提供本地数据持久化,SyncWeatherFragment/SyncWeatherViewModel/WeatherAdapter/AddWeatherDialog 提供用户录入与列表展示,WeatherUtil 提供天气数据工具方法,共同构成完整的测试闭环(本页不展开其细节)。

Related Links

  • 天气同步任务实现 WeatherSyncTask.java
  • 同步演示 WeatherSyncDemo.java
  • 测试工具压测任务 SyncWeatherTask.java
  • 测试工具天气数据库 WeatherDao.java
  • 测试工具天气实体 WeatherEntity.java
  • 设备同步框架(SyncTaskManager/DeviceSyncTask):参见"设备管理"目录下同步框架相关页面
  • 设备连接与 BLE 通道:参见"设备管理"目录下连接管理相关页面
Prev
消息与联系人同步
Next
设备查找