import Toybox.Lang; import Toybox.Complications; import Toybox.System; // ============================================================================ // 系统 Complication 桥接 // ---------------------------------------------------------------------------- // Toybox.Complications(CIQ 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?; 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 个**第三方发布**的 complication(n 从 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; 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; } // 完赛预测(627–630)与配速预测(631–634)都是秒。 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; } }