接入系统 Complication,补齐倒计时/自定义格式/28天累计/空气质量

字段 125 → 156 项。

Toybox.Complications(source/Comp.mc)
  这条路推翻了我之前几个「表盘做不到」的判断 —— 当时只查了 ActivityMonitor /
  UserProfile / Weather,漏了系统把这些作为 complication 发布出来:
    - 下一个日程(CIQ 确实没有日历 API,但有 CALENDAR_EVENTS complication)
    - 训练状态、完赛预测 ×4、配速预测 ×4、最近高尔夫得分
    - 本周跑步 / 骑行距离(按运动分类的,按天历史做不出来)
    - 第三方 App 1–5(CGM、补水、行情这类 App 发布的)
  需要 ComplicationSubscriber 权限。

其余补齐
  - 近 28 天累计:步数 / 卡路里 / 距离 / 楼层
  - 事件倒计时:设置里填 YYYY-MM-DD,显示还剩几天
  - 自定义时间格式、日期格式 1/2:单字母占位符(h/H/m/s/a/D/M/N/Y/y/w)
  - OWM 空气质量:AQI 等级 + 描述 + PM2.5 + PM10 + CO,走
    /data/2.5/air_pollution —— 那个接口在免费档就开放,不需要 One Call 订阅

运行时踩的坑
  - Complication.unit 不保证是 String(周跑步距离返回的就不是),直接调
    .length() 抛 Symbol Not Found。unit 和 shortLabel 都改成先 instanceof 判型。
  - 浮点 value 直接 toString 印成 "0.000000",统一格式化成一位小数。
  - 第三方 App 槽位每取一次就要走完整个迭代器,四个格子都选 App 时会走四遍。
    改成每帧只枚举一次,由 view 在绘制开始时调 Comp.beginFrame() 失效。

测试
  全字段验证的 harness 本身会触发看门狗(一次 onUpdate 里算 156 个字段),
  改成分批每帧 12 个。实际使用一帧只算 4 个,不受影响。
  156 项全部可渲染,零运行时错误;17 款设备全部编译通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 09:03:21 +08:00
parent 4d96ab0912
commit d9d45d983b
14 changed files with 651 additions and 19 deletions

View File

@@ -13,9 +13,10 @@ import Toybox.Position;
// HTTP 请求。Connect IQ 的方案是**后台服务**:系统按定时器唤醒一个独立进程,
// 在里面发请求,把结果通过 Background.exit() 交回前台,前台再写进 Storage。
//
// 每次唤醒发个请求:
// 每次唤醒发个请求:
// 1. 当前天气 /data/2.5/weather
// 2. 短期预报 /data/2.5/forecast?cnt=16约 48 小时,够算明日/后日高低温)
// 3. 空气质量 /data/2.5/air_pollutionAQI / PM2.5 / PM10 / CO
//
// ⚠️ 预报接口必须带 cnt 限制条数。后台进程的内存预算非常小(约 32KB
// 完整的 5 天 / 3 小时预报解析出来能有几十 KB会直接爆掉。
@@ -49,7 +50,7 @@ class Fenix8V3Background extends System.ServiceDelegate {
return;
}
mResult = {};
mPending = 2;
mPending = 3; // 当前天气 + 短预报 + 空气质量
// 一律用 units=metric 请求,单位换算统一放到 Fields 里按用户设置做,
// 这样切换单位不用重新联网。
var common = {"lat" => lat, "lon" => lon,
@@ -66,6 +67,12 @@ class Fenix8V3Background extends System.ServiceDelegate {
Communications.makeWebRequest(
"https://api.openweathermap.org/data/2.5/forecast",
fc, opts, method(:onForecast));
// 空气质量走独立接口。它在免费档里是开放的One Call 才要订阅),
// 所以 AQI / PM2.5 / PM10 / CO 这几项不用付费也能有。
Communications.makeWebRequest(
"https://api.openweathermap.org/data/2.5/air_pollution",
{"lat" => lat, "lon" => lon, "appid" => Owm.key()},
opts, method(:onAir));
}
// 当前天气的回调。OWM 的 JSON 是嵌套的,逐层取值时每层都要判空 ——
@@ -143,6 +150,25 @@ class Fenix8V3Background extends System.ServiceDelegate {
done();
}
// 空气质量的回调。响应形如 { list: [ { main: {aqi}, components: {...} } ] }。
function onAir(code as Number, body as Dictionary?) as Void {
if (code == 200 && body != null) {
var list = body.get("list") as Array?;
if (list != null && list.size() > 0) {
var e = list[0] as Dictionary;
var main = e.get("main") as Dictionary?;
if (main != null) { put("aqi", main.get("aqi")); }
var comp = e.get("components") as Dictionary?;
if (comp != null) {
put("pm2_5", comp.get("pm2_5"));
put("pm10", comp.get("pm10"));
put("co", comp.get("co"));
}
}
}
done();
}
// 距 Unix 纪元的整天数,用来给预报条目分桶。
function dayNumber(unix as Number) as Number {
return unix / 86400;