VEU.WathBody/1-docs/4-modules/m13-fenix-data-field/api_specs/02-core.md

6.5 KiB
Raw Permalink Blame History

Спецификация 02: ядро — SetTracker / RestTimer / ZoneCalc

Источники: module_reqs разд. 1 (единицы 2–4), Ф-2.1–Ф-2.4 (с errata Ф-2.3), решение гейта 1.3 № 1/5. Чистые классы Monkey C без UI и без обращений к Toybox.WatchUi — тестируются Toybox.Test.

Общее для всех: часы ядра — Activity.Info.timerTime (активное время активности, мс; при паузе стоит — таймеры поля замирают сами, отдельная обработка паузы не нужна). Значения времени приходят извне (параметрами) — ядро системное время не читает.

1. ZoneCalc — зоны пульса

Вход: maxBpmFor(hrMaxBpm, age):

  • hrMaxBpm > 0 → hrMaxBpm (ручной ввод);
  • иначе формула Танаки: 208 − 0,7 × age, округление до целого вверх (Math.ceil); age вне 14–100 → умолчание 45.

zoneFor(bpm, maxBpm) → зона:

Зона Диапазон от maxBpm Цвет (MIP) Подпись ru / en
BLUE bpm < 50 % Graphics.COLOR_BLUE «восстановление» / «recovery»
GREEN 50 % ≤ bpm < 70 % COLOR_GREEN «лёгкая» / «easy»
YELLOW 70 % ≤ bpm < 85 % COLOR_YELLOW «средняя» / «moderate»
RED 85 % ≤ bpm COLOR_RED «высокая» / «high»
  • сравнение целочисленное без Float: bpm*2 vs maxBpm (50 %), bpm*10 vs maxBpm*7 (70 %), bpm*20 vs maxBpm*17 (85 %);
  • bpm == null или ≤ 0 → зона NONE (серый, прочерк) — «нет данных»;
  • подписи зон совпадают с «Профилем» телефона m10 (согласованный словарь системы).

2. SetTracker — метрики подхода (границы Lap→Lap, errata Ф-2.3)

Состояние: lapAtMs (timerTime последнего Lap), sumBpm, nBpm.

  • onLap(timerTimeMs): lapAtMs = timerTimeMs; sumBpm = 0; nBpm = 0 — метрики обнуляются на границе (Ф-2.3);
  • onSample(bpm): bpm != null && bpm > 0 → sumBpm += bpm; nBpm += 1; null-пульс пропускается (в среднее не попадает);
  • elapsedMs(timerTimeMs) as Long: timerTimeMs − lapAtMs;
  • avgBpm() as Number or Null: nBpm > 0 → округление (sumBpm + nBpm/2) / nBpm, иначе null («—» до первого сэмпла);
  • стартовая граница — onTimerStart активности (lapAtMs = 0): метрики считаются с начала активности до первого Lap.

«Время подхода» = elapsedMs (включает предшествующий отдых — errata Ф-2.1); «средний пульс подхода» = avgBpm за тот же интервал.

3. RestTimer — автономный таймер отдыха (Ф-2.4 errata)

Состояние: active, restEndMs, fired.

  • start(timerTimeMs, restSec): active = true; restEndMs = timerTimeMs + restSec*1000; fired = false;
  • remainingMs(timerTimeMs) as Long: active → restEndMs − timerTimeMs, иначе 0;
  • isResting(timerTimeMs): active && remainingMs > 0 — на экране строка «ОТДЫХ mm:ss» (остаток);
  • onEnd(timerTimeMs): active && remainingMs ≤ 0 && !fired → fired = true, вернуть true — сигнал на однократную лёгкую вибрацию (спека 04); повторные вызовы false;
  • пауза активности: timerTime стоит → остаток не меняется (отдых «заморожен», Ф-2.6 штатная пауза); отдельной логики нет;
  • до первого Lap таймер не активен (active = false при старте активности).

Умолчание длительности — FieldSettings.restSec (120 = Rest.DefaultSec телефона, спека 04).

4. Vibe — паттерны и гистерезис (спека 04 — параметры)

Красная зона: onZone(zone, timerTimeMs):

  • зона RED и прошло ≥ 60 000 мс от последней двойной вибрации → сигнал двойной вибрации, фиксируется время;
  • выход из RED (зона GREEN/BLUE/NONE) → гистерезис сброшен — следующее входжение в RED вибрирует сразу.

5. Тесты (Toybox.Test, test\)

ZoneCalcTest:

  1. Танака: maxBpmFor(0, 45) == 177 (208 − 31,5 = 176,5 → ceil);
  2. ручной приоритет: maxBpmFor(190, 45) == 190;
  3. возраст вне диапазона → расчёт от 45 (maxBpmFor(0, 5) == 177);
  4. границы зон при maxBpm 180: 89 → BLUE, 90 → GREEN, 125 → GREEN, 126 → YELLOW, 152 → YELLOW, 153 → RED (50 % = 90, 70 % = 126, 85 % = 153 — целочисленные пороги);
  5. zoneFor(null, 180) == NONE.

SetTrackerTest:

  1. среднее округление: сэмплы [100, 101, 101] → 101;
  2. null-сэмплы не влияют: [100, null, 102] → 101;
  3. onLap обнуляет: сэмплы до/после Lap — среднее только после;
  4. elapsedMs от границы: Lap на 60 000, сейчас 75 500 → 15 500;
  5. до сэмплов avgBpm() == null.

RestTimerTest:

  1. старт по Lap на t=60 000, restSec 120: remainingMs(60 000)==120 000, remainingMs(120 000) == 60 000, remainingMs(180 000) == 0;
  2. onEnd срабатывает однократно: false при остатке > 0; true при 180 000; повторный вызов false;
  3. isResting: true при остатке > 0, false при ≤ 0;
  4. после конца отдыха isResting == false, но active до следующего Lap не перезапускается;
  5. новый start сбрасывает fired.

Vibe (гистерезис) — в тестах RestTimer/Vibe: 6. RED в t=0 → вибрация; RED в t=30 000 → нет (< 60 с); RED в t=61 000 → вибрация; 7. GREEN между — сброс: RED t=0 → вибрация; GREEN t=10 000; RED t=20 000 → вибрация (сброс сработал).