天气同步
天气同步是 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 是整个流程的编排者:
- 由
SyncTaskManager在设备同步流程中按任务类型调度启动; - 通过高德地图定位 SDK 获取当前城市(单次高精度定位,失败时回退到 IP 定位,再失败则使用默认城市"北京市");
- 通过高德地图天气服务(
WeatherSearch,实时天气类型)获取省份、城市、天气现象、温度、湿度、风向、风力、报告时间; - 将中文天气现象/风向字符串映射为手表固件约定的字节编码(天气编码 0~38、风向编码 0~9),构造
PushInfoDataToDeviceCmd.Weather; - 调用
WatchManager.syncWeatherInfo()经 JL RCSP SDK 将数据下发至手表; - 根据结果回调决定下一次同步的调度间隔:成功 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
流程关键节点回顾:
- 入口与前置门禁:
start()先排除 OTA 场景,再通过checkEnv()双条件门禁(连接 + 网络),任一不满足立即以失败收口。 - 定位分支:定位成功 → 天气查询;权限失败 → IP 兜底(再失败 → 默认"北京市");其他错误 → 直接失败。
- 天气数据门禁:搜索 code 必须为 1000,结果非空,才进入编码阶段。
- 编码与下发:字符串映射为 4 个字节编码 + 时间戳,构造
PushInfoDataToDeviceCmd.Weather后走WatchManager.syncWeatherInfo()。 - 节流收口:
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 ms | AMapLocationClientOption.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 != 1000 | onFinish(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_CONNECT | SyncWeatherTask.startTest() |
| 测试工具:天气列表为空 | 返回 ERR_INVALID_PARAM | startTestSyncWeather() |
需要特别指出的两个源码中实际存在的边界(实现者应在扩展时留意):
- IP 兜底的静默失败:
onResponse中!response.isSuccessful() || null == response.body()时直接return,此时既不refreshWeather也不onFinish,任务停留在"无结果"状态,直到SyncTaskManager的其他机制介入。 - 数值解析异常未捕获:
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 响应、实时天气全字段、设备同步失败原因),线上问题可按 tagWeatherSyncTask检索。 - 密钥硬编码:高德 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 通道:参见"设备管理"目录下连接管理相关页面