- Split render into FieldTable/Fields/Icons/Layout/Settings modules - Add Fenix8V3Background, Owm (weather), Solar modules - Add tools/ generators (gen_fields/gen_icons/gen_themes/gen_preview_svg) - Tune README/manifest/strings/settings
319 lines
19 KiB
Python
319 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""一张表 → 字段 id 表、两种语言的文案、以及设置项 XML。
|
||
|
||
字段编号沿用 https://watchface.io/docs/datafields,方便两边对照;但这里只收录
|
||
Connect IQ 在手表本机能算出来的字段。
|
||
|
||
需要联网服务的(OpenWeatherMap 的部分、StormGlass)、需要手机侧与 Garmin
|
||
Connect 集成的(按运动分类的周/月累计)、需要用户装了别的 App 的(CGM、
|
||
补水、行情),都不在这里 —— 具体原因见 README 第 7 节。
|
||
|
||
python3 tools/gen_fields.py
|
||
|
||
产出:
|
||
source/FieldTable.mc 字段 id 表 + 下标反查
|
||
resources/settings/settings.xml 四个数据位的选择器
|
||
resources/strings/fields.xml 英文的字段名与表盘标签
|
||
resources-chn/strings/fields.xml 中文的字段名与表盘标签
|
||
|
||
加字段的流程:在下面的 FIELDS 表里加一行 → 跑一遍本脚本 →
|
||
到 source/Fields.mc 对应分类的函数里补一个分支。设置项、两种语言的文案、
|
||
表盘上的标签都会自动跟上,不会漏。
|
||
"""
|
||
import os
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
ROOT = os.path.dirname(HERE)
|
||
|
||
# id, key, English name, Chinese name, on-face label (en), on-face label (cn)
|
||
FIELDS = [
|
||
(0, "Off", "Off", "关闭", "", ""),
|
||
|
||
# ---- date / time ----------------------------------------------------
|
||
(860, "Time", "Time", "时间", "TIME", "时间"),
|
||
(880, "UtcTime", "UTC Time", "UTC 时间", "UTC", "UTC"),
|
||
(864, "AltTime1", "Alternate Time 1", "第二时区 1", "TZ1", "时区1"),
|
||
(865, "AltTime2", "Alternate Time 2", "第二时区 2", "TZ2", "时区2"),
|
||
(851, "DayMonthDay", "Day Month Day", "星期 月 日", "DATE", "日期"),
|
||
(872, "DayDate", "Day Date", "星期 日", "DATE", "日期"),
|
||
(852, "MonthDay", "Month Day", "月 日", "DATE", "日期"),
|
||
(855, "DayOfMonth", "Day of Month", "日", "DAY", "日"),
|
||
(870, "DayShort", "Day (Tue)", "星期简写", "DAY", "星期"),
|
||
(856, "DayFull", "Full Day (Tuesday)", "星期全称", "DAY", "星期"),
|
||
(853, "MonthName", "Month (Jan)", "月份", "MON", "月"),
|
||
(868, "MonthNum", "Month (6)", "月份数字", "MON", "月"),
|
||
(858, "Meridiem", "AM/PM", "上午/下午", "", ""),
|
||
(859, "WeekIso", "Week Number (ISO)", "周数 (ISO)", "WEEK", "周"),
|
||
(850, "Seconds", "Seconds", "秒", "SEC", "秒"),
|
||
(869, "Seconds0", "Seconds (09)", "秒 (前导零)", "SEC", "秒"),
|
||
(873, "WeekCal", "Week Number", "周数", "WEEK", "周"),
|
||
|
||
# ---- activity -------------------------------------------------------
|
||
(1, "Steps", "Steps", "步数", "STEP", "步数"),
|
||
(14, "StepsRound", "Steps Rounded", "步数 (取整)", "STEP", "步数"),
|
||
(707, "StepsToGo", "Steps to Go", "剩余步数", "TO GO", "还差"),
|
||
(2, "Calories", "Calories", "卡路里", "KCAL", "千卡"),
|
||
(603, "ActiveCal", "Active Calories", "活动卡路里", "ACAL", "活动"),
|
||
(150, "ActiveMin", "Active Minutes", "活动分钟", "MIN", "分钟"),
|
||
(152, "ActiveMod", "Active Min (Moderate)", "中强度分钟", "MOD", "中强度"),
|
||
(154, "ActiveVig", "Active Min (Vigorous)", "高强度分钟", "VIG", "高强度"),
|
||
(151, "ActiveWeek", "Active Minutes (Week)", "本周活动分钟", "WEEK", "周分钟"),
|
||
(3, "Distance", "Distance", "距离", "DST", "距离"),
|
||
(4, "FloorsUp", "Floors Climbed", "上楼层数", "FLR", "楼层"),
|
||
(5, "FloorsDown", "Floors Descended", "下楼层数", "DESC", "下楼"),
|
||
(6, "MetersUp", "Distance Climbed", "爬升高度", "ASC", "爬升"),
|
||
(9, "PushDist", "Push Distance", "推行距离", "PUSH", "推行"),
|
||
(10, "Pushes", "Pushes", "推行次数", "PUSH", "推行"),
|
||
(219, "StepsWeek", "Steps (Week to Date)", "本周步数", "WEEK", "周步数"),
|
||
(220, "Steps7d", "Steps (Last 7 Days)", "近 7 天步数", "7D", "7天"),
|
||
(211, "CalWeek", "Calories (Week to Date)", "本周卡路里", "WEEK", "周千卡"),
|
||
(212, "Cal7d", "Calories (Last 7 Days)", "近 7 天卡路里", "7D", "7天"),
|
||
(213, "DistWeek", "Distance (Week to Date)", "本周距离", "WEEK", "周距离"),
|
||
(214, "Dist7d", "Distance (Last 7 Days)", "近 7 天距离", "7D", "7天"),
|
||
(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天"),
|
||
|
||
# ---- body -----------------------------------------------------------
|
||
(618, "HeartRate", "Heart Rate", "心率", "HR", "心率"),
|
||
(276, "RestingHr", "Resting Heart Rate", "静息心率", "RHR", "静息"),
|
||
(12, "Stress", "Stress Score", "压力指数", "STRS", "压力"),
|
||
(635, "PulseOx", "Pulse Ox", "血氧", "SPO2", "血氧"),
|
||
(11, "Respiration", "Respiration Rate", "呼吸频率", "RESP", "呼吸"),
|
||
(623, "BodyBattery", "Body Battery", "身体电量", "BODY", "体能"),
|
||
(8, "MoveBar", "Move Bar", "活动条", "MOVE", "活动"),
|
||
(15, "MoveBarRev", "Move Bar Reversed", "活动条 (反向)", "MOVE", "活动"),
|
||
(281, "Weight", "Weight", "体重", "WT", "体重"),
|
||
(742, "Bmi", "BMI", "BMI", "BMI", "BMI"),
|
||
(13, "Recovery", "Time to Recovery", "恢复时间", "RCVR", "恢复"),
|
||
(278, "Vo2Run", "VO2 Max (Run)", "最大摄氧量 (跑)", "VO2", "摄氧"),
|
||
(277, "Vo2Bike", "VO2 Max (Bike)", "最大摄氧量 (骑)", "VO2", "摄氧"),
|
||
|
||
# ---- system ---------------------------------------------------------
|
||
(250, "Battery", "Battery", "电量", "BATT", "电量"),
|
||
(251, "BatteryDays", "Battery in Days", "剩余天数", "DAYS", "天数"),
|
||
(257, "BatteryBoth", "Battery / Days", "电量 / 天数", "BATT", "电量"),
|
||
(253, "Alarms", "Alarms", "闹钟", "ALRM", "闹钟"),
|
||
(255, "Notifications", "Notifications", "通知", "MSG", "通知"),
|
||
(254, "DoNotDisturb", "Do Not Disturb", "勿扰模式", "DND", "勿扰"),
|
||
(256, "Bluetooth", "Bluetooth", "蓝牙", "BT", "蓝牙"),
|
||
(252, "Solar", "Solar Intensity", "太阳能强度", "SOLR", "太阳能"),
|
||
|
||
# ---- environment ----------------------------------------------------
|
||
(615, "Altitude", "Altitude", "海拔", "ALT", "海拔"),
|
||
(616, "SeaPressure", "Sea Level Pressure", "海平面气压", "hPa", "气压"),
|
||
(733, "AmbPressure", "Ambient Pressure", "环境气压", "hPa", "气压"),
|
||
(712, "Sunrise", "Sunrise", "日出", "RISE", "日出"),
|
||
(717, "Sunset", "Sunset", "日落", "SET", "日落"),
|
||
(700, "NextSun", "Next Sun Event", "下一太阳事件", "SUN", "太阳"),
|
||
(731, "UntilSun", "Time Until Sun Event", "距太阳事件", "SUN", "太阳"),
|
||
(706, "MoonAge", "Moon Age", "月龄", "MOON", "月龄"),
|
||
(732, "MoonLit", "Moon Illumination", "月球照明", "MOON", "月相"),
|
||
(710, "CivilAm", "Civil Twilight Start", "民用晨光始", "CIVIL", "民用晨"),
|
||
(709, "NauticalAm", "Nautical Twilight Start", "航海晨光始", "NAUT", "航海晨"),
|
||
(708, "AstroAm", "Astronomical Twilight Start", "天文晨光始", "ASTR", "天文晨"),
|
||
(711, "BlueAm", "Blue Hour (AM)", "蓝调时刻 (晨)", "BLUE", "蓝调晨"),
|
||
(714, "GoldenAm", "Golden Hour (AM)", "黄金时刻 (晨)", "GOLD", "黄金晨"),
|
||
(715, "GoldenPm", "Golden Hour (PM)", "黄金时刻 (昏)", "GOLD", "黄金昏"),
|
||
(718, "BluePm", "Blue Hour (PM)", "蓝调时刻 (昏)", "BLUE", "蓝调昏"),
|
||
(719, "CivilPm", "Civil Twilight End", "民用昏影终", "CIVIL", "民用昏"),
|
||
(720, "NauticalPm", "Nautical Twilight End", "航海昏影终", "NAUT", "航海昏"),
|
||
(721, "AstroPm", "Astronomical Twilight End", "天文昏影终", "ASTR", "天文昏"),
|
||
|
||
# ---- weather --------------------------------------------------------
|
||
(306, "Temp", "Temperature", "气温", "TEMP", "气温"),
|
||
(639, "TempHiLo", "Temperature High/Low", "最高/最低气温", "HI/LO", "高低温"),
|
||
(300, "FeelsLike", "Feels Like", "体感温度", "FEEL", "体感"),
|
||
(301, "TempHigh", "High Temperature", "最高气温", "HIGH", "最高"),
|
||
(302, "TempLow", "Low Temperature", "最低气温", "LOW", "最低"),
|
||
(305, "Humidity", "Humidity", "湿度", "HUM", "湿度"),
|
||
(304, "Precip", "Precipitation %", "降水概率", "RAIN", "降水"),
|
||
(308, "WindSpeed", "Wind Speed", "风速", "WIND", "风速"),
|
||
(307, "WindBearing", "Wind Bearing", "风向角", "WIND", "风向"),
|
||
(310, "WindDir", "Wind Direction", "风向", "WIND", "风向"),
|
||
(309, "WeatherTime", "Weather Update Time", "天气更新时间", "WX", "更新"),
|
||
(312, "PressureSea", "Pressure (Sea Level)", "海平面气压", "hPa", "气压"),
|
||
(313, "Uvi", "UV Index", "紫外线指数", "UVI", "紫外线"),
|
||
(314, "Visibility", "Visibility", "能见度", "VIS", "能见度"),
|
||
(315, "DewPoint", "Dew Point", "露点", "DEW", "露点"),
|
||
|
||
# ---- OpenWeatherMap -------------------------------------------------
|
||
# Fetched by the background service; see source/Owm.mc.
|
||
(500, "OwmTemp", "OWM Temperature", "OWM 气温", "TEMP", "气温"),
|
||
(515, "OwmMinMax", "OWM Max/Min Temp", "OWM 最高/最低", "HI/LO", "高低温"),
|
||
(503, "OwmFeels", "OWM Feels Like", "OWM 体感温度", "FEEL", "体感"),
|
||
(504, "OwmMax", "OWM Temp Max", "OWM 最高温", "HIGH", "最高"),
|
||
(505, "OwmMin", "OWM Temp Min", "OWM 最低温", "LOW", "最低"),
|
||
(507, "OwmDescShort", "OWM Description", "OWM 天气", "WX", "天气"),
|
||
(502, "OwmDescLong", "OWM Description (Long)", "OWM 天气详述", "WX", "天气"),
|
||
(501, "OwmHumidity", "OWM Humidity", "OWM 湿度", "HUM", "湿度"),
|
||
(506, "OwmPressure", "OWM Pressure", "OWM 气压", "hPa", "气压"),
|
||
(509, "OwmVisibility", "OWM Visibility", "OWM 能见度", "VIS", "能见度"),
|
||
(510, "OwmWind", "OWM Wind Speed", "OWM 风速", "WIND", "风速"),
|
||
(511, "OwmWindDeg", "OWM Wind Degrees", "OWM 风向角", "WIND", "风向"),
|
||
(518, "OwmWindDir", "OWM Wind Direction", "OWM 风向", "WIND", "风向"),
|
||
(512, "OwmGust", "OWM Wind Gust", "OWM 阵风", "GUST", "阵风"),
|
||
(513, "OwmRain", "OWM Rain 1h", "OWM 1h 降雨", "RAIN", "降雨"),
|
||
(532, "OwmSnow", "OWM Snow 1h", "OWM 1h 降雪", "SNOW", "降雪"),
|
||
(536, "OwmClouds", "OWM Cloud Cover", "OWM 云量", "CLD", "云量"),
|
||
(537, "OwmPrecip", "OWM Precipitation %", "OWM 降水概率", "RAIN", "降水"),
|
||
(530, "OwmDew", "OWM Dew Point", "OWM 露点", "DEW", "露点"),
|
||
(534, "OwmTomorrow", "OWM Temp Tomorrow", "OWM 明日气温", "TMRW", "明日"),
|
||
(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", "后日"),
|
||
(516, "OwmCity", "OWM City Name", "OWM 城市", "CITY", "城市"),
|
||
(517, "OwmUpdated", "OWM Update Time", "OWM 更新时间", "OWM", "更新"),
|
||
|
||
# ---- custom ---------------------------------------------------------
|
||
(703, "Custom1", "Custom Text 1", "自定义文字 1", "", ""),
|
||
(704, "Custom2", "Custom Text 2", "自定义文字 2", "", ""),
|
||
(705, "Custom3", "Custom Text 3", "自定义文字 3", "", ""),
|
||
]
|
||
|
||
SLOTS = [("LeftTop", "Top-Left Field", "左上数据"),
|
||
("RightTop", "Top-Right Field", "右上数据"),
|
||
("BottomLeft", "Bottom-Left Field", "左下数据"),
|
||
("BottomRight", "Bottom-Right Field", "右下数据")]
|
||
|
||
RING = [(0, "Steps", "Steps", "步数"), (1, "Calories", "Calories", "卡路里"),
|
||
(2, "Distance", "Distance", "距离"), (3, "Floors", "Floors", "楼层"),
|
||
(4, "MoveBar", "Move Bar", "动动条"), (5, "ActiveMin", "Active Minutes", "活动分钟"),
|
||
(6, "BodyBattery", "Body Battery", "身体电量")]
|
||
|
||
OTHER_SETTINGS = """
|
||
<setting propertyKey="@Properties.TempUnit" title="@Strings.SettingTempUnit">
|
||
<settingConfig type="list">
|
||
<listEntry value="0">@Strings.TempUnitAuto</listEntry>
|
||
<listEntry value="1">@Strings.TempUnitC</listEntry>
|
||
<listEntry value="2">@Strings.TempUnitF</listEntry>
|
||
</settingConfig>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.AltOffset1" title="@Strings.SettingAltOffset1">
|
||
<settingConfig type="numeric" min="-720" max="840"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.AltOffset2" title="@Strings.SettingAltOffset2">
|
||
<settingConfig type="numeric" min="-720" max="840"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.OwmKey" title="@Strings.SettingOwmKey">
|
||
<settingConfig type="alphaNumeric" maxLength="40"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.Custom1" title="@Strings.SettingCustom1">
|
||
<settingConfig type="alphaNumeric" maxLength="12"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.Custom2" title="@Strings.SettingCustom2">
|
||
<settingConfig type="alphaNumeric" maxLength="12"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.Custom3" title="@Strings.SettingCustom3">
|
||
<settingConfig type="alphaNumeric" maxLength="12"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.ShowTop" title="@Strings.SettingShowTop">
|
||
<settingConfig type="boolean"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.ShowBand" title="@Strings.SettingShowBand">
|
||
<settingConfig type="boolean"/>
|
||
</setting>
|
||
|
||
<setting propertyKey="@Properties.ShowBottom" title="@Strings.SettingShowBottom">
|
||
<settingConfig type="boolean"/>
|
||
</setting>
|
||
"""
|
||
|
||
|
||
def field_list(indent=8):
|
||
pad = " " * indent
|
||
return "\n".join('%s<listEntry value="%d">@Strings.Fld%s</listEntry>'
|
||
% (pad, fid, key) for fid, key, _, _, _, _ in FIELDS)
|
||
|
||
|
||
def write(path, text):
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
open(path, "w").write(text)
|
||
print("wrote", os.path.relpath(path, ROOT))
|
||
|
||
|
||
def main():
|
||
ids = [f[0] for f in FIELDS]
|
||
assert len(ids) == len(set(ids)), "duplicate field id"
|
||
|
||
# ---- FieldTable.mc ----
|
||
L = ["// ⚠️ 本文件由 tools/gen_fields.py 自动生成,请勿手改。",
|
||
"// 要加字段:在生成器的 FIELDS 表里加一行,跑一遍生成器,",
|
||
"// 再到 source/Fields.mc 对应分类的函数里补一个分支。",
|
||
"//",
|
||
"// 字段编号沿用 https://watchface.io/docs/datafields,只收录 Connect IQ",
|
||
"// 表盘在本机能算出来的那些。",
|
||
"import Toybox.Lang;", "", "module FieldTable {", "",
|
||
" // 全部字段 id,顺序与 FieldLabels 资源串(逗号分隔的标签表)严格一致 ——",
|
||
" // Fields.label() 就是靠这个下标去取对应标签的。",
|
||
" const IDS = ["]
|
||
row = " "
|
||
for i, fid in enumerate(ids):
|
||
piece = "%d%s" % (fid, "," if i < len(ids) - 1 else "")
|
||
if len(row) + len(piece) > 92:
|
||
L.append(row)
|
||
row = " "
|
||
row += piece
|
||
L += [row, " ];", "",
|
||
" // 由字段 id 反查它在 IDS 里的下标;找不到返回 0(对应「关闭」)。",
|
||
" function indexOf(id as Number) as Number {",
|
||
" for (var i = 0; i < IDS.size(); i++) {",
|
||
" if (IDS[i] == id) { return i; }",
|
||
" }",
|
||
" return 0;",
|
||
" }",
|
||
"}"]
|
||
write(os.path.join(ROOT, "source", "FieldTable.mc"), "\n".join(L) + "\n")
|
||
|
||
# ---- settings.xml ----
|
||
S = ['<settings>', '',
|
||
' <setting propertyKey="@Properties.Theme" title="@Strings.SettingTheme">',
|
||
' <settingConfig type="list">']
|
||
names = ["Ember", "Aurora", "Brass", "Voltage", "Reef", "Acid", "Kelp"]
|
||
for i, n in enumerate(names):
|
||
S.append(' <listEntry value="%d">@Strings.Theme%s</listEntry>' % (i + 1, n))
|
||
S += [' </settingConfig>', ' </setting>', '']
|
||
for slot, _, _ in SLOTS:
|
||
S += [' <setting propertyKey="@Properties.%s" title="@Strings.Setting%s">' % (slot, slot),
|
||
' <settingConfig type="list">', field_list(12),
|
||
' </settingConfig>', ' </setting>', '']
|
||
for ring in ("RingTR", "RingTL", "RingBR", "RingBL"):
|
||
S += [' <setting propertyKey="@Properties.%s" title="@Strings.Setting%s">' % (ring, ring),
|
||
' <settingConfig type="list">']
|
||
for rid, key, _, _ in RING:
|
||
S.append(' <listEntry value="%d">@Strings.Metric%s</listEntry>' % (rid, key))
|
||
S += [' </settingConfig>', ' </setting>', '']
|
||
S.append(OTHER_SETTINGS.rstrip())
|
||
S.append('</settings>')
|
||
write(os.path.join(ROOT, "resources", "settings", "settings.xml"), "\n".join(S) + "\n")
|
||
|
||
# ---- field strings, both languages ----
|
||
for lang, folder, ni, li in (("en", "resources", 2, 4), ("chn", "resources-chn", 3, 5)):
|
||
X = ['<strings>']
|
||
X.append(' <!-- Field names shown in the settings picker. -->')
|
||
for f in FIELDS:
|
||
X.append(' <string id="Fld%s">%s</string>' % (f[1], f[ni]))
|
||
X.append('')
|
||
X.append(' <!-- On-face captions, in FieldTable.IDS order. -->')
|
||
X.append(' <string id="FieldLabels">%s</string>'
|
||
% ",".join(f[li] for f in FIELDS))
|
||
X.append('')
|
||
for rid, key, en, cn in RING:
|
||
X.append(' <string id="Metric%s">%s</string>' % (key, en if lang == "en" else cn))
|
||
X.append('</strings>')
|
||
write(os.path.join(ROOT, folder, "strings", "fields.xml"), "\n".join(X) + "\n")
|
||
|
||
print("%d fields, %d slots" % (len(FIELDS), len(SLOTS)))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|