接入系统 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

@@ -34,6 +34,7 @@ fenix8v3-watchface/
│ ├── Fields.mc # 125 项数据字段的取值与格式化
│ ├── Solar.mc # 日出/晨昏蒙影/黄金蓝调时刻的太阳高度角计算
│ ├── Owm.mc # OpenWeatherMap 缓存读取
│ ├── Comp.mc # 系统 / 第三方 Complication 桥接
│ ├── Fenix8V3Background.mc # 后台服务:拉取 OWM
│ ├── Settings.mc # 所有设置项的读取与默认值
│ ├── FieldTable.mc # ⚠️ 自动生成:字段 id 表
@@ -181,21 +182,48 @@ python3 tools/gen_themes.py && python3 tools/gen_preview_svg.py
### 数据字段
四个数据位共用一张 **125 项** 的字段表,编号沿用 [watchface.io/docs/datafields](https://watchface.io/docs/datafields),两边可以直接对照。底部两格的小标签跟随所选字段自动变化,不需要单独配置。
四个数据位共用一张 **156** 的字段表,编号沿用 [watchface.io/docs/datafields](https://watchface.io/docs/datafields),两边可以直接对照。底部两格的小标签跟随所选字段自动变化,不需要单独配置。
| 分类 | 字段 |
|------|------|
| 日期时间 | 秒前导零两种、时间、UTC 时间、第二时区 1/2、星期月日、星期日、月日、日、星期简写/全称)、月份(名称/数字、上午下午、周数ISO / 日历) |
| 活动 | 步数、步数取整、剩余步数、卡路里、活动卡路里、活动分钟(总/中强度/高强度/本周)、距离、上楼层数、下楼层数、爬升高度、推行距离、推行次数、**本周与近 7 天累计**(步数/卡路里/距离/楼层/活动分钟) |
| 活动 | 步数、步数取整、剩余步数、卡路里、活动卡路里、活动分钟(总/中强度/高强度/本周)、距离、上楼层数、下楼层数、爬升高度、推行距离、推行次数、**本周 / 近 7 天 / 近 28 天累计**(步数/卡路里/距离/楼层/活动分钟) |
| 身体 | 心率、静息心率、压力指数、血氧、呼吸频率、身体电量、活动条(正/反向、体重、BMI、恢复时间、最大摄氧量跑/骑) |
| 系统 | 电量、剩余天数、电量+天数、闹钟、通知、勿扰、蓝牙、太阳能强度 |
| 环境 | 海拔、海平面气压、环境气压、日出、日落、下一太阳事件、距太阳事件、月龄、月球照明、民用/航海/天文晨昏蒙影(各 2 项)、黄金时刻(晨/昏)、蓝调时刻(晨/昏) |
| 天气 | 气温、最高/最低、体感、最高、最低、湿度、降水概率、风速、风向角、风向、更新时间、海平面气压、紫外线、能见度、露点 |
| OpenWeatherMap | 气温、体感、最高/最低、天气描述(简/详、湿度、气压、能见度、风速、风向角、风向、阵风、1h 降雨、1h 降雪、云量、降水概率、露点、明日与后日气温及高低温、城市名、更新时间(共 25 项) |
| 自定义 | 自定义文字 1 / 2 / 3 |
| OpenWeatherMap | 气温、体感、最高/最低、天气描述(简/详、湿度、气压、能见度、风速、风向角、风向、阵风、1h 降雨、1h 降雪、云量、降水概率、露点、明日与后日气温及高低温、城市名、更新时间、**空气质量AQI 等级 + 描述 + PM2.5 + PM10 + CO**(共 30 项) |
| 系统 Complication | 下一个日程、训练状态、完赛预测5k/10k/半马/全马)、配速预测(同上四项)、最近高尔夫得分、本周跑步距离、本周骑行距离、第三方 App 15 |
| 自定义 | 自定义文字 1 / 2 / 3、自定义时间格式、日期格式 1 / 2、事件倒计时 |
加字段:在 `tools/gen_fields.py``FIELDS` 表里加一行,跑一遍生成器,再到 `source/Fields.mc``value()` 里补一个分支。设置项、两种语言的文案、表盘标签会自动跟上。
### 系统 Complication
`Toybox.Complications`CIQ 4.2+17 款设备全都有)让表盘可以订阅系统和**其它 App 发布**的复杂功能。这条路解锁了一批原本以为做不到的字段:
- **日历事件** —— CIQ 确实没有日历 API但系统把「下一个日程」作为 complication 发布了出来
- **训练状态、完赛/配速预测、最近高尔夫得分** —— `ActivityMonitor.Info``UserProfile` 上都没有,这里有
- **本周跑步 / 骑行距离** —— 这是**按运动分类**的,`ActivityMonitor` 的按天历史做不出来
- **第三方 App 15** —— CGM、补水、行情这类 App 发布的复杂功能,按序号取
需要 `ComplicationSubscriber` 权限。第三方 App 那五个槽位按「有值且有短标签」过滤后按序号取,避免序号随手表上装了什么 App 而漂移。
### 自定义格式与倒计时
时间/日期格式串用单字母占位符(设置项能填的长度有限,这样中英文都好写):
| 占位符 | 含义 | 占位符 | 含义 |
|--------|------|--------|------|
| `h` / `H` | 小时(跟随 12/24 制)/ 补零 | `D` | 日 |
| `m` / `s` | 分钟 / 秒(补零) | `M` / `N` | 月(数字 / 名称) |
| `a` | 上午/下午24 小时制下为空) | `Y` / `y` | 四位年 / 两位年 |
| | | `w` | 星期 |
其它字符原样输出,所以 `D/M``10/9``w N D``周四 9月 10`
事件倒计时填 `YYYY-MM-DD`,显示还剩几天。
### 秒针与后台天气
**秒**850/869通过 `onPartialUpdate` 每秒重绘:只有当某个数据位真的选了秒时才请求这个回调,并且只在该格的裁剪区内重绘,不动整个表盘。常亮的 AMOLED 机型不参与——那时数据位本来就不显示。
@@ -208,14 +236,14 @@ python3 tools/gen_themes.py && python3 tools/gen_preview_svg.py
| 没做的 | 原因 |
|--------|------|
| 运动项目的 WTD / 近 7 天 / 本月 / 近 28 天 汇总(80 项) | Connect IQ 只有 `ActivityMonitor.getHistory()` 按天总计(步数/卡路里/距离/楼层/活动分钟),**没有**按运动分类的历史。所以通用的本周/近 7 天累计已经做了,按跑步/骑行/游泳分开的做不了——那些在 GreenBlack 那边是手机侧与 Garmin Connect 集成同步来的 |
| 运动细分的多数汇总(游泳、自定义运动 1/2 的距离/时长/次数,60 项) | `ActivityMonitor.getHistory()` 只有按天总计,没有按运动分类的历史。**跑步和骑行的本周距离例外**——系统把这两项作为 complication 发布了,已经做了;游泳和自定义运动没有对应的 complication 类型 |
| 日历事件、计时器 | 日历已通过 Complication 做了。**计时器**仍然做不了:表盘拿不到触摸输入,没法启停 |
| StormGlass.io 潮汐5 项) | 需要额外的付费 API key且与本表盘的定位无关 |
| 第三方 App 复杂功能CGM、Hydration、QuoteGlance 等 12 项) | 需要用户装了对应的 App并通过 Complications API 订阅 |
| 日历事件、计时器、倒计时 | 日历不对 CIQ 开放;表盘拿不到触摸输入,没法做启停 |
| 睡眠得分、训练状态、完赛/配速预测 | `ActivityMonitor.Info``UserProfile` 上没有这些字段SDK 9.1.0 实测) |
| 睡眠得分 | `ActivityMonitor.Info` 上没有,也没有对应的 complication 类型SDK 9.1.0 实测)。训练状态与完赛/配速预测已改由 Complication 提供 |
| 高尔夫成绩 | 无对应 API |
| 标记类字段Marker约 35 项) | 那是给刻度型表盘做环形标记用的,本表盘的进度环用的是自己的指标体系 |
| OWM 的 AQI 与 UVI | 免费档的 `/data/2.5/weather` 不返回;要 One Call 3.0 订阅。露点用 Magnus 公式从温湿度算出 |
| OWM 的 UVI | 要 One Call 3.0 订阅。**AQI 已经做了**——空气质量走 `/data/2.5/air_pollution`,那个接口在免费档就开放。露点用 Magnus 公式从温湿度算出 |
---
@@ -238,7 +266,7 @@ python3 tools/gen_themes.py && python3 tools/gen_preview_svg.py
### 权限
`manifest.xml` 声明项:`UserProfile`静息心率、体重、VO2max`SensorHistory`(血氧、身体电量)、`Communications``Background`OpenWeatherMap。用户安装时会看到授权提示。
`manifest.xml` 声明项:`UserProfile`静息心率、体重、VO2max`SensorHistory`(血氧、身体电量)、`Communications``Background`OpenWeatherMap`ComplicationSubscriber`(日历、训练状态、第三方 App。用户安装时会看到授权提示。
> `Sensor` 权限**不能**用于 `type="watchface"`(除非同时申请 `Background`),编译器会直接拒绝。所以海拔与气压走 `Activity.Info` 而不是 `Sensor.getInfo()`。