机型覆盖 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>
This commit is contained in:
2026-09-10 11:27:54 +08:00
parent d9d45d983b
commit 22340b09fa
24 changed files with 374 additions and 25 deletions

29
CHANGELOG.md Normal file
View File

@@ -0,0 +1,29 @@
# 更新记录
## 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
太阳与云之间的暗缝不足一个像素,会糊成一块 —— 该尺寸改用专画的简化标记。
**其它**
- 补 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 或以
其它形式分发,需先取得原作者授权,或将视觉部分替换为原创设计。

View File

@@ -1,6 +1,6 @@
# Fenix 8 V3 — Garmin 数据表盘 # 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"` - **SDK**Connect IQ SDK 9.1.0`monkeyc` 9.1.0`minSdkVersion="4.2.0"`
- **语言**Monkey C界面文案含英文与简体中文 - **语言**Monkey C界面文案含英文与简体中文
@@ -24,6 +24,7 @@ fenix8v3-watchface/
├── tools/ ├── tools/
│ ├── gen_themes.py # design/*.svg -> source/Themes.mc │ ├── gen_themes.py # design/*.svg -> source/Themes.mc
│ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案 │ ├── gen_fields.py # 字段表 -> FieldTable.mc + settings.xml + 两种语言文案
│ ├── gen_devices.py # 扫描 SDK -> manifest 机型列表 + monkey.jungle
│ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标 │ ├── gen_icons.py # design/*.svg -> 各尺寸图标位图 + 启动图标
│ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html │ └── gen_preview_svg.py # source/Themes.mc -> preview-themes.svg + preview.html
├── source/ ├── source/
@@ -49,10 +50,14 @@ fenix8v3-watchface/
重新生成全部派生文件: 重新生成全部派生文件:
```bash ```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 && 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` 里那一张表。 三份渲染实现(表盘 / SVG 预览 / HTML 预览)只有 **一份颜色数据**`Themes.mc`。预览脚本直接解析它,不会与固件漂移。字段表、设置项、两种语言的文案同样只有 `tools/gen_fields.py` 里那一张表。
--- ---
@@ -283,7 +288,16 @@ AMOLED 机型在常亮模式下必须压低点亮像素,否则既费电又有
- 时间与日期压暗到 55%,其余数据位全部隐藏 - 时间与日期压暗到 55%,其余数据位全部隐藏
- 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮 - 整个画面按分钟在 ±4px 内游走,避免固定像素长期点亮
只在 `DeviceSettings.requiresBurnInProtection` 为真时启用MIP 机型Fenix 7 / Enduro 3 等)不受影响,照常全量绘制。 只在 `DeviceSettings.requiresBurnInProtection` 为真时启用MIP 机型不受影响,照常全量绘制。
实测fenix847mm取表盘内区
| | 点亮像素数 | 平均亮度(功耗代理) |
|---|---|---|
| 普通模式 | 27.6% | 14.1% |
| 常亮模式 | 10.4% | **2.9%** |
两个口径差别很大:「点亮像素数」把每个抗锯齿边缘像素都算成亮的,和实际功耗对不上。曾试过连进度环也砍掉,平均亮度只从 3.01% 降到 2.91% —— 0.1 个百分点换掉一个可见特征不划算,所以环保留。
模拟器可用 `Settings → Display Mode` 切换验证。 模拟器可用 `Settings → Display Mode` 切换验证。
@@ -328,7 +342,9 @@ for d in enduro3 fenix843mm fenix847mm fenix8pro47mm fenix8solar47mm fenix8solar
done 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 设备装全了,脚本会打印找到的数量。
--- ---

View File

@@ -7,26 +7,64 @@
minSdkVersion="4.2.0" minSdkVersion="4.2.0"
name="@Strings.AppName" name="@Strings.AppName"
type="watchface" type="watchface"
version="1.0.0"> version="1.1.0">
<iq:products> <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="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="fenix843mm"/>
<iq:product id="fenix847mm"/> <iq:product id="fenix847mm"/>
<iq:product id="fenix8pro47mm"/> <iq:product id="fenix8pro47mm"/>
<iq:product id="fenix8solar47mm"/> <iq:product id="fenix8solar47mm"/>
<iq:product id="fenix8solar51mm"/> <iq:product id="fenix8solar51mm"/>
<iq:product id="epix2"/> <iq:product id="fenixe"/>
<iq:product id="epix2pro47mm"/> <iq:product id="fr165"/>
<iq:product id="epix2pro51mm"/> <iq:product id="fr165m"/>
<iq:product id="fenix7"/>
<iq:product id="fenix7x"/>
<iq:product id="fenix7s"/>
<iq:product id="fr965"/>
<iq:product id="fr265"/>
<iq:product id="fr255"/> <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="venu3"/>
<iq:product id="venu3s"/>
<iq:product id="venu441mm"/>
<iq:product id="venu445mm"/>
<iq:product id="vivoactive5"/> <iq:product id="vivoactive5"/>
<iq:product id="vivoactive6"/>
</iq:products> </iq:products>
<iq:languages> <iq:languages>

View File

@@ -2,31 +2,68 @@ project.manifest = manifest.xml
base.sourcePath = source base.sourcePath = source
# resources-chn holds the Simplified Chinese variant of every string; the # resources-chn 是简体中文文案;目录名的 -chn 后缀就是语言标记。
# -chn suffix is what tells monkeyc which language it belongs to.
base.resourcePath = resources;resources-chn base.resourcePath = resources;resources-chn
# Two more resource trees per device, both generated by tools/gen_icons.py: # 下面每台设备再挂两棵资源树,都由 tools/gen_icons.py 生成:
# resources-icons-<screen width> sunrise/sunset/weather rasterised from the # resources-icons-<屏宽> 按该屏幕像素网格光栅化的日出/日落/天气图标
# design paths for that exact pixel grid # resources-launcher-<尺寸> 该机型要求尺寸的启动图标
# resources-launcher-<size> launcher icon at the size the device asks for # 这一整段由 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 enduro3.resourcePath = $(base.resourcePath);resources-icons-280;resources-launcher-40
epix2.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60 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 epix2pro47mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60 epix2pro51mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-60
fenix7.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40 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 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 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 fenix843mm.resourcePath = $(base.resourcePath);resources-icons-416;resources-launcher-60
fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65 fenix847mm.resourcePath = $(base.resourcePath);resources-icons-454;resources-launcher-65
fenix8pro47mm.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 fenix8solar47mm.resourcePath = $(base.resourcePath);resources-icons-260;resources-launcher-40
fenix8solar51mm.resourcePath = $(base.resourcePath);resources-icons-280;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 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 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 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 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 vivoactive5.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-56
vivoactive6.resourcePath = $(base.resourcePath);resources-icons-390;resources-launcher-54
base.excludeAnnotations = release base.excludeAnnotations = release

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

@@ -281,6 +281,12 @@ class Fenix8V3View extends WatchUi.WatchFace {
// 低功耗画面。相比正常画面砍掉的东西: // 低功耗画面。相比正常画面砍掉的东西:
// - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画) // - 日期带底色(整条亮橙色横幅是最费电的元素,直接不画)
// - 未点亮的刻度(只画有数据的那部分,且压暗到 45% // - 未点亮的刻度(只画有数据的那部分,且压暗到 45%
//
// 关于点亮量实测普通模式的平均亮度AMOLED 功耗的合理代理)是 14.1%
// 常亮是 2.9%,约 1/5。曾经试过连进度环也砍掉平均亮度只从 3.01% 降到
// 2.91% —— 0.1 个百分点换掉一个可见特征,不划算,所以环留着。
// (常按「点亮像素数」算会得到 10% 左右,但那个口径把每个抗锯齿边缘像素
// 都算成「亮」,跟实际功耗对不上。)
// - 四个数据位、电池(全部隐藏) // - 四个数据位、电池(全部隐藏)
// 保留的时间和日期压暗到 55%。 // 保留的时间和日期压暗到 55%。
// //

View File

@@ -244,8 +244,18 @@ module Fields {
return null; return null;
} }
// 本项目已经被这个 API 家族坑过三次Properties.getValue、
// deleteTemporalEvent、Complication.unit一律按「声明了权限也可能抛」
// 来防。取不到就当作没有用户档案,相关字段显示 "--"。
function profile() as UserProfile.Profile? { 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; return mProf;
} }

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

@@ -41,8 +41,11 @@ ICONS = {
"weather": ("weather-icon", 343.55, 89.5, False), "weather": ("weather-icon", 343.55, 89.5, False),
} }
# screen width -> whether the panel can blend alpha (MIP cannot) # 屏宽 -> 该尺寸下的屏幕能否做 alpha 混合(MIP 屏不能)。
WIDTHS = {240: False, 260: False, 280: False, 390: True, 416: True, 454: True} # 这张表和启动图标尺寸表都由 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 SS = 8 # supersampling factor
@@ -118,8 +121,8 @@ def render(paths, size, cx, cy, blend):
return img return img
# Launcher icon sizes the target devices ask for. # 各机型要求的启动图标尺寸Garmin 每款表都有自己的规定值,不能用缩放糊弄)。
LAUNCHER = [40, 56, 60, 65, 70] LAUNCHER = [38, 40, 54, 56, 60, 61, 65, 70]
def launcher(size): def launcher(size):
@@ -160,6 +163,46 @@ def launcher(size):
return img.resize((size, size), Image.LANCZOS) 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(): def main():
svg = open(SRC).read() svg = open(SRC).read()
for size in LAUNCHER: for size in LAUNCHER:
@@ -182,6 +225,10 @@ def main():
if key not in cache: if key not in cache:
p = subpaths(svg, pid) p = subpaths(svg, pid)
cache[key] = flip_chevron(p) if flip else p 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 = render(cache[key], size, cx, cy, blend)
img.save(os.path.join(outdir, "%s.png" % name)) img.save(os.path.join(outdir, "%s.png" % name))
names.append(name) names.append(name)