接入系统 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()`。

View File

@@ -40,6 +40,7 @@
<iq:uses-permission id="UserProfile"/>
<iq:uses-permission id="Communications"/>
<iq:uses-permission id="Background"/>
<iq:uses-permission id="ComplicationSubscriber"/>
<iq:uses-permission id="SensorHistory"/>
</iq:permissions>

View File

@@ -18,6 +18,10 @@
<string id="FldSeconds"></string>
<string id="FldSeconds0">秒 (前导零)</string>
<string id="FldWeekCal">周数</string>
<string id="FldCustomTime">自定义时间格式</string>
<string id="FldDateFmt1">日期格式 1</string>
<string id="FldDateFmt2">日期格式 2</string>
<string id="FldCountdown">事件倒计时</string>
<string id="FldSteps">步数</string>
<string id="FldStepsRound">步数 (取整)</string>
<string id="FldStepsToGo">剩余步数</string>
@@ -42,6 +46,10 @@
<string id="FldFloorsWeek">本周楼层</string>
<string id="FldFloors7d">近 7 天楼层</string>
<string id="FldActiveMin7d">近 7 天活动分钟</string>
<string id="FldSteps28d">近 28 天步数</string>
<string id="FldCal28d">近 28 天卡路里</string>
<string id="FldDist28d">近 28 天距离</string>
<string id="FldFloors28d">近 28 天楼层</string>
<string id="FldHeartRate">心率</string>
<string id="FldRestingHr">静息心率</string>
<string id="FldStress">压力指数</string>
@@ -97,6 +105,24 @@
<string id="FldUvi">紫外线指数</string>
<string id="FldVisibility">能见度</string>
<string id="FldDewPoint">露点</string>
<string id="FldNextCal">下一个日程</string>
<string id="FldTrainStatus">训练状态</string>
<string id="FldRace5k">5k 完赛预测</string>
<string id="FldRace10k">10k 完赛预测</string>
<string id="FldRaceHalf">半马完赛预测</string>
<string id="FldRaceFull">全马完赛预测</string>
<string id="FldPace5k">5k 配速预测</string>
<string id="FldPace10k">10k 配速预测</string>
<string id="FldPaceHalf">半马配速预测</string>
<string id="FldPaceFull">全马配速预测</string>
<string id="FldGolfScore">最近高尔夫得分</string>
<string id="FldRunWeek">本周跑步距离</string>
<string id="FldBikeWeek">本周骑行距离</string>
<string id="FldApp1">第三方 App 1</string>
<string id="FldApp2">第三方 App 2</string>
<string id="FldApp3">第三方 App 3</string>
<string id="FldApp4">第三方 App 4</string>
<string id="FldApp5">第三方 App 5</string>
<string id="FldOwmTemp">OWM 气温</string>
<string id="FldOwmMinMax">OWM 最高/最低</string>
<string id="FldOwmFeels">OWM 体感温度</string>
@@ -120,6 +146,11 @@
<string id="FldOwmMinMaxTmrw">OWM 明日高低温</string>
<string id="FldOwmDayAfter">OWM 后日气温</string>
<string id="FldOwmMinMaxD2">OWM 后日高低温</string>
<string id="FldOwmAqi">OWM 空气质量</string>
<string id="FldOwmAqiText">OWM 空气质量描述</string>
<string id="FldOwmPm25">OWM PM2.5</string>
<string id="FldOwmPm10">OWM PM10</string>
<string id="FldOwmCo">OWM 一氧化碳</string>
<string id="FldOwmCity">OWM 城市</string>
<string id="FldOwmUpdated">OWM 更新时间</string>
<string id="FldCustom1">自定义文字 1</string>
@@ -127,7 +158,7 @@
<string id="FldCustom3">自定义文字 3</string>
<!-- On-face captions, in FieldTable.IDS order. -->
<string id="FieldLabels">,时间,UTC,时区1,时区2,日期,日期,日期,日,星期,星期,月,月,,周,秒,秒,周,步数,步数,还差,千卡,活动,分钟,中强度,高强度,周分钟,距离,楼层,下楼,爬升,推行,推行,周步数,7天,周千卡,7天,周距离,7天,周楼层,7天,7天,心率,静息,压力,血氧,呼吸,体能,活动,活动,体重,BMI,恢复,摄氧,摄氧,电量,天数,电量,闹钟,通知,勿扰,蓝牙,太阳能,海拔,气压,气压,日出,日落,太阳,太阳,月龄,月相,民用晨,航海晨,天文晨,蓝调晨,黄金晨,黄金昏,蓝调昏,民用昏,航海昏,天文昏,气温,高低温,体感,最高,最低,湿度,降水,风速,风向,风向,更新,气压,紫外线,能见度,露点,气温,高低温,体感,最高,最低,天气,天气,湿度,气压,能见度,风速,风向,风向,阵风,降雨,降雪,云量,降水,露点,明日,明日,后日,后日,城市,更新,,,</string>
<string id="FieldLabels">,时间,UTC,时区1,时区2,日期,日期,日期,日,星期,星期,月,月,,周,秒,秒,周,时间,日期,日期,倒计时,步数,步数,还差,千卡,活动,分钟,中强度,高强度,周分钟,距离,楼层,下楼,爬升,推行,推行,周步数,7天,周千卡,7天,周距离,7天,周楼层,7天,7天,28天,28天,28天,28天,心率,静息,压力,血氧,呼吸,体能,活动,活动,体重,BMI,恢复,摄氧,摄氧,电量,天数,电量,闹钟,通知,勿扰,蓝牙,太阳能,海拔,气压,气压,日出,日落,太阳,太阳,月龄,月相,民用晨,航海晨,天文晨,蓝调晨,黄金晨,黄金昏,蓝调昏,民用昏,航海昏,天文昏,气温,高低温,体感,最高,最低,湿度,降水,风速,风向,风向,更新,气压,紫外线,能见度,露点,日程,训练,5K,10K,半马,全马,5K配速,10K配速,半马配速,全马配速,高尔夫,跑步,骑行,App1,App2,App3,App4,App5,气温,高低温,体感,最高,最低,天气,天气,湿度,气压,能见度,风速,风向,风向,阵风,降雨,降雪,云量,降水,露点,明日,明日,后日,后日,空气,空气,PM2.5,PM10,CO,城市,更新,,,</string>
<string id="MetricSteps">步数</string>
<string id="MetricCalories">卡路里</string>

View File

@@ -13,6 +13,10 @@
<string id="SettingTempUnit">温度单位</string>
<string id="SettingAltOffset1">第二时区 1相对本地分钟数</string>
<string id="SettingAltOffset2">第二时区 2相对本地分钟数</string>
<string id="SettingCountdownDate">倒计时目标日期YYYY-MM-DD</string>
<string id="SettingTimeFmt">自定义时间格式h m a</string>
<string id="SettingDateFmt1">日期格式 1D M Y w</string>
<string id="SettingDateFmt2">日期格式 2D M Y w</string>
<string id="SettingOwmKey">OpenWeatherMap API 密钥</string>
<string id="SettingCustom1">自定义文字 1</string>
<string id="SettingCustom2">自定义文字 2</string>

View File

@@ -13,6 +13,10 @@
<property id="TempUnit" type="number">0</property>
<property id="AltOffset1" type="number">0</property>
<property id="AltOffset2" type="number">0</property>
<property id="CountdownDate" type="string"></property>
<property id="TimeFmt" type="string">h:m a</property>
<property id="DateFmt1" type="string">D/M</property>
<property id="DateFmt2" type="string">w D M</property>
<property id="OwmKey" type="string"></property>
<property id="Custom1" type="string"></property>
<property id="Custom2" type="string"></property>

View File

@@ -32,6 +32,10 @@
<listEntry value="850">@Strings.FldSeconds</listEntry>
<listEntry value="869">@Strings.FldSeconds0</listEntry>
<listEntry value="873">@Strings.FldWeekCal</listEntry>
<listEntry value="861">@Strings.FldCustomTime</listEntry>
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
<listEntry value="735">@Strings.FldCountdown</listEntry>
<listEntry value="1">@Strings.FldSteps</listEntry>
<listEntry value="14">@Strings.FldStepsRound</listEntry>
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
@@ -56,6 +60,10 @@
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
<listEntry value="204">@Strings.FldFloors7d</listEntry>
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
<listEntry value="828">@Strings.FldSteps28d</listEntry>
<listEntry value="832">@Strings.FldCal28d</listEntry>
<listEntry value="836">@Strings.FldDist28d</listEntry>
<listEntry value="840">@Strings.FldFloors28d</listEntry>
<listEntry value="618">@Strings.FldHeartRate</listEntry>
<listEntry value="276">@Strings.FldRestingHr</listEntry>
<listEntry value="12">@Strings.FldStress</listEntry>
@@ -111,6 +119,24 @@
<listEntry value="313">@Strings.FldUvi</listEntry>
<listEntry value="314">@Strings.FldVisibility</listEntry>
<listEntry value="315">@Strings.FldDewPoint</listEntry>
<listEntry value="612">@Strings.FldNextCal</listEntry>
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
<listEntry value="627">@Strings.FldRace5k</listEntry>
<listEntry value="628">@Strings.FldRace10k</listEntry>
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
<listEntry value="630">@Strings.FldRaceFull</listEntry>
<listEntry value="631">@Strings.FldPace5k</listEntry>
<listEntry value="632">@Strings.FldPace10k</listEntry>
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
<listEntry value="634">@Strings.FldPaceFull</listEntry>
<listEntry value="641">@Strings.FldGolfScore</listEntry>
<listEntry value="619">@Strings.FldRunWeek</listEntry>
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
<listEntry value="695">@Strings.FldApp1</listEntry>
<listEntry value="696">@Strings.FldApp2</listEntry>
<listEntry value="697">@Strings.FldApp3</listEntry>
<listEntry value="698">@Strings.FldApp4</listEntry>
<listEntry value="699">@Strings.FldApp5</listEntry>
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
@@ -134,6 +160,11 @@
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
<listEntry value="542">@Strings.FldOwmCo</listEntry>
<listEntry value="516">@Strings.FldOwmCity</listEntry>
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
<listEntry value="703">@Strings.FldCustom1</listEntry>
@@ -162,6 +193,10 @@
<listEntry value="850">@Strings.FldSeconds</listEntry>
<listEntry value="869">@Strings.FldSeconds0</listEntry>
<listEntry value="873">@Strings.FldWeekCal</listEntry>
<listEntry value="861">@Strings.FldCustomTime</listEntry>
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
<listEntry value="735">@Strings.FldCountdown</listEntry>
<listEntry value="1">@Strings.FldSteps</listEntry>
<listEntry value="14">@Strings.FldStepsRound</listEntry>
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
@@ -186,6 +221,10 @@
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
<listEntry value="204">@Strings.FldFloors7d</listEntry>
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
<listEntry value="828">@Strings.FldSteps28d</listEntry>
<listEntry value="832">@Strings.FldCal28d</listEntry>
<listEntry value="836">@Strings.FldDist28d</listEntry>
<listEntry value="840">@Strings.FldFloors28d</listEntry>
<listEntry value="618">@Strings.FldHeartRate</listEntry>
<listEntry value="276">@Strings.FldRestingHr</listEntry>
<listEntry value="12">@Strings.FldStress</listEntry>
@@ -241,6 +280,24 @@
<listEntry value="313">@Strings.FldUvi</listEntry>
<listEntry value="314">@Strings.FldVisibility</listEntry>
<listEntry value="315">@Strings.FldDewPoint</listEntry>
<listEntry value="612">@Strings.FldNextCal</listEntry>
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
<listEntry value="627">@Strings.FldRace5k</listEntry>
<listEntry value="628">@Strings.FldRace10k</listEntry>
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
<listEntry value="630">@Strings.FldRaceFull</listEntry>
<listEntry value="631">@Strings.FldPace5k</listEntry>
<listEntry value="632">@Strings.FldPace10k</listEntry>
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
<listEntry value="634">@Strings.FldPaceFull</listEntry>
<listEntry value="641">@Strings.FldGolfScore</listEntry>
<listEntry value="619">@Strings.FldRunWeek</listEntry>
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
<listEntry value="695">@Strings.FldApp1</listEntry>
<listEntry value="696">@Strings.FldApp2</listEntry>
<listEntry value="697">@Strings.FldApp3</listEntry>
<listEntry value="698">@Strings.FldApp4</listEntry>
<listEntry value="699">@Strings.FldApp5</listEntry>
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
@@ -264,6 +321,11 @@
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
<listEntry value="542">@Strings.FldOwmCo</listEntry>
<listEntry value="516">@Strings.FldOwmCity</listEntry>
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
<listEntry value="703">@Strings.FldCustom1</listEntry>
@@ -292,6 +354,10 @@
<listEntry value="850">@Strings.FldSeconds</listEntry>
<listEntry value="869">@Strings.FldSeconds0</listEntry>
<listEntry value="873">@Strings.FldWeekCal</listEntry>
<listEntry value="861">@Strings.FldCustomTime</listEntry>
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
<listEntry value="735">@Strings.FldCountdown</listEntry>
<listEntry value="1">@Strings.FldSteps</listEntry>
<listEntry value="14">@Strings.FldStepsRound</listEntry>
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
@@ -316,6 +382,10 @@
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
<listEntry value="204">@Strings.FldFloors7d</listEntry>
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
<listEntry value="828">@Strings.FldSteps28d</listEntry>
<listEntry value="832">@Strings.FldCal28d</listEntry>
<listEntry value="836">@Strings.FldDist28d</listEntry>
<listEntry value="840">@Strings.FldFloors28d</listEntry>
<listEntry value="618">@Strings.FldHeartRate</listEntry>
<listEntry value="276">@Strings.FldRestingHr</listEntry>
<listEntry value="12">@Strings.FldStress</listEntry>
@@ -371,6 +441,24 @@
<listEntry value="313">@Strings.FldUvi</listEntry>
<listEntry value="314">@Strings.FldVisibility</listEntry>
<listEntry value="315">@Strings.FldDewPoint</listEntry>
<listEntry value="612">@Strings.FldNextCal</listEntry>
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
<listEntry value="627">@Strings.FldRace5k</listEntry>
<listEntry value="628">@Strings.FldRace10k</listEntry>
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
<listEntry value="630">@Strings.FldRaceFull</listEntry>
<listEntry value="631">@Strings.FldPace5k</listEntry>
<listEntry value="632">@Strings.FldPace10k</listEntry>
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
<listEntry value="634">@Strings.FldPaceFull</listEntry>
<listEntry value="641">@Strings.FldGolfScore</listEntry>
<listEntry value="619">@Strings.FldRunWeek</listEntry>
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
<listEntry value="695">@Strings.FldApp1</listEntry>
<listEntry value="696">@Strings.FldApp2</listEntry>
<listEntry value="697">@Strings.FldApp3</listEntry>
<listEntry value="698">@Strings.FldApp4</listEntry>
<listEntry value="699">@Strings.FldApp5</listEntry>
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
@@ -394,6 +482,11 @@
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
<listEntry value="542">@Strings.FldOwmCo</listEntry>
<listEntry value="516">@Strings.FldOwmCity</listEntry>
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
<listEntry value="703">@Strings.FldCustom1</listEntry>
@@ -422,6 +515,10 @@
<listEntry value="850">@Strings.FldSeconds</listEntry>
<listEntry value="869">@Strings.FldSeconds0</listEntry>
<listEntry value="873">@Strings.FldWeekCal</listEntry>
<listEntry value="861">@Strings.FldCustomTime</listEntry>
<listEntry value="862">@Strings.FldDateFmt1</listEntry>
<listEntry value="863">@Strings.FldDateFmt2</listEntry>
<listEntry value="735">@Strings.FldCountdown</listEntry>
<listEntry value="1">@Strings.FldSteps</listEntry>
<listEntry value="14">@Strings.FldStepsRound</listEntry>
<listEntry value="707">@Strings.FldStepsToGo</listEntry>
@@ -446,6 +543,10 @@
<listEntry value="203">@Strings.FldFloorsWeek</listEntry>
<listEntry value="204">@Strings.FldFloors7d</listEntry>
<listEntry value="200">@Strings.FldActiveMin7d</listEntry>
<listEntry value="828">@Strings.FldSteps28d</listEntry>
<listEntry value="832">@Strings.FldCal28d</listEntry>
<listEntry value="836">@Strings.FldDist28d</listEntry>
<listEntry value="840">@Strings.FldFloors28d</listEntry>
<listEntry value="618">@Strings.FldHeartRate</listEntry>
<listEntry value="276">@Strings.FldRestingHr</listEntry>
<listEntry value="12">@Strings.FldStress</listEntry>
@@ -501,6 +602,24 @@
<listEntry value="313">@Strings.FldUvi</listEntry>
<listEntry value="314">@Strings.FldVisibility</listEntry>
<listEntry value="315">@Strings.FldDewPoint</listEntry>
<listEntry value="612">@Strings.FldNextCal</listEntry>
<listEntry value="626">@Strings.FldTrainStatus</listEntry>
<listEntry value="627">@Strings.FldRace5k</listEntry>
<listEntry value="628">@Strings.FldRace10k</listEntry>
<listEntry value="629">@Strings.FldRaceHalf</listEntry>
<listEntry value="630">@Strings.FldRaceFull</listEntry>
<listEntry value="631">@Strings.FldPace5k</listEntry>
<listEntry value="632">@Strings.FldPace10k</listEntry>
<listEntry value="633">@Strings.FldPaceHalf</listEntry>
<listEntry value="634">@Strings.FldPaceFull</listEntry>
<listEntry value="641">@Strings.FldGolfScore</listEntry>
<listEntry value="619">@Strings.FldRunWeek</listEntry>
<listEntry value="620">@Strings.FldBikeWeek</listEntry>
<listEntry value="695">@Strings.FldApp1</listEntry>
<listEntry value="696">@Strings.FldApp2</listEntry>
<listEntry value="697">@Strings.FldApp3</listEntry>
<listEntry value="698">@Strings.FldApp4</listEntry>
<listEntry value="699">@Strings.FldApp5</listEntry>
<listEntry value="500">@Strings.FldOwmTemp</listEntry>
<listEntry value="515">@Strings.FldOwmMinMax</listEntry>
<listEntry value="503">@Strings.FldOwmFeels</listEntry>
@@ -524,6 +643,11 @@
<listEntry value="522">@Strings.FldOwmMinMaxTmrw</listEntry>
<listEntry value="535">@Strings.FldOwmDayAfter</listEntry>
<listEntry value="523">@Strings.FldOwmMinMaxD2</listEntry>
<listEntry value="538">@Strings.FldOwmAqi</listEntry>
<listEntry value="539">@Strings.FldOwmAqiText</listEntry>
<listEntry value="540">@Strings.FldOwmPm25</listEntry>
<listEntry value="541">@Strings.FldOwmPm10</listEntry>
<listEntry value="542">@Strings.FldOwmCo</listEntry>
<listEntry value="516">@Strings.FldOwmCity</listEntry>
<listEntry value="517">@Strings.FldOwmUpdated</listEntry>
<listEntry value="703">@Strings.FldCustom1</listEntry>
@@ -597,6 +721,22 @@
<settingConfig type="numeric" min="-720" max="840"/>
</setting>
<setting propertyKey="@Properties.CountdownDate" title="@Strings.SettingCountdownDate">
<settingConfig type="alphaNumeric" maxLength="10"/>
</setting>
<setting propertyKey="@Properties.TimeFmt" title="@Strings.SettingTimeFmt">
<settingConfig type="alphaNumeric" maxLength="16"/>
</setting>
<setting propertyKey="@Properties.DateFmt1" title="@Strings.SettingDateFmt1">
<settingConfig type="alphaNumeric" maxLength="16"/>
</setting>
<setting propertyKey="@Properties.DateFmt2" title="@Strings.SettingDateFmt2">
<settingConfig type="alphaNumeric" maxLength="16"/>
</setting>
<setting propertyKey="@Properties.OwmKey" title="@Strings.SettingOwmKey">
<settingConfig type="alphaNumeric" maxLength="40"/>
</setting>

View File

@@ -18,6 +18,10 @@
<string id="FldSeconds">Seconds</string>
<string id="FldSeconds0">Seconds (09)</string>
<string id="FldWeekCal">Week Number</string>
<string id="FldCustomTime">Custom Time Format</string>
<string id="FldDateFmt1">Date Format 1</string>
<string id="FldDateFmt2">Date Format 2</string>
<string id="FldCountdown">Event Countdown</string>
<string id="FldSteps">Steps</string>
<string id="FldStepsRound">Steps Rounded</string>
<string id="FldStepsToGo">Steps to Go</string>
@@ -42,6 +46,10 @@
<string id="FldFloorsWeek">Floors (Week to Date)</string>
<string id="FldFloors7d">Floors (Last 7 Days)</string>
<string id="FldActiveMin7d">Active Minutes (7 Days)</string>
<string id="FldSteps28d">Steps (28 Days)</string>
<string id="FldCal28d">Calories (28 Days)</string>
<string id="FldDist28d">Distance (28 Days)</string>
<string id="FldFloors28d">Floors (28 Days)</string>
<string id="FldHeartRate">Heart Rate</string>
<string id="FldRestingHr">Resting Heart Rate</string>
<string id="FldStress">Stress Score</string>
@@ -97,6 +105,24 @@
<string id="FldUvi">UV Index</string>
<string id="FldVisibility">Visibility</string>
<string id="FldDewPoint">Dew Point</string>
<string id="FldNextCal">Next Calendar Event</string>
<string id="FldTrainStatus">Training Status</string>
<string id="FldRace5k">Race Time (5k)</string>
<string id="FldRace10k">Race Time (10k)</string>
<string id="FldRaceHalf">Race Time (Half)</string>
<string id="FldRaceFull">Race Time (Marathon)</string>
<string id="FldPace5k">Pace (5k)</string>
<string id="FldPace10k">Pace (10k)</string>
<string id="FldPaceHalf">Pace (Half)</string>
<string id="FldPaceFull">Pace (Marathon)</string>
<string id="FldGolfScore">Last Golf Round Score</string>
<string id="FldRunWeek">Run Distance (Week)</string>
<string id="FldBikeWeek">Bike Distance (Week)</string>
<string id="FldApp1">App 1 (on watch)</string>
<string id="FldApp2">App 2 (on watch)</string>
<string id="FldApp3">App 3 (on watch)</string>
<string id="FldApp4">App 4 (on watch)</string>
<string id="FldApp5">App 5 (on watch)</string>
<string id="FldOwmTemp">OWM Temperature</string>
<string id="FldOwmMinMax">OWM Max/Min Temp</string>
<string id="FldOwmFeels">OWM Feels Like</string>
@@ -120,6 +146,11 @@
<string id="FldOwmMinMaxTmrw">OWM Min/Max Tomorrow</string>
<string id="FldOwmDayAfter">OWM Temp Day After</string>
<string id="FldOwmMinMaxD2">OWM Min/Max Day After</string>
<string id="FldOwmAqi">OWM Air Quality (AQI)</string>
<string id="FldOwmAqiText">OWM AQI Description</string>
<string id="FldOwmPm25">OWM PM2.5</string>
<string id="FldOwmPm10">OWM PM10</string>
<string id="FldOwmCo">OWM CO</string>
<string id="FldOwmCity">OWM City Name</string>
<string id="FldOwmUpdated">OWM Update Time</string>
<string id="FldCustom1">Custom Text 1</string>
@@ -127,7 +158,7 @@
<string id="FldCustom3">Custom Text 3</string>
<!-- On-face captions, in FieldTable.IDS order. -->
<string id="FieldLabels">,TIME,UTC,TZ1,TZ2,DATE,DATE,DATE,DAY,DAY,DAY,MON,MON,,WEEK,SEC,SEC,WEEK,STEP,STEP,TO GO,KCAL,ACAL,MIN,MOD,VIG,WEEK,DST,FLR,DESC,ASC,PUSH,PUSH,WEEK,7D,WEEK,7D,WEEK,7D,WEEK,7D,7D,HR,RHR,STRS,SPO2,RESP,BODY,MOVE,MOVE,WT,BMI,RCVR,VO2,VO2,BATT,DAYS,BATT,ALRM,MSG,DND,BT,SOLR,ALT,hPa,hPa,RISE,SET,SUN,SUN,MOON,MOON,CIVIL,NAUT,ASTR,BLUE,GOLD,GOLD,BLUE,CIVIL,NAUT,ASTR,TEMP,HI/LO,FEEL,HIGH,LOW,HUM,RAIN,WIND,WIND,WIND,WX,hPa,UVI,VIS,DEW,TEMP,HI/LO,FEEL,HIGH,LOW,WX,WX,HUM,hPa,VIS,WIND,WIND,WIND,GUST,RAIN,SNOW,CLD,RAIN,DEW,TMRW,TMRW,D+2,D+2,CITY,OWM,,,</string>
<string id="FieldLabels">,TIME,UTC,TZ1,TZ2,DATE,DATE,DATE,DAY,DAY,DAY,MON,MON,,WEEK,SEC,SEC,WEEK,TIME,DATE,DATE,DAYS,STEP,STEP,TO GO,KCAL,ACAL,MIN,MOD,VIG,WEEK,DST,FLR,DESC,ASC,PUSH,PUSH,WEEK,7D,WEEK,7D,WEEK,7D,WEEK,7D,7D,28D,28D,28D,28D,HR,RHR,STRS,SPO2,RESP,BODY,MOVE,MOVE,WT,BMI,RCVR,VO2,VO2,BATT,DAYS,BATT,ALRM,MSG,DND,BT,SOLR,ALT,hPa,hPa,RISE,SET,SUN,SUN,MOON,MOON,CIVIL,NAUT,ASTR,BLUE,GOLD,GOLD,BLUE,CIVIL,NAUT,ASTR,TEMP,HI/LO,FEEL,HIGH,LOW,HUM,RAIN,WIND,WIND,WIND,WX,hPa,UVI,VIS,DEW,CAL,TRN,5K,10K,HM,FM,5K,10K,HM,FM,GOLF,RUN,BIKE,APP1,APP2,APP3,APP4,APP5,TEMP,HI/LO,FEEL,HIGH,LOW,WX,WX,HUM,hPa,VIS,WIND,WIND,WIND,GUST,RAIN,SNOW,CLD,RAIN,DEW,TMRW,TMRW,D+2,D+2,AQI,AQI,PM2.5,PM10,CO,CITY,OWM,,,</string>
<string id="MetricSteps">Steps</string>
<string id="MetricCalories">Calories</string>

View File

@@ -13,6 +13,10 @@
<string id="SettingTempUnit">Temperature Unit</string>
<string id="SettingAltOffset1">Alternate Time 1 (minutes from local)</string>
<string id="SettingAltOffset2">Alternate Time 2 (minutes from local)</string>
<string id="SettingCountdownDate">Countdown Target (YYYY-MM-DD)</string>
<string id="SettingTimeFmt">Custom Time Format (h m a)</string>
<string id="SettingDateFmt1">Date Format 1 (D M Y w)</string>
<string id="SettingDateFmt2">Date Format 2 (D M Y w)</string>
<string id="SettingOwmKey">OpenWeatherMap API Key</string>
<string id="SettingCustom1">Custom Text 1</string>
<string id="SettingCustom2">Custom Text 2</string>

184
source/Comp.mc Normal file
View File

@@ -0,0 +1,184 @@
import Toybox.Lang;
import Toybox.Complications;
import Toybox.System;
// ============================================================================
// 系统 Complication 桥接
// ----------------------------------------------------------------------------
// Toybox.ComplicationsCIQ 4.2+,本项目支持的 17 款设备全都有)让表盘可以订阅
// 系统和**其它 App 发布**的复杂功能数据。
//
// 这个模块解锁了一批我原本判断「表盘做不到」的字段 —— 当时只查了
// ActivityMonitor / UserProfile / Weather漏掉了这条路
//
// 日历事件 CIQ 确实没有日历 API但系统把「下一个日程」作为
// complication 发布出来了
// 训练状态 ActivityMonitor.Info 上没有,这里有
// 完赛/配速预测 同上5k/10k/半马/全马各两套
// 本周跑步/骑行距离 这是**按运动分类**的ActivityMonitor 的按天历史做不出来
// 高尔夫成绩 无其它来源
// 第三方 App CGM、补水、行情这类 App 发布的复杂功能,
// 通过迭代器按序号取(对应目录里的 App 1..App 5
//
// 取值策略每次绘制现查getComplication 很轻量),不做订阅回调 ——
// 表盘每分钟才重绘一次,订阅的实时性没有意义,反而要维护状态。
// ============================================================================
module Comp {
// 字段编号 → complication 类型。
// 返回 null 表示这个编号不归本模块管。
function typeFor(id as Number) {
if (id == 612) { return Complications.COMPLICATION_TYPE_CALENDAR_EVENTS; }
if (id == 626) { return Complications.COMPLICATION_TYPE_TRAINING_STATUS; }
if (id == 627) { return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_5K; }
if (id == 628) { return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_10K; }
if (id == 629) {
return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_HALF_MARATHON;
}
if (id == 630) {
return Complications.COMPLICATION_TYPE_RACE_PREDICTOR_MARATHON;
}
if (id == 631) { return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_5K; }
if (id == 632) { return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_10K; }
if (id == 633) {
return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_HALF_MARATHON;
}
if (id == 634) {
return Complications.COMPLICATION_TYPE_RACE_PACE_PREDICTOR_MARATHON;
}
if (id == 641) {
return Complications.COMPLICATION_TYPE_LAST_GOLF_ROUND_SCORE;
}
if (id == 619) {
return Complications.COMPLICATION_TYPE_WEEKLY_RUN_DISTANCE;
}
if (id == 620) {
return Complications.COMPLICATION_TYPE_WEEKLY_BIKE_DISTANCE;
}
return null;
}
// 第三方 App 列表的每帧缓存。
//
// 枚举一次要走完整个迭代器,四个数据位如果都选了 App 槽就会走四遍。
// 表盘每分钟才画一次,本来不算大开销,但 onUpdate 有看门狗时限,
// 能省则省 —— 所以每帧只枚举一次,由 view 在绘制开始时调 beginFrame() 失效。
var mApps as Array<Complications.Complication>?;
function beginFrame() as Void {
mApps = null;
}
// 695..699 是「第三方 App 1..5」,不对应固定类型,靠迭代器按序号取。
function isAppSlot(id as Number) as Boolean {
return id >= 695 && id <= 699;
}
function owns(id as Number) as Boolean {
return isAppSlot(id) || typeFor(id) != null;
}
// 按类型取一个 complication 对象;取不到返回 null。
// 用 try 包住是因为设备不支持某个类型时 getComplication 会抛,而不是返回
// null —— 和本项目里其它 CIQ API 的老毛病一样。
function byType(type) as Complications.Complication? {
try {
return Complications.getComplication(new Complications.Id(type));
} catch (e) {
return null;
}
}
// 取第 n 个**第三方发布**的 complicationn 从 0 起)。
//
// 迭代器里既有系统自带的,也有别的 App 发布的。这里只保留「有短标签且有值」
// 的项 —— 没装对应 App 时那些是空壳,全留着的话序号会随手表上装了什么 App
// 而漂移,用户设好的第 3 个明天可能就变成别的东西了。
//
// Complication 的成员是 complicationId / value / unit / shortLabel /
// longLabel / ranges没有 label别写错
function nthApp(n as Number) as Complications.Complication? {
if (mApps == null) {
var list = [] as Array<Complications.Complication>;
try {
var it = Complications.getComplications();
if (it != null) {
var c = it.next();
while (c != null) {
if (c.value != null && c.shortLabel != null
&& c.shortLabel instanceof Lang.String) {
list.add(c);
}
c = it.next();
}
}
} catch (e) {
// 设备不支持枚举时就当作一个都没有。
}
mApps = list;
}
return (n >= 0 && n < mApps.size()) ? mApps[n] : null;
}
// 把 complication 的值转成可显示的字符串。
//
// value 的类型随 complication 而变数字、字符串都有unit 有时带单位词。
// 完赛/配速预测的 value 是**秒数**,直接印出来是一串没意义的数字,
// 所以单独格式化成 h:mm:ss / mm:ss。
function render(c as Complications.Complication?, id as Number) as String? {
if (c == null || c.value == null) { return null; }
var v = c.value;
if (isTimeSeconds(id) && !(v instanceof Lang.String)) {
return hms(v.toNumber());
}
// 浮点直接 toString 会印成 "0.000000",统一保留一位小数。
if (v instanceof Lang.Float || v instanceof Lang.Double) {
return trimUnit(v.toFloat().format("%.1f"), c);
}
var s = v.toString();
// 单位词能明显帮上忙时才拼(比如距离的 km纯数字类就不拼了。
//
// ⚠️ unit 的类型不保证是 String —— 周跑步距离那种返回的不是字符串,
// 直接调 .length() 会抛 "Could not find symbol 'length'"。
// 所以先用 instanceof 判一次再说。
if (c.unit != null && c.unit instanceof Lang.String) {
var u = c.unit as String;
if (u.length() > 0 && u.length() <= 3) { s = s + u; }
}
return s;
}
// 把单位词拼到已格式化好的数值后面(如果 unit 确实是个短字符串)。
function trimUnit(s as String, c as Complications.Complication) as String {
if (c.unit != null && c.unit instanceof Lang.String) {
var u = c.unit as String;
if (u.length() > 0 && u.length() <= 3) { return s + u; }
}
return s;
}
// 完赛预测627630与配速预测631634都是秒。
function isTimeSeconds(id as Number) as Boolean {
return id >= 627 && id <= 634;
}
// 秒 → h:mm:ss不足一小时则 m:ss。配速通常是后者。
function hms(total as Number) as String {
if (total <= 0) { return "--"; }
var h = total / 3600;
var m = (total % 3600) / 60;
var s = total % 60;
if (h > 0) {
return h.format("%d") + ":" + m.format("%02d") + ":" + s.format("%02d");
}
return m.format("%d") + ":" + s.format("%02d");
}
// 字段取值总入口。不属于本模块的编号返回 null。
function value(id as Number) as String? {
if (!owns(id)) { return null; }
var c = isAppSlot(id) ? nthApp(id - 695) : byType(typeFor(id));
var out = render(c, id);
return out == null ? "--" : out;
}
}

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;

View File

@@ -12,6 +12,7 @@ import Fields;
import Layout;
import Icons;
import Settings;
import Comp;
// ============================================================================
// 表盘主体
@@ -429,6 +430,7 @@ class Fenix8V3View extends WatchUi.WatchFace {
var ti = Settings.themeIndex(Themes.NAMES.size());
var ds = System.getDeviceSettings();
var ctx = new Fields.Ctx(ds, Settings.tempUnit());
Comp.beginFrame(); // 让第三方 App 列表这一帧只枚举一次
var info = ctx.info();
var stats = ctx.stats();
var level = 0.0f;

View File

@@ -11,12 +11,14 @@ module FieldTable {
// 全部字段 id顺序与 FieldLabels 资源串(逗号分隔的标签表)严格一致 ——
// Fields.label() 就是靠这个下标去取对应标签的。
const IDS = [
0,860,880,864,865,851,872,852,855,870,856,853,868,858,859,850,869,873,1,14,707,2,
603,150,152,154,151,3,4,5,6,9,10,219,220,211,212,213,214,203,204,200,618,276,12,635,
11,623,8,15,281,742,13,278,277,250,251,257,253,255,254,256,252,615,616,733,712,717,
700,731,706,732,710,709,708,711,714,715,718,719,720,721,306,639,300,301,302,305,304,
308,307,310,309,312,313,314,315,500,515,503,504,505,507,502,501,506,509,510,511,518,
512,513,532,536,537,530,534,522,535,523,516,517,703,704,705
0,860,880,864,865,851,872,852,855,870,856,853,868,858,859,850,869,873,861,862,863,
735,1,14,707,2,603,150,152,154,151,3,4,5,6,9,10,219,220,211,212,213,214,203,204,200,
828,832,836,840,618,276,12,635,11,623,8,15,281,742,13,278,277,250,251,257,253,255,
254,256,252,615,616,733,712,717,700,731,706,732,710,709,708,711,714,715,718,719,720,
721,306,639,300,301,302,305,304,308,307,310,309,312,313,314,315,612,626,627,628,629,
630,631,632,633,634,641,619,620,695,696,697,698,699,500,515,503,504,505,507,502,501,
506,509,510,511,518,512,513,532,536,537,530,534,522,535,523,538,539,540,541,542,516,
517,703,704,705
];
// 由字段 id 反查它在 IDS 里的下标;找不到返回 0对应「关闭」

View File

@@ -15,6 +15,7 @@ import FieldTable;
import Settings;
import Solar;
import Owm;
import Comp;
// ============================================================================
// 数据字段
@@ -80,6 +81,89 @@ module Fields {
return Lang.format(mDateFmt, [month(g), g.day.format("%d")]);
}
// 按用户填的格式串拼时间/日期。
//
// 用单字母占位符,因为设置项里能填的长度有限,而且这样中英文都好写:
// h 小时12/24 跟随手表) H 小时补零
// m 分钟补零 s 秒补零
// a 上午/下午24 小时制下为空)
// D 日 M 月(数字) N 月(名称)
// Y 四位年 y 两位年 w 星期
// 其它字符原样输出,所以 "D/M" → "10/9""w N D" → "周四 9月 10"。
function applyFormat(fmt as String, g as Gregorian.Info, c as Ctx) as String {
var out = "";
var chars = fmt.toCharArray();
for (var i = 0; i < chars.size(); i++) {
var ch = chars[i];
if (ch == 'h') {
var hh = g.hour;
if (!c.is24) {
if (hh == 0) { hh = 12; } else if (hh > 12) { hh = hh - 12; }
}
out += hh.format("%d");
} else if (ch == 'H') {
out += g.hour.format("%02d");
} else if (ch == 'm') {
out += g.min.format("%02d");
} else if (ch == 's') {
out += g.sec.format("%02d");
} else if (ch == 'a') {
out += c.is24 ? "" : meridiem(g);
} else if (ch == 'D') {
out += g.day.format("%d");
} else if (ch == 'M') {
out += g.month.format("%d");
} else if (ch == 'N') {
out += month(g);
} else if (ch == 'Y') {
out += g.year.format("%d");
} else if (ch == 'y') {
out += (g.year % 100).format("%02d");
} else if (ch == 'w') {
out += dow(g);
} else {
out += ch.toString();
}
}
return out;
}
// 距目标日期还有几天。目标日期是设置里填的 YYYY-MM-DD 字符串。
// 解析失败(没填/格式不对)返回 null字段显示 "--"。
function daysUntil(target as String, g as Gregorian.Info) as Number? {
if (target.length() < 8) { return null; }
var parts = splitOn(target, '-');
if (parts.size() < 3) { return null; }
var y = parts[0].toNumber();
var mo = parts[1].toNumber();
var d = parts[2].toNumber();
if (y == null || mo == null || d == null) { return null; }
// 都换算成「儒略日式」的绝对天数再相减,省得处理跨月跨年。
return absDay(y, mo, d) - absDay(g.year, g.month, g.day);
}
function absDay(y as Number, m as Number, d as Number) as Number {
var yy = y;
var mm = m;
if (mm <= 2) { yy = yy - 1; mm = mm + 12; }
var a = yy / 100;
var b = 2 - a + a / 4;
return (365.25 * (yy + 4716)).toNumber() + (30.6001 * (mm + 1)).toNumber()
+ d + b - 1524;
}
function splitOn(str as String, sep as Char) as Array<String> {
var out = [] as Array<String>;
var cur = "";
var chars = str.toCharArray();
for (var i = 0; i < chars.size(); i++) {
if (chars[i] == sep) { out.add(cur); cur = ""; }
else { cur += chars[i].toString(); }
}
out.add(cur);
return out;
}
// 英文出 "10",中文出 "10日"。
// 后缀带在本地化的日期格式串里,所以这里把月份传空串即可。
function dayOnly(g as Gregorian.Info) as String {
@@ -375,6 +459,15 @@ module Fields {
// view 只在有格子选了秒时才去请求那个回调。
if (id == 850) { return c.greg.sec.format("%d"); } // 秒
if (id == 869) { return c.greg.sec.format("%02d"); } // 秒 (前导零)
// 自定义格式:格式串来自设置,占位符说明见 applyFormat
if (id == 861) { return applyFormat(Settings.text("TimeFmt"), c.greg, c); }
if (id == 862) { return applyFormat(Settings.text("DateFmt1"), c.greg, c); }
if (id == 863) { return applyFormat(Settings.text("DateFmt2"), c.greg, c); }
if (id == 735) { // 事件倒计时
var left = daysUntil(Settings.text("CountdownDate"), c.greg);
if (left == null) { return DASH; }
return left.format("%d");
} // 秒 (前导零)
if (id == 870) { return dow(c.greg); } // 星期简写
if (id == 856) { return dow(c.greg); } // 星期全称
if (id == 853) { return month(c.greg); } // 月份
@@ -450,6 +543,14 @@ module Fields {
return (id == 213) ? dist(v, c) : rounded(v);
}
// 近 7 天步数 / 近 7 天卡路里 / 近 7 天距离 / 近 7 天楼层 / 近 7 天活动分钟
if (id == 828 || id == 832 || id == 836 || id == 840) {
// 近 28 天。getHistory 能返回多少天由固件决定sumHistory 内部会按
// 实际条数截断,所以拿不到 28 天时是「有多少算多少」而不是报错。
var w28 = (id == 828) ? 0 : ((id == 832) ? 1 : ((id == 836) ? 2 : 3));
var v28 = sumHistory(c, w28, 27);
if (v28 == null) { return DASH; }
return (id == 836) ? dist(v28, c) : rounded(v28);
}
if (id == 220 || id == 212 || id == 214 || id == 204 || id == 200) {
var which = (id == 220) ? 0 : ((id == 212) ? 1
: ((id == 214) ? 2 : ((id == 204) ? 3 : 4)));
@@ -760,6 +861,18 @@ module Fields {
if (id == 523) { // OWM 后日高低温
return temp(Owm.get("hi2"), c) + "/" + temp(Owm.get("lo2"), c);
}
if (id == 538 || id == 539) { // 空气质量指数 / 描述
var aqi = Owm.get("aqi");
if (aqi == null) { return DASH; }
if (id == 538) { return num(aqi); }
// OWM 的 AQI 是 1..5 的等级,不是欧美常见的 0..500 数值。
var names = ["", "优", "良", "中", "差", "很差"];
var k = aqi.toNumber();
return (k >= 1 && k <= 5) ? names[k] : DASH;
}
if (id == 540) { return oneDp(Owm.get("pm2_5")); } // PM2.5
if (id == 541) { return oneDp(Owm.get("pm10")); } // PM10
if (id == 542) { return oneDp(Owm.get("co")); } // 一氧化碳
if (id == 516) { return str(Owm.get("city")); } // OWM 城市
if (id == 517) { // OWM 更新时间
var dt = Owm.get("dt");
@@ -768,10 +881,21 @@ module Fields {
return DASH;
}
function oneDp(v) as String {
return v == null ? DASH : v.toFloat().format("%.1f");
}
function str(v) as String {
return v == null ? DASH : v.toString();
}
// ---- 系统 Complication ----
// 日历、训练状态、完赛/配速预测、按运动的周距离、以及第三方 App 发布的
// 复杂功能。全部由 Comp.mc 经 Toybox.Complications 取得。
function complication(id as Number, c as Ctx) as String? {
return Comp.value(id);
}
// ---- 自定义文字 ----
// 用户在设置里自己填的三段文字。
function custom(id as Number, c as Ctx) as String? {
@@ -791,6 +915,7 @@ module Fields {
if (v == null) { v = system(id, c); }
if (v == null) { v = environment(id, c); }
if (v == null) { v = weather(id, c); }
if (v == null) { v = complication(id, c); }
if (v == null) { v = owm(id, c); }
if (v == null) { v = custom(id, c); }
return v == null ? "" : v;

View File

@@ -47,6 +47,10 @@ FIELDS = [
(850, "Seconds", "Seconds", "", "SEC", ""),
(869, "Seconds0", "Seconds (09)", "秒 (前导零)", "SEC", ""),
(873, "WeekCal", "Week Number", "周数", "WEEK", ""),
(861, "CustomTime", "Custom Time Format", "自定义时间格式", "TIME", "时间"),
(862, "DateFmt1", "Date Format 1", "日期格式 1", "DATE", "日期"),
(863, "DateFmt2", "Date Format 2", "日期格式 2", "DATE", "日期"),
(735, "Countdown", "Event Countdown", "事件倒计时", "DAYS", "倒计时"),
# ---- activity -------------------------------------------------------
(1, "Steps", "Steps", "步数", "STEP", "步数"),
@@ -73,6 +77,10 @@ FIELDS = [
(203, "FloorsWeek", "Floors (Week to Date)", "本周楼层", "WEEK", "周楼层"),
(204, "Floors7d", "Floors (Last 7 Days)", "近 7 天楼层", "7D", "7天"),
(200, "ActiveMin7d", "Active Minutes (7 Days)", "近 7 天活动分钟", "7D", "7天"),
(828, "Steps28d", "Steps (28 Days)", "近 28 天步数", "28D", "28天"),
(832, "Cal28d", "Calories (28 Days)", "近 28 天卡路里", "28D", "28天"),
(836, "Dist28d", "Distance (28 Days)", "近 28 天距离", "28D", "28天"),
(840, "Floors28d", "Floors (28 Days)", "近 28 天楼层", "28D", "28天"),
# ---- body -----------------------------------------------------------
(618, "HeartRate", "Heart Rate", "心率", "HR", "心率"),
@@ -137,6 +145,27 @@ FIELDS = [
(314, "Visibility", "Visibility", "能见度", "VIS", "能见度"),
(315, "DewPoint", "Dew Point", "露点", "DEW", "露点"),
# ---- 系统 Complication含第三方 App 发布的)------------------------
# 这些走 Toybox.Complications见 source/Comp.mc。
(612, "NextCal", "Next Calendar Event", "下一个日程", "CAL", "日程"),
(626, "TrainStatus", "Training Status", "训练状态", "TRN", "训练"),
(627, "Race5k", "Race Time (5k)", "5k 完赛预测", "5K", "5K"),
(628, "Race10k", "Race Time (10k)", "10k 完赛预测", "10K", "10K"),
(629, "RaceHalf", "Race Time (Half)", "半马完赛预测", "HM", "半马"),
(630, "RaceFull", "Race Time (Marathon)", "全马完赛预测", "FM", "全马"),
(631, "Pace5k", "Pace (5k)", "5k 配速预测", "5K", "5K配速"),
(632, "Pace10k", "Pace (10k)", "10k 配速预测", "10K", "10K配速"),
(633, "PaceHalf", "Pace (Half)", "半马配速预测", "HM", "半马配速"),
(634, "PaceFull", "Pace (Marathon)", "全马配速预测", "FM", "全马配速"),
(641, "GolfScore", "Last Golf Round Score", "最近高尔夫得分", "GOLF", "高尔夫"),
(619, "RunWeek", "Run Distance (Week)", "本周跑步距离", "RUN", "跑步"),
(620, "BikeWeek", "Bike Distance (Week)", "本周骑行距离", "BIKE", "骑行"),
(695, "App1", "App 1 (on watch)", "第三方 App 1", "APP1", "App1"),
(696, "App2", "App 2 (on watch)", "第三方 App 2", "APP2", "App2"),
(697, "App3", "App 3 (on watch)", "第三方 App 3", "APP3", "App3"),
(698, "App4", "App 4 (on watch)", "第三方 App 4", "APP4", "App4"),
(699, "App5", "App 5 (on watch)", "第三方 App 5", "APP5", "App5"),
# ---- OpenWeatherMap -------------------------------------------------
# Fetched by the background service; see source/Owm.mc.
(500, "OwmTemp", "OWM Temperature", "OWM 气温", "TEMP", "气温"),
@@ -162,6 +191,11 @@ FIELDS = [
(522, "OwmMinMaxTmrw", "OWM Min/Max Tomorrow", "OWM 明日高低温", "TMRW", "明日"),
(535, "OwmDayAfter", "OWM Temp Day After", "OWM 后日气温", "D+2", "后日"),
(523, "OwmMinMaxD2", "OWM Min/Max Day After", "OWM 后日高低温", "D+2", "后日"),
(538, "OwmAqi", "OWM Air Quality (AQI)", "OWM 空气质量", "AQI", "空气"),
(539, "OwmAqiText", "OWM AQI Description", "OWM 空气质量描述", "AQI", "空气"),
(540, "OwmPm25", "OWM PM2.5", "OWM PM2.5", "PM2.5", "PM2.5"),
(541, "OwmPm10", "OWM PM10", "OWM PM10", "PM10", "PM10"),
(542, "OwmCo", "OWM CO", "OWM 一氧化碳", "CO", "CO"),
(516, "OwmCity", "OWM City Name", "OWM 城市", "CITY", "城市"),
(517, "OwmUpdated", "OWM Update Time", "OWM 更新时间", "OWM", "更新"),
@@ -198,6 +232,22 @@ OTHER_SETTINGS = """
<settingConfig type="numeric" min="-720" max="840"/>
</setting>
<setting propertyKey="@Properties.CountdownDate" title="@Strings.SettingCountdownDate">
<settingConfig type="alphaNumeric" maxLength="10"/>
</setting>
<setting propertyKey="@Properties.TimeFmt" title="@Strings.SettingTimeFmt">
<settingConfig type="alphaNumeric" maxLength="16"/>
</setting>
<setting propertyKey="@Properties.DateFmt1" title="@Strings.SettingDateFmt1">
<settingConfig type="alphaNumeric" maxLength="16"/>
</setting>
<setting propertyKey="@Properties.DateFmt2" title="@Strings.SettingDateFmt2">
<settingConfig type="alphaNumeric" maxLength="16"/>
</setting>
<setting propertyKey="@Properties.OwmKey" title="@Strings.SettingOwmKey">
<settingConfig type="alphaNumeric" maxLength="40"/>
</setting>