Compare commits

...

5 Commits

Author SHA1 Message Date
00bce1b6ab 新增 tools/install.sh:一条命令编译并侧载到手表
编译 release 版(enduro3 上 49.6 KB,debug 版是 168 KB),自动找已挂载的
GARMIN 卷复制到 GARMIN/APPS/。找不到卷时打印 .prg 路径和排查提示 —— macOS
上 fenix 7 之后的机型多数走 MTP,Finder 根本看不到这个盘,得用 OpenMTP 之类
的工具,这一点脚本里和 README 里都写了。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 21:16:01 +08:00
230180febf 新增 docs/:商店文案、图标与各机型截图
Connect IQ Store 后台要填的素材都放进 docs/,代码不依赖其中任何文件。

截图
  八个屏宽各一张(218/240/260/280/360/390/416/454),外加七套主题各一张。
  全部是模拟器的真实渲染 —— 字体是 Garmin 内置的 Roboto / Bionic,和真机一致。
  preview-themes.svg 用的是网页字体,不能拿去当商店截图,docs/README.md 里
  特别提了这点。

  ⚠️ 进度环是摆拍的。模拟器的 Edit Activity Monitor Info 只能改活动分钟、
  恢复时间、呼吸和压力,改不了步数/距离/卡路里(那几个输入框是 Java 自绘的
  表格,AppleScript 也够不着),所以环在模拟器里永远全灰。截图构建把四段弧
  固定成了设计稿的填充状态。除环之外画面上每个数字都是模拟器的真实取值。

  数据位用的是日出/高低温/心率/身体电量,不是默认的日出/高低温/距离/步数 ——
  后两项在模拟器里是 0,截出来底部是 "0.0 0"。这些都写进 docs/README.md 了。

文案
  中英文各一份,含名称、简介、详细描述、关键词、以及要向用户解释的四项权限。
  详细描述按「进度环 / 四个数据位 / 七套主题 / 常亮 / OWM 选用 / 其它」组织,
  156 项字段按分类列出。

图标
  store-icon-1024.png 与 512.png,与启动图标同一套视觉(渐变环 + 橙色日期带),
  由 tools/gen_icons.py 的 launcher() 渲染。

docs/README.md 还记了后台要填但不在仓库里的项(分类、价格、隐私政策 —— 启用
OWM 时会把坐标发给 api.openweathermap.org,这点建议在描述里写明),以及发布
前必须先解决的设计稿授权问题。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 12:21:22 +08:00
696b728c78 收紧常亮模式:去掉进度环,时间日期压暗到 38%
按要求把常亮再砍一档。常亮下现在不画:整条日期带底色、整个进度环、
四个数据位与电池;保留的时间、冒号、日期由 55% 压暗到 38%。

实测(fenix847mm,取表盘内区):

                  点亮像素数    平均亮度(功耗代理)
    普通模式         27.6%          14.1%
    常亮(改前)      10.4%           3.0%
    常亮(改后)       9.7%           2.5%

两个口径的差别值得记一笔:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,
对大号数字表盘天然不友好 —— 时间数字压到 38% 后白色仍有 ~97 亮度,照样计入。
AMOLED 的实际功耗更接近各子像素亮度之和,也就是「平均亮度」那一列。
两个口径现在都留了余量,38% 下时间和日期在模拟器里依然清晰可读。

LICENSE、UserProfile 的 try 保护、CHANGELOG 与版本号 1.1.0 已在上一个提交
22340b0 完成,本次只动常亮相关部分与对应文档。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:37:58 +08:00
22340b09fa 机型覆盖 17 → 55 款,补 LICENSE 与 CHANGELOG
机型
  新增 38 款圆屏 + CIQ ≥ 4.2 的机型:fenix 7 Pro 全系、Venu 2/3S、
  Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、
  FR255S/265S/570/955 等。其中 34 款的屏宽已有现成图标资源。

  机型列表和 monkey.jungle 改由 tools/gen_devices.py 扫描本机 SDK 生成,
  筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」(Complications 要 4.2)。
  55 条资源路径不再手写。

  新增 218px 与 360px 两档图标。218px 上图标只有 14×14,设计稿里太阳与云
  之间那道 1.7 设计像素的暗缝连一个物理像素都占不到,光栅化出来是一坨没有
  结构的橙块 —— 该尺寸改用按目标像素直接摆的简化标记。

常亮模式:结论是不改
  为了压所谓的 10% 点亮率,一度把进度环也砍掉了。换成平均亮度(AMOLED 功耗
  的合理代理)重新量之后发现:普通模式 14.1%,常亮 2.9%,本来就只有 1/5;
  砍掉环只从 3.01% 降到 2.91%。0.1 个百分点换掉一个可见特征不划算,环留着。
  两种度量的差异和这个决定都写进了代码注释和 README。

其它
  - LICENSE(MIT)。只覆盖代码 —— design/ 下的设计稿版权归第三方,
    发布前需取得原作者授权或替换视觉,文件内已注明。
  - UserProfile.getProfile() 加异常保护(这个 API 家族已经坑过三次)
  - CHANGELOG.md,版本号 1.0.0 → 1.1.0

验证:55 款全部编译通过(分三批 18+18+19);.iq 上架包可正常导出。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:27:54 +08:00
d9d45d983b 接入系统 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>
2026-09-10 09:03:21 +08:00
55 changed files with 1363 additions and 79 deletions

34
CHANGELOG.md Normal file
View File

@@ -0,0 +1,34 @@
# 更新记录
## 1.1.0
**机型覆盖 17 → 55 款**
新增 38 款圆屏、Connect IQ ≥ 4.2 的机型,包括 fenix 7 Pro 全系、Venu 2/3S、
Approach S50/S70、Descent G2/Mk3、D2 系列、Instinct 3 AMOLED、FR255S/265S/570/955 等。
机型列表与资源路径改由 `tools/gen_devices.py` 扫描本机 SDK 生成,不再手写。
新增两档图标尺寸218px、360px。218px 上设计稿的天气图标只有 14×14
太阳与云之间的暗缝不足一个像素,会糊成一块 —— 该尺寸改用专画的简化标记。
**常亮模式收紧**
砍掉整个进度环,时间/日期由 55% 压暗到 38%。实测点亮像素数 10.4% → 9.7%
平均亮度 3.0% → 2.5%(普通模式分别是 27.6% / 14.1%)。
**其它**
- 补 LICENSEMIT仅覆盖代码`design/` 下的设计稿版权归第三方,见文件内说明)
- `UserProfile.getProfile()` 加异常保护
- 版本号 1.0.0 → 1.1.0
## 1.0.0
`watchface-kit` 设计稿 1:1 还原的多指标数据表盘。
- 156 项可选数据字段,编号对齐 watchface.io 目录
- 七套主题,配色逐根刻度实测自设计稿
- 常亮AOD模式含防烧屏位移
- 简体中文 / 英文双语,表盘文字亦本地化
- OpenWeatherMap 接入(用户自备 API key后台定时拉取
- 系统 Complication 接入(日历、训练状态、完赛预测、第三方 App

29
LICENSE Normal file
View File

@@ -0,0 +1,29 @@
MIT License
Copyright (c) 2026 ericwyuan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---------------------------------------------------------------------------
注意:本许可只覆盖**本仓库的代码**。
design/ 目录下的设计稿watchface-*.svg、watchface-tokens.json、rebuild.py
来自第三方 watchface-kit版权归其原作者所有不在本许可范围内。
表盘的视觉呈现是对那套设计的 1:1 还原 —— 若要发布到 Connect IQ Store 或以
其它形式分发,需先取得原作者授权,或将视觉部分替换为原创设计。

100
README.md
View File

@@ -1,6 +1,6 @@
# Fenix 8 V3 — Garmin 数据表盘
`watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLEDFenix 8 / Epix 2 / Venu 3 等)与圆屏 MIPFenix 7 / Enduro 3 / FR255 等)共 17 款设备
`watchface-kit` 设计稿 1:1 还原的多指标数据表盘。设计画布 **500×500**,渲染时按屏宽等比缩放 `s = screenWidth / 500`,支持圆形 AMOLED 与圆屏 MIP 共 **55 款**设备Fenix 7/8 全系含 Pro、Epix 2、Enduro 3、Venu 2/3、FR 系列、Approach S50/S70、Descent、D2、Instinct 3 AMOLED 等)
- **SDK**Connect IQ SDK 9.1.0`monkeyc` 9.1.0`minSdkVersion="4.2.0"`
- **语言**Monkey C界面文案含英文与简体中文
@@ -24,6 +24,7 @@ fenix8v3-watchface/
├── tools/
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc
│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
│ ├── gen_devices.py # 扫描 SDK -> manifest 机型列表 + monkey.jungle
│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
├── source/
@@ -34,6 +35,7 @@ fenix8v3-watchface/
│ ├── Fields.mc # 125 项数据字段的取值与格式化
│ ├── Solar.mc # 日出/晨昏蒙影/黄金蓝调时刻的太阳高度角计算
│ ├── Owm.mc # OpenWeatherMap 缓存读取
│ ├── Comp.mc # 系统 / 第三方 Complication 桥接
│ ├── Fenix8V3Background.mc # 后台服务:拉取 OWM
│ ├── Settings.mc # 所有设置项的读取与默认值
│ ├── FieldTable.mc # ⚠️ 自动生成:字段 id 表
@@ -42,16 +44,21 @@ fenix8v3-watchface/
├── resources-chn/ # 简体中文文案
├── resources-icons-<宽度>/ # ⚠️ 自动生成:按屏幕尺寸光栅化的图标
├── resources-launcher-<尺寸>/ # ⚠️ 自动生成:各尺寸启动图标
├── docs/ # 上架素材:商店文案、图标、各机型截图
└── bin/ # 编译产物(已 gitignore
```
重新生成全部派生文件:
```bash
python3 tools/gen_themes.py && python3 tools/gen_fields.py \
python3 tools/gen_devices.py \
&& python3 tools/gen_themes.py && python3 tools/gen_fields.py \
&& python3 tools/gen_icons.py && python3 tools/gen_preview_svg.py
```
`gen_devices.py` 要先跑 —— 它决定了需要哪些图标尺寸,`gen_icons.py`
`WIDTHS` / `LAUNCHER` 两张表必须跟它对齐,少一档就会有机型编译不过。
三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。
---
@@ -181,21 +188,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 +242,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 +272,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()`。
@@ -255,9 +289,18 @@ AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有
- 时间与日期压暗到 55%,其余数据位全部隐藏
- 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮
只在 `DeviceSettings.requiresBurnInProtection` 为真时启用MIP 机型Fenix 7 / Enduro 3 等)不受影响,照常全量绘制。
只在 `DeviceSettings.requiresBurnInProtection` 为真时启用MIP 机型不受影响,照常全量绘制。
模拟器可用 `Settings → Display Mode` 切换验证。
实测fenix847mm取表盘内区
| | 点亮像素数 | 平均亮度(功耗代理) |
|---|---|---|
| 普通模式 | 27.6% | 14.1% |
| 常亮模式 | **9.7%** | **2.5%** |
常亮下砍掉的东西:整条日期带底色、**整个进度环**、四个数据位与电池;保留的时间、冒号、日期压暗到 38%。
两个口径要分清「点亮像素数」把每个抗锯齿边缘像素都算成亮的对大号数字表盘天然不友好AMOLED 的功耗更接近各子像素亮度之和,也就是表里的「平均亮度」。本实现两个口径都留了余量。
---
@@ -300,7 +343,9 @@ for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm fenix8solar
done
```
屏幕分辨率跨度较大(fenix7s 240×240 至 fenix847mm / venu3 454×454全部按 `s = width/500` 缩放。
屏幕分辨率跨度较大(FR255S 218×218 至 454×454 共 8 档),全部按 `s = width/500` 缩放。
机型列表和 `monkey.jungle``tools/gen_devices.py` 扫描本机已安装的 SDK 设备生成:筛选条件是「支持 watchFace + 圆屏 + CIQ ≥ 4.2」Complications 需要 4.2)。**注意结果取决于本机装了哪些设备**,跑之前先确认 SDK 设备装全了,脚本会打印找到的数量。
---
@@ -313,7 +358,22 @@ monkeydo bin/Fenix8V3_fenix847mm.prg fenix847mm # 载入表盘
非 release 编译会同时产出 `bin/*-settings.json`,可在模拟器 `View → Settings` 载入以实时切换主题与指标。注意模拟器会**持久化**已保存的设置,改 `properties.xml` 的默认值对已运行过的实例不生效,需先重置应用数据。
真机侧载:用 Garmin Express 或 Connect 手机 App 导入对应机型的 `bin/Fenix8V3_<device>.prg`,或把 `.prg` 拷入手表 `/GARMIN/Apps/`
### 侧载到真机
```bash
tools/install.sh # 默认 enduro3
tools/install.sh fenix847mm # 指定机型id 见 manifest.xml
```
脚本会编译 release 版并自动找已挂载的 GARMIN 卷复制过去。找不到卷时会打印
`.prg` 的路径和排查提示。
手动做的话就是:编译出 `bin/Fenix8V3_<机型>.prg`,拷进手表的 `GARMIN/APPS/`
推出后拔线,表上长按 UP/MENU → 表盘 → 选 Fenix 8 V3。再长按 MENU 进表盘设置。
> macOS 上 fenix 7 之后的机型多数走 MTPFinder 看不到这个盘。装
> [OpenMTP](https://openmtp.ganeshrvel.com)(免费)或 Android File Transfer
> 就能拖文件了。
---
@@ -336,6 +396,16 @@ python3 tools/gen_preview_svg.py
---
## 12.5 上架素材
商店后台要填的文案、图标和截图都在 `docs/`,见 `docs/README.md`
截图是模拟器的真实渲染Garmin 内置字体),八个屏宽各一张,外加七套主题各一张。
其中**进度环是摆拍的** —— 模拟器灌不进步数和距离,环会全灰,具体说明在
`docs/README.md` 里写清楚了。
---
## 13. 设计稿版权
`design/` 内的 SVG、tokens 与 `rebuild.py` 来自 `watchface-kit`,版权归原作者所有。本仓库是按该设计稿实现的 Monkey C 表盘。

72
docs/README.md Normal file
View File

@@ -0,0 +1,72 @@
# 上架素材
Connect IQ Store 后台要填的东西都在这个目录里。代码不依赖这里的任何文件。
```
docs/
├── store-listing-zh.md 中文商店文案(名称 / 简介 / 详细描述 / 关键词 / 权限说明)
├── store-listing-en.md 英文商店文案
├── store-icon-1024.png 商店图标(大)
├── store-icon-512.png 商店图标(小)
└── screenshots/
├── 218x218.png ~ 454x454.png 八个屏宽各一张
└── theme-01-ember.png ~ theme-07-kelp.png 七套主题454px
```
## 截图是怎么来的
全部是 **Connect IQ 模拟器的真实渲染**,不是网页预览图 —— 字体是 Garmin 内置的
Roboto / Bionic和真机一致。`preview-themes.svg` 那张用的是网页字体,**不要**拿去
当商店截图。
每个屏宽各挑了一台代表机型:
| 尺寸 | 机型 | 尺寸 | 机型 |
|------|------|------|------|
| 218 | FR255S | 360 | Venu 2S |
| 240 | fenix 7S | 390 | Vivoactive 5 |
| 260 | fenix 7 | 416 | fenix 8 43mm |
| 280 | Enduro 3 | 454 | fenix 8 47mm |
截图里显示的数据位是 **日出 / 最高最低气温 / 心率 / 身体电量** ——
这四项在模拟器里有真实取值,不是默认配置(默认是日出 / 高低温 / 距离 / 步数)。
换掉后两项是因为模拟器灌不进步数和距离,默认配置截出来底部是 `0.0``0`
### ⚠️ 进度环是摆拍的
模拟器的「Edit Activity Monitor Info」对话框只能改活动分钟、恢复时间、呼吸和
压力,**改不了步数 / 距离 / 卡路里**(那几个输入框是 Java 自绘的表格,
脚本也够不着)。所以进度环在模拟器里永远是全灰的,截图没法看。
截图构建把四段弧固定成了设计稿的填充状态(右上 21/21、左上 10/21、
右下 9/21、左下 14/21。**除进度环外,画面上每个数字都是模拟器的真实取值**
没有伪造。
如果要出完全真实的截图,把 `.prg` 侧载到真机戴一天再拍。
## 重新生成
图标:
```bash
python3 tools/gen_icons.py # 顺带产出 store-icon 用的同款视觉
```
截图:本目录下的 PNG 是用模拟器抓的,流程见提交记录里的 `shot.sh` / `theme.sh`
(脚本在临时目录,没有入库 —— 它们依赖具体的窗口位置,换台机器就要重调)。
手工重做的话:编译 → `monkeydo` 载入 → 截模拟器窗口 → 按日期带定位裁出表盘圆形区。
## 后台还要填但不在这里的
- **分类**Watch Faces
- **支持的机型**55 款,以 `manifest.xml``<iq:products>` 为准
- **价格**:免费 / 付费自行决定
- **隐私政策**:如果启用 OpenWeatherMap需要说明会把**坐标**发给
`api.openweathermap.org`。不填 key 时完全不联网,这一点建议在描述里写清楚
- **联系邮箱 / 支持链接**
## ⚠️ 发布前必须先解决
表盘的视觉是对第三方 `watchface-kit` 设计稿的 1:1 还原(见仓库根目录的
`LICENSE``README.md` 第 13 节)。**以自己的名义上架前,需要先取得原作者
授权,或把视觉部分替换成原创设计。** 这是法律问题,不是技术问题。

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
docs/store-icon-1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
docs/store-icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

90
docs/store-listing-en.md Normal file
View File

@@ -0,0 +1,90 @@
# Connect IQ Store listing (English)
## App name
```
Fenix 8 V3
```
## Short description
```
A dense data watch face: four progress arcs, four configurable slots, 156 data
fields to choose from, seven colour themes.
```
## Full description
```
Fenix 8 V3 packs a lot of information onto the dial without making it feel
crowded.
■ Four progress arcs
The rim is split into four independent arcs. Each one tracks a metric of your
choosing - steps, calories, distance, floors, move bar, active minutes or body
battery - so a glance tells you where the day stands. Every tick is
individually coloured, so the arc shifts hue as it fills.
■ Four data slots, 156 fields
Top-left, top-right, bottom-left and bottom-right. Each can show any of:
· Date & time seconds, UTC, two alternate time zones, week number
(ISO or calendar), custom time and date formats
· Activity steps, calories, distance, floors, ascent, active minutes,
plus week-to-date, 7-day and 28-day totals
· Body heart rate, resting HR, stress, pulse ox, respiration,
body battery, weight, BMI, recovery time, VO2 max
· System battery, battery in days, alarms, notifications, do not
disturb, bluetooth, solar intensity
· Environment altitude, pressure, sunrise/sunset, next sun event, moon age
and illumination, civil/nautical/astronomical twilight,
golden hour, blue hour
· Weather temperature, feels-like, high/low, humidity, precipitation,
wind speed and direction, UV index, visibility, dew point
· From watch next calendar event, training status, race and pace
predictors (5k/10k/half/marathon), weekly run and bike
distance, and complications published by your other apps
· Custom three custom text lines, event countdown
The captions under the bottom two slots follow whatever you pick - no separate
setting to keep in sync.
■ Seven themes
Ember, Aurora, Brass, Voltage, Reef, Acid and Kelp. Each carries its own arc
gradient, date band colours and time-digit gradient.
■ Always-on display
On AMOLED watches the face switches to a low-power layout while asleep: the
date band fill and the progress ring are dropped, the time and date are dimmed,
and the whole layout drifts a few pixels each minute to avoid burn-in.
■ OpenWeatherMap (optional)
Add your own OpenWeatherMap API key for 30 further weather fields, including
air quality (AQI, PM2.5, PM10, CO) and the next two days' forecast. Without a
key the watch face never touches the network.
■ Also
· English and Simplified Chinese - weekday, month and field captions all follow
the watch language
· Temperature unit can be forced to Celsius or Fahrenheit; distance and
12/24-hour follow the watch
· The bluetooth glyph dims when the phone is out of range
```
## Keywords / tags
```
data field, progress ring, complications, always-on, AMOLED, weather,
heart rate, body battery, sunrise sunset, chinese
```
## Permissions to explain to users
```
· UserProfile - resting heart rate, weight, VO2 max
· SensorHistory - pulse ox, body battery
· Communications + Background - only used to fetch weather, and only when an
OpenWeatherMap API key has been entered
· ComplicationSubscriber - calendar, training status, and data published by
other apps
```

75
docs/store-listing-zh.md Normal file
View File

@@ -0,0 +1,75 @@
# Connect IQ Store 上架文案(简体中文)
## 应用名称
```
Fenix 8 V3
```
## 一句话简介(短描述)
```
多指标数据表盘:四段进度环 + 四个可自由配置的数据位156 项数据任选,七套配色。
```
## 详细描述
```
Fenix 8 V3 是一款信息密度高、但不拥挤的数据表盘。
■ 四段进度环
表盘外圈是四段独立的进度弧,各自绑定一项指标(步数、卡路里、距离、楼层、
动动条、活动分钟、身体电量),一眼就能看出今天的完成度。每根刻度的颜色都
是渐变的,进度越高颜色越靠近弧段末端的色相。
■ 四个数据位156 项数据任选
左上、右上、左下、右下四格,每格都能从 156 项数据里任选:
· 时间日期 秒、UTC、第二时区 ×2、周数ISO / 日历)、自定义时间与日期格式
· 活动 步数、卡路里、距离、楼层、爬升、活动分钟,以及本周 / 近 7 天 /
近 28 天累计
· 身体 心率、静息心率、压力、血氧、呼吸、身体电量、体重、BMI、
恢复时间、最大摄氧量
· 系统 电量、剩余天数、闹钟、通知、勿扰、蓝牙、太阳能强度
· 环境 海拔、气压、日出日落、下一太阳事件、月龄月相,
以及民用 / 航海 / 天文晨昏蒙影、黄金时刻、蓝调时刻
· 天气 气温、体感、最高最低、湿度、降水概率、风速风向、紫外线、
能见度、露点
· 手表数据 下一个日程、训练状态、完赛与配速预测5k/10k/半马/全马)、
本周跑步与骑行距离、以及其它 App 发布的复杂功能
· 自定义 三段自定义文字、事件倒计时
底部两格的小标签会跟着所选数据自动变化,不用另外设置。
■ 七套配色
余烬、极光、黄铜、电压、珊瑚礁、酸绿、海藻。每套都有自己的进度环渐变、
日期带配色和时间数字渐变。
■ 常亮显示
AMOLED 机型支持常亮。常亮时自动切换到低功耗版面:去掉日期带底色和进度环,
时间与日期压暗,整个画面每分钟在几个像素内轻微游走以避免烧屏。
■ OpenWeatherMap选用
填入自己的 OpenWeatherMap API key 后,可额外获得 30 项天气数据,包括空气质量
AQI、PM2.5、PM10、CO与未来两日预报。不填则完全不联网。
■ 其它
· 简体中文 / 英文双语,表盘上的星期、月份、数据标签都会跟随手表语言
· 温度单位可强制摄氏或华氏,距离与时间制式跟随手表设置
· 手机断连时蓝牙图标自动压暗
```
## 关键词 / 标签
```
数据表盘, 进度环, 数据字段, 常亮, AMOLED, 天气, 心率, 身体电量, 日出日落, 中文
```
## 需要向用户说明的权限
```
· 用户档案 —— 用于静息心率、体重、最大摄氧量
· 传感器历史 —— 用于血氧、身体电量
· 通信与后台 —— 仅在填写了 OpenWeatherMap API key 时用于拉取天气
· 复杂功能订阅 —— 用于日程、训练状态、以及其它 App 发布的数据
```

View File

@@ -7,26 +7,64 @@
minSdkVersion="4.2.0"
name="@Strings.AppName"
type="watchface"
version="1.0.0">
version="1.1.0">
<iq:products>
<iq:product id="approachs50"/>
<iq:product id="approachs7042mm"/>
<iq:product id="approachs7047mm"/>
<iq:product id="d2airx10"/>
<iq:product id="d2mach1"/>
<iq:product id="d2mach2"/>
<iq:product id="descentg2"/>
<iq:product id="descentmk343mm"/>
<iq:product id="descentmk351mm"/>
<iq:product id="enduro3"/>
<iq:product id="epix2"/>
<iq:product id="epix2pro42mm"/>
<iq:product id="epix2pro47mm"/>
<iq:product id="epix2pro51mm"/>
<iq:product id="fenix7"/>
<iq:product id="fenix7pro"/>
<iq:product id="fenix7pronowifi"/>
<iq:product id="fenix7s"/>
<iq:product id="fenix7spro"/>
<iq:product id="fenix7x"/>
<iq:product id="fenix7xpro"/>
<iq:product id="fenix7xpronowifi"/>
<iq:product id="fenix843mm"/>
<iq:product id="fenix847mm"/>
<iq:product id="fenix8pro47mm"/>
<iq:product id="fenix8solar47mm"/>
<iq:product id="fenix8solar51mm"/>
<iq:product id="epix2"/>
<iq:product id="epix2pro47mm"/>
<iq:product id="epix2pro51mm"/>
<iq:product id="fenix7"/>
<iq:product id="fenix7x"/>
<iq:product id="fenix7s"/>
<iq:product id="fr965"/>
<iq:product id="fr265"/>
<iq:product id="fenixe"/>
<iq:product id="fr165"/>
<iq:product id="fr165m"/>
<iq:product id="fr255"/>
<iq:product id="fr255m"/>
<iq:product id="fr255s"/>
<iq:product id="fr255sm"/>
<iq:product id="fr265"/>
<iq:product id="fr265s"/>
<iq:product id="fr57042mm"/>
<iq:product id="fr57047mm"/>
<iq:product id="fr955"/>
<iq:product id="fr965"/>
<iq:product id="fr970"/>
<iq:product id="instinct3amoled45mm"/>
<iq:product id="instinct3amoled50mm"/>
<iq:product id="instinctcrossoveramoled"/>
<iq:product id="marq2"/>
<iq:product id="marq2aviator"/>
<iq:product id="venu2"/>
<iq:product id="venu2plus"/>
<iq:product id="venu2s"/>
<iq:product id="venu3"/>
<iq:product id="venu3s"/>
<iq:product id="venu441mm"/>
<iq:product id="venu445mm"/>
<iq:product id="vivoactive5"/>
<iq:product id="vivoactive6"/>
</iq:products>
<iq:languages>
@@ -40,6 +78,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

@@ -2,31 +2,68 @@ project.manifest = manifest.xml
base.sourcePath = source
# resources-chn holds the Simplified Chinese variant of every string; the
# -chn suffix is what tells monkeyc which language it belongs to.
# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。
base.resourcePath = resources;resources-chn
# Two more resource trees per device, both generated by tools/gen_icons.py:
# resources-icons-<screen width> sunrise/sunset/weather rasterised from the
# design paths for that exact pixel grid
# resources-launcher-<size> launcher icon at the size the device asks for
# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:
# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标
# resources-launcher-<尺寸> 该机型要求尺寸的启动图标
# 这一整段由 tools/gen_devices.py 生成,不要手改。
approachs50.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
approachs7042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
approachs7047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
d2airx10.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
d2mach1.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
d2mach2.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
descentg2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
descentmk343mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
descentmk351mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
epix2pro42mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix7pro.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix7pronowifi.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix7s.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
fenix7spro.resourcePath = $(base.resourcePath);resources-icons-240;resources-launcher-40
fenix7x.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
fenix7xpro.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
fenix7xpronowifi.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fenix8pro47mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
fenixe.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
fr165.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
fr165m.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
fr255.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fr255m.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fr255s.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40
fr255sm.resourcePath = $(base.resourcePath);resources-icons-218;resources-launcher-40
fr265.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
fr265s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-60
fr57042mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
fr57047mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fr955.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fr965.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fr970.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
instinct3amoled45mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
instinct3amoled50mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
instinctcrossoveramoled.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-38
marq2.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
marq2aviator.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-60
venu2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
venu2plus.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-70
venu2s.resourcePath = $(base.resourcePath);resources-icons-360;resources-launcher-61
venu3.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-70
venu3s.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-70
venu441mm.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
venu445mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
vivoactive6.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
base.excludeAnnotations = release

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

@@ -0,0 +1,5 @@
<drawables>
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
</drawables>

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 153 B

View File

@@ -0,0 +1,5 @@
<drawables>
<bitmap id="IconSunrise" filename="sunrise.png" packingFormat="png"/>
<bitmap id="IconSunset" filename="sunset.png" packingFormat="png"/>
<bitmap id="IconWeather" filename="weather.png" packingFormat="png"/>
</drawables>

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

View File

@@ -0,0 +1,3 @@
<drawables>
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
</drawables>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,3 @@
<drawables>
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
</drawables>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,3 @@
<drawables>
<bitmap id="LauncherIcon" filename="launcher_icon.png"/>
</drawables>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

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;
// ============================================================================
// 表盘主体
@@ -279,45 +280,25 @@ class Fenix8V3View extends WatchUi.WatchFace {
// ------------------------------- 常亮模式 ------------------------------
// 低功耗画面。相比正常画面砍掉的东西:
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
// - 未点亮的刻度(只画有数据的那部分,且压暗到 45%
// - **整个进度环**
// - 时间、冒号、日期压暗到 38%
//
// 度量口径要注意:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,和实际
// 功耗对不上AMOLED 功耗更接近各子像素亮度之和(平均亮度)。
// 本实现两个口径都留了余量,实测数字见 README 的常亮小节。
// - 四个数据位、电池(全部隐藏)
// 保留的时间和日期压暗到 55%。
//
// 另外整个画面按分钟在 ±4 像素内游走,避免同一个像素长期点亮导致烧屏。
// pTR/pTL/pBR/pBL 现在用不上(常亮下不画环),保留形参是为了以后要把环
// 加回来时不必改调用点。
function drawAmbient(dc as Dc, ti as Number, ds as System.DeviceSettings?,
pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void {
var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT);
var ox = ((g.min % 5) - 2) * 2.0 / Layout.scale; // ±4 设备像素,换算回设计像素
var oy = ((g.min / 5) % 5 - 2) * 2.0 / Layout.scale;
var top = Themes.RINGTOP[ti] as Array<Number>;
var bot = Themes.RINGBOTTOM[ti] as Array<Number>;
var litTR = (pTR * Layout.TOP_N).toNumber();
var litTL = (pTL * Layout.TOP_N).toNumber();
var litBR = (pBR * Layout.BOT_N).toNumber();
var litBL = (pBL * Layout.BOT_N).toNumber();
for (var i = 0; i < Layout.TOP_N; i++) {
var a = Layout.TOP_PH + Layout.TOP_PITCH * i;
if (i >= Layout.TOP_N - litTR) {
dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, a, Layout.TOP_HW);
}
if (i >= Layout.TOP_N - litTL) {
dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, -a, Layout.TOP_HW);
}
}
for (var i = 0; i < Layout.BOT_N; i++) {
var a = Layout.BOT_PH + Layout.BOT_PITCH * i;
if (i < litBL) {
dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, 180.0 + a, Layout.BOT_HW);
}
if (i < litBR) {
dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT);
tick(dc, 180.0 - a, Layout.BOT_HW);
}
}
// 进度环在常亮下不画 —— 它是点亮像素数的大头。
var h = g.hour;
if (ds == null || !ds.is24Hour) {
@@ -327,17 +308,17 @@ class Fenix8V3View extends WatchUi.WatchFace {
var mins = Themes.MINUTES[ti] as Array<Number>;
Layout.text(dc, Layout.HOURS_R + ox, Layout.TIME_CY + oy, Layout.fontTime,
h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT,
dim(hours[hours.size() / 2], 0.55));
dim(hours[hours.size() / 2], 0.38));
Layout.text(dc, Layout.MINUTES_L + ox, Layout.TIME_CY + oy, Layout.fontTime,
g.min.format("%02d"), Graphics.TEXT_JUSTIFY_LEFT,
dim(mins[mins.size() / 2], 0.55));
dc.setColor(dim(Themes.COLON[ti] as Number, 0.55), Graphics.COLOR_TRANSPARENT);
dim(mins[mins.size() / 2], 0.38));
dc.setColor(dim(Themes.COLON[ti] as Number, 0.38), Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(Layout.px(Layout.COLON_X + ox), Layout.px(Layout.COLON_Y1 + oy),
Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H));
dc.fillRectangle(Layout.px(Layout.COLON_X + ox), Layout.px(Layout.COLON_Y2 + oy),
Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H));
var band = dim(Themes.ACCENT[ti] as Number, 0.55);
var band = dim(Themes.ACCENT[ti] as Number, 0.38);
Layout.text(dc, Layout.DOW_CX + ox, Layout.BAND_CY + oy, Layout.fontBand,
Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, band);
Layout.text(dc, Layout.MD_CX + ox, Layout.BAND_CY + oy, Layout.fontBand,
@@ -429,6 +410,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 {
@@ -160,8 +244,18 @@ module Fields {
return null;
}
// 本项目已经被这个 API 家族坑过三次Properties.getValue、
// deleteTemporalEvent、Complication.unit一律按「声明了权限也可能抛」
// 来防。取不到就当作没有用户档案,相关字段显示 "--"。
function profile() as UserProfile.Profile? {
if (!mGotProf) { mProf = UserProfile.getProfile(); mGotProf = true; }
if (!mGotProf) {
try {
mProf = UserProfile.getProfile();
} catch (e) {
mProf = null;
}
mGotProf = true;
}
return mProf;
}
@@ -375,6 +469,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 +553,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 +871,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 +891,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 +925,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;

118
tools/gen_devices.py Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""扫描本机安装的 Connect IQ 设备,重写 manifest 的机型列表和 monkey.jungle。
筛选条件(三条都要满足):
1. 支持 watchFace 类型
2. Connect IQ >= 4.2 —— Toybox.Complications 是这个版本引入的,
本表盘的日历/训练状态/第三方 App 字段依赖它
3. 圆屏deviceFamily 以 round- 开头)
python3 tools/gen_devices.py
改写两处:
manifest.xml <iq:products> 整块
monkey.jungle 每台设备的 resourcePath图标按屏宽、启动图标按各自要求的尺寸
⚠️ 结果取决于本机 SDK 里**装了哪些设备**。如果某台机器上少装了几款,
跑一遍会把它们从 manifest 里删掉。所以脚本会打印找到的数量,
提交前确认一下数字对不对(当前应为 55 款)。
图标资源的尺寸集合由本脚本算出后交给 tools/gen_icons.py 使用,
两者的 WIDTHS / LAUNCHER 必须一致,所以改完这里要跟着跑一遍 gen_icons.py。
"""
import json
import os
import re
from collections import defaultdict
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
DEV = os.path.expanduser(
"~/Library/Application Support/Garmin/ConnectIQ/Devices")
def eligible():
"""返回 [(设备id, 屏宽, 启动图标尺寸, 是否支持alpha), ...],按 id 排序。"""
out = []
if not os.path.isdir(DEV):
raise SystemExit("找不到设备目录:%s" % DEV)
for d in sorted(os.listdir(DEV)):
p = os.path.join(DEV, d, "compiler.json")
if not os.path.exists(p):
continue
try:
c = json.load(open(p))
except Exception:
continue
types = {t["type"] for t in c.get("appTypes", []) if isinstance(t, dict)}
if "watchFace" not in types:
continue
if not str(c.get("deviceFamily", "")).startswith("round"):
continue
vers = [str(pn.get("connectIQVersion", "0"))
for pn in c.get("partNumbers", [])]
if not any(_ciq_ok(v) for v in vers):
continue
li = c.get("launcherIcon", {})
out.append((d, c["resolution"]["width"], li.get("width"),
bool(c.get("alphaBlendingSupport"))))
return out
def _ciq_ok(v):
try:
a = [int(x) for x in v.split(".")[:2]]
return a[0] > 4 or (a[0] == 4 and a[1] >= 2)
except Exception:
return False
def write_manifest(devs):
p = os.path.join(ROOT, "manifest.xml")
s = open(p).read()
body = "\n".join(' <iq:product id="%s"/>' % d for d, _, _, _ in devs)
block = " <iq:products>\n%s\n </iq:products>" % body
s = re.sub(r" <iq:products>.*?</iq:products>", block, s, flags=re.S)
open(p, "w").write(s)
print("manifest.xml: 写入 %d 款机型" % len(devs))
def write_jungle(devs):
L = ["project.manifest = manifest.xml", "",
"base.sourcePath = source", "",
"# resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。",
"base.resourcePath = resources;resources-chn", "",
"# 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:",
"# resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标",
"# resources-launcher-<尺寸> 该机型要求尺寸的启动图标",
"# 这一整段由 tools/gen_devices.py 生成,不要手改。",
""]
for d, w, li, _ in devs:
L.append("%s.resourcePath = $(base.resourcePath)"
";resources-icons-%d;resources-launcher-%d" % (d, w, li))
L += ["", "base.excludeAnnotations = release"]
open(os.path.join(ROOT, "monkey.jungle"), "w").write("\n".join(L) + "\n")
print("monkey.jungle: 写入 %d 条资源路径" % len(devs))
def main():
devs = eligible()
if not devs:
raise SystemExit("一款符合条件的机型都没找到,请检查 SDK 设备是否已安装")
widths = defaultdict(set)
for _, w, _, alpha in devs:
widths[w].add(alpha)
print("符合条件的机型:%d" % len(devs))
print("需要的图标屏宽:%s" % sorted(widths))
print("需要的启动图标尺寸:%s" % sorted({li for _, _, li, _ in devs}))
for w in sorted(widths):
if len(widths[w]) > 1:
print(" ⚠️ %dpx 同时存在支持/不支持 alpha 的机型,"
"图标按硬边生成" % w)
write_manifest(devs)
write_jungle(devs)
print("\n接着跑python3 tools/gen_icons.py")
if __name__ == "__main__":
main()

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>

View File

@@ -41,8 +41,11 @@ ICONS = {
"weather": ("weather-icon", 343.55, 89.5, False),
}
# screen width -> whether the panel can blend alpha (MIP cannot)
WIDTHS = {240: False, 260: False, 280: False, 390: True, 416: True, 454: True}
# 屏宽 -> 该尺寸下的屏幕能否做 alpha 混合(MIP 屏不能)。
# 这张表和启动图标尺寸表都由 tools/gen_devices.py 扫描出的机型集合决定,
# 改完设备列表要跟着更新这里 —— 少一个尺寸就会有机型编译不过。
WIDTHS = {218: False, 240: False, 260: False, 280: False,
360: True, 390: True, 416: True, 454: True}
SS = 8 # supersampling factor
@@ -118,8 +121,8 @@ def render(paths, size, cx, cy, blend):
return img
# Launcher icon sizes the target devices ask for.
LAUNCHER = [40, 56, 60, 65, 70]
# 各机型要求的启动图标尺寸Garmin 每款表都有自己的规定值,不能用缩放糊弄)。
LAUNCHER = [38, 40, 54, 56, 60, 61, 65, 70]
def launcher(size):
@@ -160,6 +163,46 @@ def launcher(size):
return img.resize((size, size), Image.LANCZOS)
def tiny_weather(size):
"""给 16px 以下的屏幕专画的天气标记。
设计稿的天气图标是「太阳被云挡住一半」,靠一道暗缝把两者分开。那道缝在
500 画布上只有 1.7px,到 218px 屏(图标 14×14连一个物理像素都占不到
光栅化出来就是一坨没有结构的橙块。
所以这个尺寸不再走设计稿路径,改用按目标像素直接摆的简化标记:
右上一个太阳圆盘,左下一朵云,中间强制留 1 个像素的空隙。
形不如原图,但至少还认得出是「天气」。
"""
from PIL import ImageDraw
ss = 8
n = size * ss
img = Image.new("L", (n, n), 0)
d = ImageDraw.Draw(img)
u = n / 14.0 # 以 14 格为基准摆放,任何尺寸都等比
def disc(cx, cy, r, fill):
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=fill)
# 太阳 + 四道光芒(光芒画成小方块,比线段在低分辨率下更容易留下来)
disc(9.2 * u, 4.6 * u, 2.7 * u, 255)
for dx, dy in ((0, -4.4), (4.3, 0), (-3.2, -3.2), (3.2, -3.2)):
d.rectangle([9.2 * u + dx * u - 0.55 * u, 4.6 * u + dy * u - 0.55 * u,
9.2 * u + dx * u + 0.55 * u, 4.6 * u + dy * u + 0.55 * u],
fill=255)
# 先用黑色把云的轮廓“撑大”一圈,留出与太阳之间的缝,再画回本体
for grow, fill in ((1.0 * u, 0), (0.0, 255)):
disc(5.0 * u, 8.6 * u, 3.3 * u + grow, fill)
disc(8.6 * u, 9.6 * u, 2.4 * u + grow, fill)
d.rectangle([1.7 * u - grow, 8.8 * u - grow, 11.0 * u + grow, 12.2 * u],
fill=fill)
small = img.resize((size, size), Image.LANCZOS)
small = small.point(lambda v: 255 if v >= 110 else 0)
out = Image.new("RGB", (size, size), (255, 255, 255)).convert("RGBA")
out.putalpha(small)
return out
def main():
svg = open(SRC).read()
for size in LAUNCHER:
@@ -182,6 +225,10 @@ def main():
if key not in cache:
p = subpaths(svg, pid)
cache[key] = flip_chevron(p) if flip else p
# 天气图标在极小尺寸下用专门的简化画法,日出/日落还撑得住。
if name == "weather" and size < 16:
img = tiny_weather(size)
else:
img = render(cache[key], size, cx, cy, blend)
img.save(os.path.join(outdir, "%s.png" % name))
names.append(name)

61
tools/install.sh Executable file
View File

@@ -0,0 +1,61 @@
#!/bin/bash
# 编译并侧载到通过 USB 接入的 Garmin 手表。
#
# tools/install.sh # 默认 enduro3
# tools/install.sh fenix847mm # 指定机型
#
# 机型 id 见 manifest.xml 的 <iq:product>。
#
# 前提:手表用数据线接到电脑,并且在 Finder 里能看到名为 GARMIN 的卷。
# 看不到的话见本文件末尾的说明。
set -e
DEV="${1:-enduro3}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SDK="$HOME/Library/Application Support/Garmin/ConnectIQ/Sdks"
MONKEYC="$(ls -d "$SDK"/*/bin/monkeyc 2>/dev/null | sort | tail -1)"
[ -z "$MONKEYC" ] && { echo "找不到 monkeyc请先装 Connect IQ SDK"; exit 1; }
[ -f "$ROOT/developer_key.der" ] || { echo "缺 developer_key.der"; exit 1; }
grep -q "\"$DEV\"\|id=\"$DEV\"" "$ROOT/manifest.xml" || {
echo "manifest.xml 里没有机型 '$DEV'"; exit 1; }
PRG="$ROOT/bin/Fenix8V3_${DEV}.prg"
echo "==> 编译 $DEVrelease"
mkdir -p "$ROOT/bin"
"$MONKEYC" -o "$PRG" -f "$ROOT/monkey.jungle" -y "$ROOT/developer_key.der" \
-d "$DEV" -r 2>&1 | tail -1
# Garmin 卷的名字通常就是 GARMIN个别机型会带型号后缀
VOL=""
for v in /Volumes/*; do
[ -d "$v/GARMIN" ] && VOL="$v" && break
done
if [ -z "$VOL" ]; then
cat <<'EOS'
==> 没找到手表。
在 Finder 里看不到 GARMIN 卷,通常是这两种情况之一:
1. 表没解锁 —— 部分机型插上后要在表上确认一次
2. 走的是 MTP 协议 —— macOS 的 Finder 不支持 MTP。fenix 7 之后的机型
多半是这种。装一个 OpenMTP免费https://openmtp.ganeshrvel.com
或 Android File Transfer就能像普通盘一样浏览手表了。
用 MTP 工具时,把下面这个文件拖到手表的 GARMIN/APPS/ 目录即可:
EOS
echo " $PRG"
exit 0
fi
DEST="$VOL/GARMIN/APPS"
mkdir -p "$DEST"
cp "$PRG" "$DEST/"
echo "==> 已复制到 $DEST/$(basename "$PRG")"
echo
echo "接下来:"
echo " 1. 在 Finder 里推出Eject手表这个卷再拔线"
echo " 2. 表上长按 UP/MENU -> 表盘 -> 找到 Fenix 8 V3"
echo " 3. 再长按 MENU 可以进这个表盘自己的设置,改主题和四个数据位"