Update watch face: modular refactor + OWM/Solar/background + codegen tools
- 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
This commit is contained in:
447
source/Fields.mc
447
source/Fields.mc
@@ -13,16 +13,27 @@ import Toybox.Position;
|
||||
import Toybox.WatchUi;
|
||||
import FieldTable;
|
||||
import Settings;
|
||||
import Solar;
|
||||
import Owm;
|
||||
|
||||
// Everything a complication slot can show. Ids match
|
||||
// https://watchface.io/docs/datafields so the two catalogues line up; see
|
||||
// tools/gen_fields.py for the table that drives the settings and the labels.
|
||||
// ============================================================================
|
||||
// 数据字段
|
||||
// ----------------------------------------------------------------------------
|
||||
// 四个数据位能显示的全部内容,共 125 项。
|
||||
//
|
||||
// Only fields Connect IQ can answer from the watch itself are here. The
|
||||
// week-to-date and 7/28-day per-sport aggregates in that catalogue come from a
|
||||
// phone-side Garmin Connect integration, and the OpenWeatherMap / StormGlass /
|
||||
// third-party-app fields need network calls and API keys; neither is something
|
||||
// a self-contained watch face can produce.
|
||||
// 字段编号沿用 https://watchface.io/docs/datafields,两边可以直接对照。
|
||||
// 驱动设置项和标签文案的那张表在 tools/gen_fields.py —— 加字段要改那里,
|
||||
// 然后在本文件对应分类的函数里补一个分支。
|
||||
//
|
||||
// 只收录**手表本机能算出来**的字段。没做的几类及原因见 README 第 7 节,简单说:
|
||||
// - 按运动分类的周/月累计:CIQ 只有按天的总计,没有分运动的历史
|
||||
// - StormGlass 潮汐、第三方 App 复杂功能:需要额外 key 或用户装了对应 App
|
||||
// - 日历、计时器:CIQ 不开放日历,表盘也拿不到触摸输入
|
||||
//
|
||||
// 组织方式:value() 按分类依次尝试 7 个函数,每个函数不认识编号就返回 null。
|
||||
// 之所以不按编号区间派发,是因为编号是**交错**的 —— 1/2/3 是活动,8/11/12 是
|
||||
// 身体,14 又回到活动。
|
||||
// ============================================================================
|
||||
module Fields {
|
||||
|
||||
const DASH = "--";
|
||||
@@ -43,7 +54,7 @@ module Fields {
|
||||
|
||||
function ready() as Boolean { return mDow != null; }
|
||||
|
||||
// Monkey C has no String.split.
|
||||
// Monkey C 没有 String.split,只能自己按逗号切。
|
||||
function splitCsv(str as String) as Array<String> {
|
||||
var out = [] as Array<String>;
|
||||
var rest = str;
|
||||
@@ -69,8 +80,8 @@ module Fields {
|
||||
return Lang.format(mDateFmt, [month(g), g.day.format("%d")]);
|
||||
}
|
||||
|
||||
// "10" in English, "10日" in Chinese -- the format string carries the
|
||||
// suffix, so feed it an empty month.
|
||||
// 英文出 "10",中文出 "10日"。
|
||||
// 后缀带在本地化的日期格式串里,所以这里把月份传空串即可。
|
||||
function dayOnly(g as Gregorian.Info) as String {
|
||||
return Lang.format(mDateFmt, ["", g.day.format("%d")]);
|
||||
}
|
||||
@@ -79,16 +90,18 @@ module Fields {
|
||||
return mMeridiem == null ? "" : mMeridiem[g.hour >= 12 ? 1 : 0];
|
||||
}
|
||||
|
||||
// Short caption printed under a bottom-row value.
|
||||
// 底部格子数值下方的小标签。按字段编号查 FieldTable 的下标,
|
||||
// 再到本地化的标签表里取词,所以中英文会自动跟随系统语言。
|
||||
function label(id as Number) as String {
|
||||
if (mLabels == null) { return ""; }
|
||||
var i = FieldTable.indexOf(id);
|
||||
return i < mLabels.size() ? mLabels[i] : "";
|
||||
}
|
||||
|
||||
// One of these is built per onUpdate and handed to every slot, so the
|
||||
// expensive lookups (forecast, sensor history, user profile) happen at most
|
||||
// once per draw and only if some slot actually asks for them.
|
||||
// 每次绘制构造一个,传给所有数据位共用。
|
||||
// 目的是让开销大的查询(天气预报、传感器历史、用户档案)**每次绘制最多做
|
||||
// 一次,且只在真的有格子要用时才做** —— 四个格子可能都要天气,但只查一次;
|
||||
// 没有格子用到 UserProfile 时就完全不去碰它。
|
||||
class Ctx {
|
||||
var greg as Gregorian.Info;
|
||||
var is24 as Boolean = true;
|
||||
@@ -152,18 +165,40 @@ module Fields {
|
||||
return mProf;
|
||||
}
|
||||
|
||||
// 日出/日落/晨昏蒙影都要位置,而两个实时来源都会间歇性为空:
|
||||
// - 天气在同步之前没有观测点坐标
|
||||
// - currentLocation 要有东西刚定过位才有值
|
||||
// 所以每次拿到位置就缓存进 Storage,两个来源都空时用缓存兜底,
|
||||
// 免得这些字段一会儿有一会儿变 "--"。
|
||||
function where() as Position.Location? {
|
||||
var w = wx();
|
||||
if (w != null && w.observationLocationPosition != null) {
|
||||
return w.observationLocationPosition;
|
||||
if (w != null && (w has :observationLocationPosition)
|
||||
&& w.observationLocationPosition != null) {
|
||||
return remember(w.observationLocationPosition);
|
||||
}
|
||||
var a = act();
|
||||
if (a != null && a.currentLocation != null) { return a.currentLocation; }
|
||||
if (a != null && a.currentLocation != null) {
|
||||
return remember(a.currentLocation);
|
||||
}
|
||||
var lat = Application.Storage.getValue("lastLat");
|
||||
var lon = Application.Storage.getValue("lastLon");
|
||||
if (lat != null && lon != null) {
|
||||
return new Position.Location({:latitude => lat as Double,
|
||||
:longitude => lon as Double,
|
||||
:format => :degrees});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function remember(loc as Position.Location) as Position.Location {
|
||||
var d = loc.toDegrees();
|
||||
Application.Storage.setValue("lastLat", d[0]);
|
||||
Application.Storage.setValue("lastLon", d[1]);
|
||||
return loc;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- helpers
|
||||
// ------------------------------- 格式化工具 ----------------------------
|
||||
function num(v as Numeric?) as String {
|
||||
return v == null ? DASH : v.format("%d");
|
||||
}
|
||||
@@ -197,7 +232,7 @@ module Fields {
|
||||
return t.format("%d") + "°";
|
||||
}
|
||||
|
||||
// ActivityMonitor distances are centimetres.
|
||||
// ActivityMonitor 的距离单位是**厘米**,先换算成公里再按单位制转换。
|
||||
function dist(cm as Numeric?, c as Ctx) as String {
|
||||
if (cm == null) { return DASH; }
|
||||
var km = cm.toFloat() / 100000.0;
|
||||
@@ -217,7 +252,7 @@ module Fields {
|
||||
return (n / 1000.0).format("%.1f") + "k";
|
||||
}
|
||||
|
||||
// Newest reading out of a SensorHistory iterator, or null.
|
||||
// 从 SensorHistory 迭代器里取最新一条读数;没有则 null。
|
||||
function latest(iter) as Numeric? {
|
||||
if (iter == null) { return null; }
|
||||
var s = iter.next();
|
||||
@@ -225,6 +260,53 @@ module Fields {
|
||||
return s.data;
|
||||
}
|
||||
|
||||
// 历史累计。
|
||||
// ActivityMonitor.getHistory() 返回过去若干天(最新在前),**不含今天**。
|
||||
// 把它加起来是本机唯一能做出「本周累计 / 近 7 天累计」的路子;
|
||||
// 按运动项目细分的那些仍然做不到,那需要手机侧与 Garmin Connect 的集成。
|
||||
//
|
||||
// which:0 步数 / 1 卡路里 / 2 距离(厘米) / 3 楼层 / 4 活动分钟
|
||||
// days :往前折算几天,今天总是算在内
|
||||
function sumHistory(c as Ctx, which as Number, days as Number) as Numeric? {
|
||||
var today = todayValue(c, which);
|
||||
if (today == null) { return null; }
|
||||
var total = today;
|
||||
if (days <= 0) { return total; }
|
||||
var hist = ActivityMonitor.getHistory();
|
||||
if (hist == null) { return total; }
|
||||
var n = days < hist.size() ? days : hist.size();
|
||||
for (var k = 0; k < n; k++) {
|
||||
var v = sampleValue(hist[k], which);
|
||||
if (v != null) { total = total + v; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
function todayValue(c as Ctx, which as Number) as Numeric? {
|
||||
var i = c.info();
|
||||
if (i == null) { return null; }
|
||||
return sampleValue(i, which);
|
||||
}
|
||||
|
||||
function sampleValue(d, which as Number) as Numeric? {
|
||||
if (which == 0) { return (d has :steps) ? d.steps : null; }
|
||||
if (which == 1) { return (d has :calories) ? d.calories : null; }
|
||||
if (which == 2) { return (d has :distance) ? d.distance : null; }
|
||||
if (which == 3) { return (d has :floorsClimbed) ? d.floorsClimbed : null; }
|
||||
if (which == 4) {
|
||||
if (!(d has :activeMinutesDay) || d.activeMinutesDay == null) { return null; }
|
||||
return d.activeMinutesDay.total;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 距本周一过了几天,本周累计据此决定往前取多少天。
|
||||
// day_of_week 是 1=周日,所以周日要特判成 6。
|
||||
function daysIntoWeek(g as Gregorian.Info) as Number {
|
||||
var dow = g.day_of_week; // 1 = Sunday
|
||||
return dow == 1 ? 6 : dow - 2;
|
||||
}
|
||||
|
||||
function dayOfYear(g as Gregorian.Info) as Number {
|
||||
var cum = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||||
var d = cum[g.month - 1] + g.day;
|
||||
@@ -234,7 +316,7 @@ module Fields {
|
||||
return d;
|
||||
}
|
||||
|
||||
// ISO 8601 week: weeks start Monday, week 1 holds the first Thursday.
|
||||
// ISO 8601 周数:周一为一周之始,含当年第一个周四的那周为第 1 周。
|
||||
function isoWeek(g as Gregorian.Info) as Number {
|
||||
var dow = g.day_of_week - 1; // 0 = Sunday
|
||||
var iso = dow == 0 ? 7 : dow; // 1 = Monday .. 7 = Sunday
|
||||
@@ -244,7 +326,7 @@ module Fields {
|
||||
return w;
|
||||
}
|
||||
|
||||
// Days since the new moon of 2000-01-06, folded into one synodic month.
|
||||
// 月龄:距 2000-01-06 那次新月的天数,对朔望月(29.530588853 天)取余。
|
||||
function moonAge(g as Gregorian.Info) as Float {
|
||||
var y = g.year;
|
||||
var m = g.month;
|
||||
@@ -253,7 +335,7 @@ module Fields {
|
||||
var b = 2 - a + a / 4;
|
||||
var jd = (365.25 * (y + 4716)).toNumber() + (30.6001 * (m + 1)).toNumber()
|
||||
+ g.day + b - 1524.5 + g.hour / 24.0;
|
||||
// Monkey C has no float modulo.
|
||||
// ⚠️ Monkey C 的 % 不支持浮点,只能手写取余。
|
||||
var syn = 29.530588853;
|
||||
var x = jd - 2451550.1;
|
||||
var age = x - (x / syn).toNumber() * syn;
|
||||
@@ -271,113 +353,136 @@ module Fields {
|
||||
return pts[i];
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- value
|
||||
// Clock, calendar and week-number fields.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ------------------------------- 分类取值 ------------------------------
|
||||
// ---- 日期时间类 ----
|
||||
// 不属于本类的编号返回 null,交给 value() 试下一个分类。
|
||||
function dateTime(id as Number, c as Ctx) as String? {
|
||||
if (id == 860) { return clockInfo(c.greg, c); }
|
||||
if (id == 880) {
|
||||
if (id == 860) { return clockInfo(c.greg, c); } // 时间
|
||||
if (id == 880) { // UTC 时间
|
||||
var utc = Gregorian.utcInfo(Time.now(), Time.FORMAT_SHORT);
|
||||
return clockInfo(utc, c);
|
||||
}
|
||||
// 第二时区 1
|
||||
if (id == 864) { return offsetClock(Settings.number("AltOffset1", 0), c); }
|
||||
// 第二时区 2
|
||||
if (id == 865) { return offsetClock(Settings.number("AltOffset2", 0), c); }
|
||||
if (id == 855) { return c.greg.day.format("%d"); }
|
||||
if (id == 868) { return c.greg.month.format("%d"); }
|
||||
if (id == 859) { return isoWeek(c.greg).format("%d"); }
|
||||
if (id == 855) { return c.greg.day.format("%d"); } // 日
|
||||
if (id == 868) { return c.greg.month.format("%d"); } // 月份数字
|
||||
if (id == 859) { return isoWeek(c.greg).format("%d"); } // 周数 (ISO)
|
||||
// 周数
|
||||
if (id == 873) { return ((dayOfYear(c.greg) + 6) / 7).format("%d"); }
|
||||
if (id == 870) { return dow(c.greg); }
|
||||
if (id == 856) { return dow(c.greg); }
|
||||
if (id == 853) { return month(c.greg); }
|
||||
if (id == 852) { return monthDay(c.greg); }
|
||||
if (id == 872) { return dow(c.greg) + " " + dayOnly(c.greg); }
|
||||
if (id == 851) { return dow(c.greg) + " " + monthDay(c.greg); }
|
||||
if (id == 858) { return meridiem(c.greg); }
|
||||
// 秒数要动起来依赖设备支持 onPartialUpdate;
|
||||
// view 只在有格子选了秒时才去请求那个回调。
|
||||
if (id == 850) { return c.greg.sec.format("%d"); } // 秒
|
||||
if (id == 869) { return c.greg.sec.format("%02d"); } // 秒 (前导零)
|
||||
if (id == 870) { return dow(c.greg); } // 星期简写
|
||||
if (id == 856) { return dow(c.greg); } // 星期全称
|
||||
if (id == 853) { return month(c.greg); } // 月份
|
||||
if (id == 852) { return monthDay(c.greg); } // 月 日
|
||||
if (id == 872) { return dow(c.greg) + " " + dayOnly(c.greg); } // 星期 日
|
||||
if (id == 851) { return dow(c.greg) + " " + monthDay(c.greg); } // 星期 月 日
|
||||
if (id == 858) { return meridiem(c.greg); } // 上午/下午
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Steps, calories, distance, floors and active minutes.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ---- 活动类 ----
|
||||
// 步数、卡路里、距离、楼层、活动分钟,以及它们的本周/近 7 天累计。
|
||||
function activity(id as Number, c as Ctx) as String? {
|
||||
var i = c.info();
|
||||
if (id == 1) { return i == null ? DASH : num(i.steps); }
|
||||
if (id == 14) { return i == null ? DASH : rounded(i.steps); }
|
||||
if (id == 707) {
|
||||
if (id == 1) { return i == null ? DASH : num(i.steps); } // 步数
|
||||
if (id == 14) { return i == null ? DASH : rounded(i.steps); } // 步数 (取整)
|
||||
if (id == 707) { // 剩余步数
|
||||
if (i == null || i.steps == null || i.stepGoal == null) { return DASH; }
|
||||
var left = i.stepGoal - i.steps;
|
||||
return left > 0 ? left.format("%d") : "0";
|
||||
}
|
||||
if (id == 2) { return i == null ? DASH : num(i.calories); }
|
||||
if (id == 603) {
|
||||
if (id == 2) { return i == null ? DASH : num(i.calories); } // 卡路里
|
||||
if (id == 603) { // 活动卡路里
|
||||
if (i == null || i.calories == null) { return DASH; }
|
||||
var p = c.profile();
|
||||
if (p == null || p.weight == null || p.height == null) { return num(i.calories); }
|
||||
return num(i.calories); // device reports total only
|
||||
}
|
||||
if (id == 150 || id == 152 || id == 154) {
|
||||
if (id == 150 || id == 152 || id == 154) { // 活动分钟 / 中强度分钟 / 高强度分钟
|
||||
if (i == null || !(i has :activeMinutesDay) || i.activeMinutesDay == null) {
|
||||
return DASH;
|
||||
}
|
||||
var am = i.activeMinutesDay;
|
||||
if (id == 152) { return num(am.moderate); }
|
||||
if (id == 154) { return num(am.vigorous); }
|
||||
if (id == 152) { return num(am.moderate); } // 中强度分钟
|
||||
if (id == 154) { return num(am.vigorous); } // 高强度分钟
|
||||
return num(am.total);
|
||||
}
|
||||
if (id == 151) {
|
||||
if (id == 151) { // 本周活动分钟
|
||||
if (i == null || !(i has :activeMinutesWeek) || i.activeMinutesWeek == null) {
|
||||
return DASH;
|
||||
}
|
||||
return num(i.activeMinutesWeek.total);
|
||||
}
|
||||
// 距离
|
||||
if (id == 3) { return i == null ? DASH : dist(i.distance, c); }
|
||||
// 上楼层数
|
||||
if (id == 4) { return i == null ? DASH : num(i.floorsClimbed); }
|
||||
// Several ActivityMonitor.Info members compile everywhere but only
|
||||
// exist on some devices and firmware -- the wheelchair counters in
|
||||
// particular threw "Symbol Not Found" on a fenix 8. Check before use.
|
||||
if (id == 5) {
|
||||
// ⚠️ ActivityMonitor.Info 上有些成员**在所有设备上都能编译通过,但只有
|
||||
// 部分设备/固件运行时才真的有**。轮椅计数器就是典型:在 fenix 8 上直接抛
|
||||
// "Symbol Not Found",用户一选这个字段表盘就崩。
|
||||
// 所以凡是非通用成员,用之前一律先 has 检查。
|
||||
if (id == 5) { // 下楼层数
|
||||
return (i == null || !(i has :floorsDescended)) ? DASH
|
||||
: num(i.floorsDescended);
|
||||
}
|
||||
if (id == 6) {
|
||||
if (id == 6) { // 爬升高度
|
||||
return (i == null || !(i has :metersClimbed)) ? DASH
|
||||
: metres(i.metersClimbed, c);
|
||||
}
|
||||
if (id == 9) {
|
||||
if (id == 9) { // 推行距离
|
||||
return (i == null || !(i has :pushDistance)) ? DASH
|
||||
: dist(i.pushDistance, c);
|
||||
}
|
||||
if (id == 10) {
|
||||
if (id == 10) { // 推行次数
|
||||
return (i == null || !(i has :pushes)) ? DASH : num(i.pushes);
|
||||
}
|
||||
if (id == 8 || id == 15) {
|
||||
if (id == 219 || id == 211 || id == 213 || id == 203) { // 本周步数 / 本周卡路里 / 本周距离 / 本周楼层
|
||||
var which = (id == 219) ? 0 : ((id == 211) ? 1 : ((id == 213) ? 2 : 3));
|
||||
var back = daysIntoWeek(c.greg);
|
||||
var v = sumHistory(c, which, back);
|
||||
if (v == null) { return DASH; }
|
||||
return (id == 213) ? dist(v, c) : rounded(v);
|
||||
}
|
||||
// 近 7 天步数 / 近 7 天卡路里 / 近 7 天距离 / 近 7 天楼层 / 近 7 天活动分钟
|
||||
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)));
|
||||
var v = sumHistory(c, which, 6);
|
||||
if (v == null) { return DASH; }
|
||||
return (id == 214) ? dist(v, c) : rounded(v);
|
||||
}
|
||||
if (id == 8 || id == 15) { // 活动条 / 活动条 (反向)
|
||||
if (i == null || i.moveBarLevel == null) { return DASH; }
|
||||
var lvl = i.moveBarLevel;
|
||||
// 活动条 (反向)
|
||||
if (id == 15) { lvl = ActivityMonitor.MOVE_BAR_LEVEL_MAX - lvl; }
|
||||
return lvl.format("%d");
|
||||
}
|
||||
if (id == 13) {
|
||||
if (id == 13) { // 恢复时间
|
||||
return (i == null || !(i has :timeToRecovery)) ? DASH
|
||||
: num(i.timeToRecovery);
|
||||
}
|
||||
if (id == 11) {
|
||||
if (id == 11) { // 呼吸频率
|
||||
return (i == null || !(i has :respirationRate)) ? DASH
|
||||
: num(i.respirationRate);
|
||||
}
|
||||
if (id == 12) {
|
||||
if (id == 12) { // 压力指数
|
||||
return (i == null || !(i has :stressScore)) ? DASH : num(i.stressScore);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Heart, stress, oxygen, body battery and profile-derived values.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ---- 身体类 ----
|
||||
// 心率、压力、血氧、身体电量,以及从用户档案推出来的体重/BMI/VO2max。
|
||||
function body(id as Number, c as Ctx) as String? {
|
||||
if (id == 618) {
|
||||
if (id == 618) { // 心率
|
||||
var a = c.act();
|
||||
if (a != null && a.currentHeartRate != null) { return num(a.currentHeartRate); }
|
||||
var h = ActivityMonitor.getHeartRateHistory(1, true);
|
||||
@@ -390,34 +495,34 @@ module Fields {
|
||||
}
|
||||
return DASH;
|
||||
}
|
||||
if (id == 276) {
|
||||
if (id == 276) { // 静息心率
|
||||
var p = c.profile();
|
||||
return (p == null || !(p has :restingHeartRate)) ? DASH
|
||||
: num(p.restingHeartRate);
|
||||
}
|
||||
if (id == 635) {
|
||||
if (id == 635) { // 血氧
|
||||
if (!(SensorHistory has :getOxygenSaturationHistory)) { return DASH; }
|
||||
return num(latest(SensorHistory.getOxygenSaturationHistory({:period => 1})));
|
||||
}
|
||||
if (id == 623) {
|
||||
if (id == 623) { // 身体电量
|
||||
if (!(SensorHistory has :getBodyBatteryHistory)) { return DASH; }
|
||||
return num(latest(SensorHistory.getBodyBatteryHistory({:period => 1})));
|
||||
}
|
||||
if (id == 281 || id == 742) {
|
||||
if (id == 281 || id == 742) { // 体重 / BMI
|
||||
var p = c.profile();
|
||||
if (p == null || !(p has :weight) || p.weight == null) { return DASH; }
|
||||
var kg = p.weight.toFloat() / 1000.0;
|
||||
if (id == 281) {
|
||||
if (id == 281) { // 体重
|
||||
return (c.metric ? kg : kg * 2.20462).format("%.1f");
|
||||
}
|
||||
if (!(p has :height) || p.height == null || p.height <= 0) { return DASH; }
|
||||
var m = p.height.toFloat() / 100.0;
|
||||
return (kg / (m * m)).format("%.1f");
|
||||
}
|
||||
if (id == 277 || id == 278) {
|
||||
if (id == 277 || id == 278) { // 最大摄氧量 (骑) / 最大摄氧量 (跑)
|
||||
var p = c.profile();
|
||||
if (p == null) { return DASH; }
|
||||
if (id == 278) {
|
||||
if (id == 278) { // 最大摄氧量 (跑)
|
||||
return (p has :vo2maxRunning) ? num(p.vo2maxRunning) : DASH;
|
||||
}
|
||||
return (p has :vo2maxCycling) ? num(p.vo2maxCycling) : DASH;
|
||||
@@ -426,84 +531,87 @@ module Fields {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Battery, alarms, notifications and radio state.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ---- 系统类 ----
|
||||
// 电量、闹钟、通知、勿扰、蓝牙、太阳能强度。
|
||||
function system(id as Number, c as Ctx) as String? {
|
||||
var st = c.stats();
|
||||
if (id == 250) {
|
||||
if (id == 250) { // 电量
|
||||
return st == null || st.battery == null ? DASH
|
||||
: st.battery.format("%d") + "%";
|
||||
}
|
||||
if (id == 251) {
|
||||
if (id == 251) { // 剩余天数
|
||||
if (st == null || !(st has :batteryInDays) || st.batteryInDays == null) {
|
||||
return DASH;
|
||||
}
|
||||
return st.batteryInDays.format("%d") + "d";
|
||||
}
|
||||
if (id == 257) {
|
||||
if (id == 257) { // 电量 / 天数
|
||||
var pct = (st == null || st.battery == null) ? DASH : st.battery.format("%d");
|
||||
var d = (st == null || !(st has :batteryInDays) || st.batteryInDays == null)
|
||||
? DASH : st.batteryInDays.format("%d");
|
||||
return pct + "/" + d;
|
||||
}
|
||||
if (id == 252) {
|
||||
if (id == 252) { // 太阳能强度
|
||||
if (st == null || !(st has :solarIntensity) || st.solarIntensity == null) {
|
||||
return DASH;
|
||||
}
|
||||
return st.solarIntensity.format("%d") + "%";
|
||||
}
|
||||
var ds = c.ds();
|
||||
// 闹钟
|
||||
if (id == 253) { return ds == null ? DASH : num(ds.alarmCount); }
|
||||
// 通知
|
||||
if (id == 255) { return ds == null ? DASH : num(ds.notificationCount); }
|
||||
if (id == 254) {
|
||||
if (id == 254) { // 勿扰模式
|
||||
return ds == null ? DASH : (ds.doNotDisturb ? "ON" : "OFF");
|
||||
}
|
||||
if (id == 256) {
|
||||
if (id == 256) { // 蓝牙
|
||||
return ds == null ? DASH : (ds.phoneConnected ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Altitude, pressure, sun events and moon phase.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ---- 环境类 ----
|
||||
// 海拔、气压、各类太阳事件(含晨昏蒙影与黄金/蓝调时刻)、月相。
|
||||
function environment(id as Number, c as Ctx) as String? {
|
||||
if (id == 615) {
|
||||
if (id == 615) { // 海拔
|
||||
var a = c.act();
|
||||
return (a == null || !(a has :altitude)) ? DASH : metres(a.altitude, c);
|
||||
}
|
||||
if (id == 616 || id == 733) {
|
||||
if (id == 616 || id == 733) { // 海平面气压 / 环境气压
|
||||
var a = c.act();
|
||||
if (a == null) { return DASH; }
|
||||
var pa = null;
|
||||
if (id == 616 && (a has :meanSeaLevelPressure)) {
|
||||
if (id == 616 && (a has :meanSeaLevelPressure)) { // 海平面气压
|
||||
pa = a.meanSeaLevelPressure;
|
||||
} else if (id == 733 && (a has :ambientPressure)) {
|
||||
} else if (id == 733 && (a has :ambientPressure)) { // 环境气压
|
||||
pa = a.ambientPressure;
|
||||
}
|
||||
if (pa == null) { return DASH; }
|
||||
return (pa.toFloat() / 100.0).format("%d");
|
||||
}
|
||||
if (id == 712 || id == 717 || id == 700 || id == 731) {
|
||||
if (id == 712 || id == 717 || id == 700 || id == 731) { // 日出 / 日落 / 下一太阳事件 / 距太阳事件
|
||||
var loc = c.where();
|
||||
if (loc == null) { return DASH; }
|
||||
var now = Time.now();
|
||||
var rise = Weather.getSunrise(loc, now);
|
||||
var set = Weather.getSunset(loc, now);
|
||||
if (id == 712) { return clock(rise, c); }
|
||||
if (id == 717) { return clock(set, c); }
|
||||
if (id == 712) { return clock(rise, c); } // 日出
|
||||
if (id == 717) { return clock(set, c); } // 日落
|
||||
if (rise == null || set == null) { return DASH; }
|
||||
var next = (now.compare(rise) < 0) ? rise
|
||||
: ((now.compare(set) < 0) ? set : rise);
|
||||
if (id == 700) { return clock(next, c); }
|
||||
if (id == 700) { return clock(next, c); } // 下一太阳事件
|
||||
var secs = next.compare(now);
|
||||
if (secs < 0) { secs = secs + 86400; }
|
||||
return (secs / 3600).format("%d") + "h" + ((secs % 3600) / 60).format("%02d");
|
||||
}
|
||||
if (id == 706) { return moonAge(c.greg).format("%.1f"); }
|
||||
if (id == 732) {
|
||||
if (Solar.owns(id)) {
|
||||
return clock(Solar.byId(id, c.where()), c);
|
||||
}
|
||||
if (id == 706) { return moonAge(c.greg).format("%.1f"); } // 月龄
|
||||
if (id == 732) { // 月球照明
|
||||
var age = moonAge(c.greg);
|
||||
var lit = (1.0 - Math.cos(2.0 * Math.PI * age / 29.530588853)) / 2.0;
|
||||
return (lit * 100.0).format("%d") + "%";
|
||||
@@ -512,66 +620,170 @@ module Fields {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Garmin's cached conditions and today's forecast.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ---- Garmin 原生天气 ----
|
||||
// 手表同步下来的当前天气与今日预报。与 OWM 那套完全独立。
|
||||
function weather(id as Number, c as Ctx) as String? {
|
||||
var w = c.wx();
|
||||
// 气温
|
||||
if (id == 306) { return w == null ? DASH : temp(w.temperature, c); }
|
||||
if (id == 300) {
|
||||
if (id == 300) { // 体感温度
|
||||
return (w == null || !(w has :feelsLikeTemperature)) ? DASH
|
||||
: temp(w.feelsLikeTemperature, c);
|
||||
}
|
||||
if (id == 305) {
|
||||
if (id == 305) { // 湿度
|
||||
return (w == null || !(w has :relativeHumidity)) ? DASH
|
||||
: num(w.relativeHumidity) + "%";
|
||||
}
|
||||
if (id == 304) {
|
||||
if (id == 304) { // 降水概率
|
||||
if (w == null || !(w has :precipitationChance)) { return DASH; }
|
||||
return num(w.precipitationChance) + "%";
|
||||
}
|
||||
if (id == 308) {
|
||||
if (id == 308) { // 风速
|
||||
if (w == null || !(w has :windSpeed) || w.windSpeed == null) { return DASH; }
|
||||
var kmh = w.windSpeed.toFloat() * 3.6;
|
||||
return (c.metric ? kmh : kmh * 0.621371).format("%d");
|
||||
}
|
||||
if (id == 307) {
|
||||
if (id == 307) { // 风向角
|
||||
return (w == null || !(w has :windBearing)) ? DASH : num(w.windBearing);
|
||||
}
|
||||
if (id == 310) {
|
||||
if (id == 310) { // 风向
|
||||
return (w == null || !(w has :windBearing)) ? DASH : windDir(w.windBearing);
|
||||
}
|
||||
if (id == 309) {
|
||||
if (id == 309) { // 天气更新时间
|
||||
if (w == null || !(w has :observationTime) || w.observationTime == null) {
|
||||
return DASH;
|
||||
}
|
||||
return clock(w.observationTime, c);
|
||||
}
|
||||
if (id == 301 || id == 302 || id == 639) {
|
||||
if (id == 312) { // 海平面气压
|
||||
if (w == null || !(w has :pressure) || w.pressure == null) { return DASH; }
|
||||
return (w.pressure.toFloat() / 100.0).format("%d");
|
||||
}
|
||||
if (id == 313) { // 紫外线指数
|
||||
return (w == null || !(w has :uvIndex)) ? DASH : num(w.uvIndex);
|
||||
}
|
||||
if (id == 314) { // 能见度
|
||||
if (w == null || !(w has :visibility) || w.visibility == null) { return DASH; }
|
||||
var km = w.visibility.toFloat() / 1000.0;
|
||||
return (c.metric ? km : km * 0.621371).format("%.1f");
|
||||
}
|
||||
if (id == 315) { // 露点
|
||||
// CIQ 没有露点字段,用 Magnus 公式从温度和湿度反推,
|
||||
// 误差在零点几度以内,够用。
|
||||
if (w == null || w.temperature == null
|
||||
|| !(w has :relativeHumidity) || w.relativeHumidity == null) {
|
||||
return DASH;
|
||||
}
|
||||
var t = w.temperature.toFloat();
|
||||
var rh = w.relativeHumidity.toFloat();
|
||||
if (rh <= 0.0) { return DASH; }
|
||||
var g2 = (17.62 * t) / (243.12 + t) + Math.ln(rh / 100.0);
|
||||
return temp(243.12 * g2 / (17.62 - g2), c);
|
||||
}
|
||||
if (id == 301 || id == 302 || id == 639) { // 最高气温 / 最低气温 / 最高/最低气温
|
||||
var f = c.forecast();
|
||||
if (f == null) { return DASH; }
|
||||
var hi = (f has :highTemperature) ? f.highTemperature : null;
|
||||
var lo = (f has :lowTemperature) ? f.lowTemperature : null;
|
||||
if (id == 301) { return temp(hi, c); }
|
||||
if (id == 302) { return temp(lo, c); }
|
||||
if (id == 301) { return temp(hi, c); } // 最高气温
|
||||
if (id == 302) { return temp(lo, c); } // 最低气温
|
||||
return temp(hi, c) + "/" + temp(lo, c);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Free text the wearer typed into the settings.
|
||||
// Returns null when the id is not one of ours, so value()
|
||||
// can try the next category.
|
||||
// ---- OpenWeatherMap ----
|
||||
// 读后台服务上次存下来的数据(本函数不联网,见 Owm.mc / Fenix8V3Background.mc)。
|
||||
// 后台一律用公制请求,单位换算和其他字段一样在这里做。
|
||||
function owm(id as Number, c as Ctx) as String? {
|
||||
if (!Owm.owns(id)) { return null; }
|
||||
if (!Owm.enabled()) { return DASH; }
|
||||
if (id == 500) { return temp(Owm.get("temp"), c); } // OWM 气温
|
||||
if (id == 503) { return temp(Owm.get("feels"), c); } // OWM 体感温度
|
||||
if (id == 504) { return temp(Owm.get("tmax"), c); } // OWM 最高温
|
||||
if (id == 505) { return temp(Owm.get("tmin"), c); } // OWM 最低温
|
||||
if (id == 515) { // OWM 最高/最低
|
||||
return temp(Owm.get("tmax"), c) + "/" + temp(Owm.get("tmin"), c);
|
||||
}
|
||||
if (id == 507) { return str(Owm.get("short")); } // OWM 天气
|
||||
if (id == 502) { return str(Owm.get("long")); } // OWM 天气详述
|
||||
if (id == 501) { // OWM 湿度
|
||||
var h = Owm.get("humidity");
|
||||
return h == null ? DASH : num(h) + "%";
|
||||
}
|
||||
if (id == 506) { return num(Owm.get("pressure")); } // OWM 气压
|
||||
if (id == 509) { // OWM 能见度
|
||||
var v = Owm.get("visibility");
|
||||
if (v == null) { return DASH; }
|
||||
var km = v.toFloat() / 1000.0;
|
||||
return (c.metric ? km : km * 0.621371).format("%.1f");
|
||||
}
|
||||
if (id == 510 || id == 512) { // OWM 风速 / OWM 阵风
|
||||
var w = Owm.get(id == 510 ? "wind" : "gust");
|
||||
if (w == null) { return DASH; }
|
||||
var kmh = w.toFloat() * 3.6;
|
||||
return (c.metric ? kmh : kmh * 0.621371).format("%d");
|
||||
}
|
||||
if (id == 511) { return num(Owm.get("winddeg")); } // OWM 风向角
|
||||
if (id == 518) { return windDir(Owm.get("winddeg")); } // OWM 风向
|
||||
if (id == 513) { // OWM 1h 降雨
|
||||
var r = Owm.get("rain");
|
||||
return r == null ? "0" : r.toFloat().format("%.1f");
|
||||
}
|
||||
if (id == 532) { // OWM 1h 降雪
|
||||
var sn = Owm.get("snow");
|
||||
return sn == null ? "0" : sn.toFloat().format("%.1f");
|
||||
}
|
||||
if (id == 536) { // OWM 云量
|
||||
var cl = Owm.get("clouds");
|
||||
return cl == null ? DASH : num(cl) + "%";
|
||||
}
|
||||
if (id == 537) { // OWM 降水概率
|
||||
var pop = Owm.get("pop");
|
||||
return pop == null ? DASH : (pop.toFloat() * 100.0).format("%d") + "%";
|
||||
}
|
||||
if (id == 530) { // OWM 露点
|
||||
// OWM 免费接口不返回露点,同样用 Magnus 公式从温湿度算。
|
||||
var t = Owm.get("temp");
|
||||
var rh = Owm.get("humidity");
|
||||
if (t == null || rh == null || rh.toFloat() <= 0.0) { return DASH; }
|
||||
var tf = t.toFloat();
|
||||
var gg = (17.62 * tf) / (243.12 + tf) + Math.ln(rh.toFloat() / 100.0);
|
||||
return temp(243.12 * gg / (17.62 - gg), c);
|
||||
}
|
||||
if (id == 534) { return temp(Owm.get("hi1"), c); } // OWM 明日气温
|
||||
if (id == 535) { return temp(Owm.get("hi2"), c); } // OWM 后日气温
|
||||
if (id == 522) { // OWM 明日高低温
|
||||
return temp(Owm.get("hi1"), c) + "/" + temp(Owm.get("lo1"), c);
|
||||
}
|
||||
if (id == 523) { // OWM 后日高低温
|
||||
return temp(Owm.get("hi2"), c) + "/" + temp(Owm.get("lo2"), c);
|
||||
}
|
||||
if (id == 516) { return str(Owm.get("city")); } // OWM 城市
|
||||
if (id == 517) { // OWM 更新时间
|
||||
var dt = Owm.get("dt");
|
||||
return dt == null ? DASH : clock(new Time.Moment(dt as Number), c);
|
||||
}
|
||||
return DASH;
|
||||
}
|
||||
|
||||
function str(v) as String {
|
||||
return v == null ? DASH : v.toString();
|
||||
}
|
||||
|
||||
// ---- 自定义文字 ----
|
||||
// 用户在设置里自己填的三段文字。
|
||||
function custom(id as Number, c as Ctx) as String? {
|
||||
if (id == 703) { return Settings.text("Custom1"); }
|
||||
if (id == 704) { return Settings.text("Custom2"); }
|
||||
if (id == 705) { return Settings.text("Custom3"); }
|
||||
if (id == 703) { return Settings.text("Custom1"); } // 自定义文字 1
|
||||
if (id == 704) { return Settings.text("Custom2"); } // 自定义文字 2
|
||||
if (id == 705) { return Settings.text("Custom3"); } // 自定义文字 3
|
||||
return null;
|
||||
}
|
||||
|
||||
// Text for one data slot. Categories are tried in turn rather
|
||||
// than dispatched on an id range, because the ids interleave.
|
||||
// 取某个字段的显示文本。
|
||||
// 按分类依次尝试,而不是按编号区间派发 —— 编号是交错的(1/2/3 活动、
|
||||
// 8/11/12 身体、14 又是活动),区间派发行不通。
|
||||
function value(id as Number, c as Ctx) as String {
|
||||
var v = dateTime(id, c);
|
||||
if (v == null) { v = activity(id, c); }
|
||||
@@ -579,6 +791,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 = owm(id, c); }
|
||||
if (v == null) { v = custom(id, c); }
|
||||
return v == null ? "" : v;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user