Fields 75 selectable fields across date/time, activity, body, system, environment, weather and custom text, numbered to match watchface.io/docs/datafields so the two catalogues line up. All four slots (both top, both bottom) pick from the same table and the bottom captions follow the selection. tools/gen_fields.py owns the table and emits FieldTable.mc, settings.xml and both languages' strings, so those cannot drift apart. Fields.Ctx fetches each API at most once per draw, and only if some slot asks -- the forecast, user profile and sensor history lookups are lazy. The rest of that catalogue is deliberately absent: OpenWeatherMap and StormGlass need network calls and user API keys, the per-sport weekly and 28-day aggregates come from a phone-side Garmin Connect integration rather than any on-device API, and the app complications need those apps installed. README section 7 lists what was left out and why. Permissions UserProfile and SensorHistory are now declared; without them resting heart rate, VO2 max, pulse ox and body battery throw at runtime. Sensor turns out to be rejected outright for type="watchface", so altitude and pressure read from Activity.Info instead. Always-on display AMOLED devices need the lit-pixel count cut while asleep. drawAmbient drops the band fill and the unlit ticks, dims what remains, hides the complications, and walks the layout within +/-4 px per minute so nothing burns in. Gated on requiresBurnInProtection, so MIP devices are untouched. Icons The comps' sunrise and weather icons have two- and three-pixel features. Rebuilding them from circles and lines looked right at 454 px and turned to mush at 280. They are now rasterised from the design paths once per screen width and tinted with drawBitmap2(:tintColor) -- which needs packingFormat="png", since a palettised source is refused at runtime. Launcher icons are generated at the five sizes the devices ask for. All 17 devices build; checked on fenix847mm and enduro3 in the simulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
253 lines
14 KiB
Python
253 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""One table -> the field id list, both languages, and the settings XML.
|
|
|
|
Field ids follow the numbering at https://watchface.io/docs/datafields so the
|
|
two catalogues line up, but only the fields Connect IQ can actually serve from
|
|
the watch are here. Anything needing a web service (OpenWeatherMap,
|
|
StormGlass), a companion phone integration (the week-to-date and 7/28-day
|
|
per-sport aggregates), or another installed app (CGM, hydration, quotes) is
|
|
deliberately absent -- see README section 8 for the full reasoning.
|
|
|
|
python3 tools/gen_fields.py
|
|
|
|
Writes:
|
|
source/FieldTable.mc ids + short label lookup
|
|
resources/settings/settings.xml the four field pickers
|
|
resources/strings/fields.xml English names and labels
|
|
resources-chn/strings/fields.xml Chinese names and labels
|
|
"""
|
|
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", "周"),
|
|
(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", "推行"),
|
|
|
|
# ---- 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", "月相"),
|
|
|
|
# ---- 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", "更新"),
|
|
|
|
# ---- 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.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 = ["// AUTO-GENERATED by tools/gen_fields.py -- do not edit by hand.",
|
|
"// Ids follow https://watchface.io/docs/datafields; only the fields a",
|
|
"// Connect IQ watch face can source on-device are listed.",
|
|
"import Toybox.Lang;", "", "module FieldTable {", "",
|
|
" // Field ids, in the same order as the Labels resource string.",
|
|
" 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, " ];", "",
|
|
" 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()
|