Files
fenix8v3-watchface/source/Fenix8V3Background.mc
ericwyuan 4d96ab0912 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
2026-09-10 07:10:55 +08:00

168 lines
7.2 KiB
MonkeyC
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}