Update watch face: modular refactor + OWM/Solar/background + codegen tools
- Split render into FieldTable/Fields/Icons/Layout/Settings modules - Add Fenix8V3Background, Owm (weather), Solar modules - Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg) - Tune README/manifest/strings/settings
This commit is contained in:
167
source/Fenix8V3Background.mc
Normal file
167
source/Fenix8V3Background.mc
Normal file
@@ -0,0 +1,167 @@
|
||||
import Toybox.Lang;
|
||||
import Toybox.Application;
|
||||
import Toybox.Background;
|
||||
import Toybox.Communications;
|
||||
import Toybox.System;
|
||||
import Toybox.Time;
|
||||
import Toybox.Position;
|
||||
|
||||
// ============================================================================
|
||||
// OpenWeatherMap —— 后台拉取端
|
||||
// ----------------------------------------------------------------------------
|
||||
// 表盘进程绝大多数时间没在运行,onUpdate 又必须够快,不可能在绘制路径里发
|
||||
// HTTP 请求。Connect IQ 的方案是**后台服务**:系统按定时器唤醒一个独立进程,
|
||||
// 在里面发请求,把结果通过 Background.exit() 交回前台,前台再写进 Storage。
|
||||
//
|
||||
// 每次唤醒发两个请求:
|
||||
// 1. 当前天气 /data/2.5/weather
|
||||
// 2. 短期预报 /data/2.5/forecast?cnt=16(约 48 小时,够算明日/后日高低温)
|
||||
//
|
||||
// ⚠️ 预报接口必须带 cnt 限制条数。后台进程的内存预算非常小(约 32KB),
|
||||
// 完整的 5 天 / 3 小时预报解析出来能有几十 KB,会直接爆掉。
|
||||
//
|
||||
// 响应会被**压平成一个扁平字典**再交回去,键名是短名("temp"/"hi1"…),
|
||||
// 与 Owm.get() 的取值一一对应 —— 这样表盘侧永远不用扛一棵 JSON 树。
|
||||
// ============================================================================
|
||||
(:background)
|
||||
class Fenix8V3Background extends System.ServiceDelegate {
|
||||
|
||||
private var mResult as Dictionary = {};
|
||||
private var mPending = 0;
|
||||
|
||||
function initialize() {
|
||||
ServiceDelegate.initialize();
|
||||
}
|
||||
|
||||
// 系统按定时器唤醒后台进程时调用。
|
||||
// 注意这里是**独立进程**,内存预算比前台小得多,而且不能碰 UI。
|
||||
function onTemporalEvent() as Void {
|
||||
if (!Owm.enabled()) {
|
||||
Background.exit(null);
|
||||
return;
|
||||
}
|
||||
var lat = Application.Storage.getValue("lastLat");
|
||||
var lon = Application.Storage.getValue("lastLon");
|
||||
if (lat == null || lon == null) {
|
||||
// 表盘一拿到位置就会存进 Storage(见 Fields.Ctx.where)。
|
||||
// 在那之前没有坐标可问,直接退出。
|
||||
Background.exit(null);
|
||||
return;
|
||||
}
|
||||
mResult = {};
|
||||
mPending = 2;
|
||||
// 一律用 units=metric 请求,单位换算统一放到 Fields 里按用户设置做,
|
||||
// 这样切换单位不用重新联网。
|
||||
var common = {"lat" => lat, "lon" => lon,
|
||||
"appid" => Owm.key(), "units" => "metric"};
|
||||
var opts = {
|
||||
:method => Communications.HTTP_REQUEST_METHOD_GET,
|
||||
:responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
|
||||
};
|
||||
Communications.makeWebRequest(
|
||||
"https://api.openweathermap.org/data/2.5/weather",
|
||||
common, opts, method(:onCurrent));
|
||||
var fc = {"lat" => lat, "lon" => lon, "appid" => Owm.key(),
|
||||
"units" => "metric", "cnt" => 16};
|
||||
Communications.makeWebRequest(
|
||||
"https://api.openweathermap.org/data/2.5/forecast",
|
||||
fc, opts, method(:onForecast));
|
||||
}
|
||||
|
||||
// 当前天气的回调。OWM 的 JSON 是嵌套的,逐层取值时每层都要判空 ——
|
||||
// 缺字段是常态(比如没下雨就没有 rain 节点)。
|
||||
function onCurrent(code as Number, body as Dictionary?) as Void {
|
||||
if (code == 200 && body != null) {
|
||||
var main = body.get("main") as Dictionary?;
|
||||
if (main != null) {
|
||||
put("temp", main.get("temp"));
|
||||
put("feels", main.get("feels_like"));
|
||||
put("tmin", main.get("temp_min"));
|
||||
put("tmax", main.get("temp_max"));
|
||||
put("pressure", main.get("pressure"));
|
||||
put("humidity", main.get("humidity"));
|
||||
}
|
||||
put("visibility", body.get("visibility"));
|
||||
var wind = body.get("wind") as Dictionary?;
|
||||
if (wind != null) {
|
||||
put("wind", wind.get("speed"));
|
||||
put("winddeg", wind.get("deg"));
|
||||
put("gust", wind.get("gust"));
|
||||
}
|
||||
var clouds = body.get("clouds") as Dictionary?;
|
||||
if (clouds != null) { put("clouds", clouds.get("all")); }
|
||||
var rain = body.get("rain") as Dictionary?;
|
||||
if (rain != null) { put("rain", rain.get("1h")); }
|
||||
var snow = body.get("snow") as Dictionary?;
|
||||
if (snow != null) { put("snow", snow.get("1h")); }
|
||||
var wx = body.get("weather") as Array?;
|
||||
if (wx != null && wx.size() > 0) {
|
||||
var w0 = wx[0] as Dictionary;
|
||||
put("short", w0.get("main"));
|
||||
put("long", w0.get("description"));
|
||||
}
|
||||
put("city", body.get("name"));
|
||||
put("dt", Time.now().value());
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
// 预报返回的是 3 小时粒度的列表。这里按「距今天几天」把条目分到
|
||||
// 明天 / 后天两个桶里,各自取最高最低温;降水概率取明天的最大值。
|
||||
function onForecast(code as Number, body as Dictionary?) as Void {
|
||||
if (code == 200 && body != null) {
|
||||
var list = body.get("list") as Array?;
|
||||
if (list != null) {
|
||||
var today = dayNumber(Time.now().value());
|
||||
var lo1 = null; var hi1 = null; var lo2 = null; var hi2 = null;
|
||||
var pop1 = null;
|
||||
var arr = list;
|
||||
for (var i = 0; i < arr.size(); i++) {
|
||||
var e = arr[i] as Dictionary;
|
||||
var dt = e.get("dt");
|
||||
if (dt == null) { continue; }
|
||||
var day = dayNumber(dt as Number) - today;
|
||||
var m = e.get("main") as Dictionary?;
|
||||
if (m == null) { continue; }
|
||||
var tmin = m.get("temp_min");
|
||||
var tmax = m.get("temp_max");
|
||||
if (day == 1) {
|
||||
if (lo1 == null || tmin < lo1) { lo1 = tmin; }
|
||||
if (hi1 == null || tmax > hi1) { hi1 = tmax; }
|
||||
var pop = e.get("pop");
|
||||
if (pop != null && (pop1 == null || pop > pop1)) { pop1 = pop; }
|
||||
} else if (day == 2) {
|
||||
if (lo2 == null || tmin < lo2) { lo2 = tmin; }
|
||||
if (hi2 == null || tmax > hi2) { hi2 = tmax; }
|
||||
}
|
||||
}
|
||||
put("lo1", lo1); put("hi1", hi1);
|
||||
put("lo2", lo2); put("hi2", hi2);
|
||||
put("pop", pop1);
|
||||
}
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
||||
// 距 Unix 纪元的整天数,用来给预报条目分桶。
|
||||
function dayNumber(unix as Number) as Number {
|
||||
return unix / 86400;
|
||||
}
|
||||
|
||||
// 只写入非空值,免得用 null 覆盖字典里已有的内容。
|
||||
function put(k as String, v) as Void {
|
||||
if (v != null) { mResult.put(k, v); }
|
||||
}
|
||||
|
||||
// 两个请求都回来了才算一轮完整数据,谁最后完成谁负责退出服务。
|
||||
// 一条都没拿到就传 null,避免用空字典覆盖掉上一次的好数据。
|
||||
function done() as Void {
|
||||
mPending--;
|
||||
if (mPending > 0) { return; }
|
||||
if (mResult.size() == 0) {
|
||||
Background.exit(null);
|
||||
} else {
|
||||
Background.exit(mResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user