230 lines
21 KiB
Markdown
230 lines
21 KiB
Markdown
# Технические требования: WatchBody («Феникс 6X») v1
|
||
|
||
> Шаг 5 процесса разработки (`std1\_process\process.md`).
|
||
> Источники: [vision.md](../1-reqs/vision.md) v1.5,
|
||
> [requirements.md](../1-reqs/requirements.md) (утверждён, раздел 7 —
|
||
> открытые вопросы), [architecture.md](../2-arch/architecture.md)
|
||
> (утверждена), дизайн LLM-планирования, документ форматов,
|
||
> UI/UX-рекомендации ([ui-ux-recommendations.md](ui-ux-recommendations.md)).
|
||
> Версия 1.1 — **УТВЕРЖДЕНО** (2026-10-17; интеграция документа метрик —
|
||
> Ф-13, Safety-пороги, daily_log — утверждена вместе с документом).
|
||
> Версия 1.2 — черновик (2026-10-17): errata ревью (З-6, З-11) +
|
||
> интеграция документа перерывов (Ф-14: сущность `break`,
|
||
> `session_kind`).
|
||
> Версия 1.3 — черновик (2026-10-17): волна 2 — Р1-А (daily_log v1
|
||
> поля), Р2-Б (UUID PK клиентских сущностей, маппинг id не нужен),
|
||
> Р3-А (Safety на телефоне), З-7 (RPE 1–10); UI/UX-стандарт
|
||
> (`ui-ux-recommendations.md`). Утверждение — Пользователь!
|
||
> Версия 1.4 — **УТВЕРЖДЕНО** (2026-10-17): ревью №2
|
||
> (`0-mng\review-2026-10-17-2.md`) — сущность `break` согласована с
|
||
> `src.breaks` (З-17), ОВ-7 — таргет `fenix6x` 280×280 (З-18),
|
||
> ОВ-10 закрыт (З-19, О-1-А), `Safety.HrStrengthMax` (З-20),
|
||
> интервал fetch 15 минут (З-21), грамматика «…с половиной» (З-26,
|
||
> О-2-Б), `Progression.IsoStepKg` (З-27, О-3-А). Ревью №3: решение
|
||
> ОВ-11 — импорт пакета программы (З-31, О-4-Б).
|
||
|
||
## 1. Решения открытых вопросов (закрытие раздела 7 requirements)
|
||
|
||
| # ОВ | Решение | Обоснование |
|
||
|---|---|---|
|
||
| 1. Максимальная ЧСС | Формула Танаки: `HRmax = 208 − 0,7 × возраст`; ручной ввод `Hr.MaxBpm` перекрывает расчёт | Точнее для 45+ (профиль пользователя); ручной ввод — окончательная истина |
|
||
| 2. Офлайн-STT | **Vosk** (малая русская модель ~50 МБ) + узкая грамматика команд (Ф-1.2: числа и слова «подход/вес/отдых/секунд/РПЭ» — «РПЭ 8» ставит RPE последнему
|
||
подходу, Ф-13.3); TTS-подтверждения — системный Android TTS | Работа без сети, малый словарь → устойчивость к шуму; sherpa-onnx — запасная кандидатура, если точность < КУ-1 на контрольной тренировке |
|
||
| 3. Канал телефон ↔ часы | Connect IQ **Communications SDK** (companion-канал через приложение Garmin Connect Mobile на телефоне). Передаётся: текущий пульс (≈1 Гц), зона, таймер отдыха (секунды до конца), заряд часов. Ограничение: на телефоне должно быть установлено и залогинено Garmin Connect Mobile; канал может не доставлять — обе стороны деградируют молча (Ф-2.4, Ф-3.2) | Единственный штатный канал сторонних приложений Garmin; проверка объёма передачи — прототипом в итерации 1 шага 7 (критерий: пульс и таймер доходят при экране часов активным) |
|
||
| 4. Расписание fetch | Внешний планировщик ОС: systemd timer (Linux-сервер) / Планировщик задач (Windows) вызывает `--mode fetch`; внутри сервера планировщика нет. Интервал вызова — 15 минут (параметр планировщика ОС, в код не входит); вместе с лагом синхронизации часов с Garmin Connect укладывается в БЦ-5 (архив ≤ 30 минут после тренировки) | Проще, надёжнее, не дублирует средства ОС |
|
||
| 10. Источник .fit атлетов без учётки владельца | v1 — только папка импорта (`--mode import`); `fetch` работает под единственной учёткой владельца; собственные учётки атлетов в конфиге — кандидат v2. Уточнение владельца: атлет регистрируется на телефоне (имя), привязка — по uuid клиентского приложения | Не раздувать v1; деградация уже работает (решение владельца О-1-А, З-19) |
|
||
| 11. Импорт пакета программы | `--mode import` принимает `program-import.json` (формат — `program-import-format.md`): пакет маппится на `src.program` (`external_id` UNIQUE) и `src.program_version.body`; повторный импорт с тем же `external_id` — новая версия `program_version`; секция `profile` пакета в v1 не переносится — Safety-порогами управляет только конфиг `Safety.*` | Пакет — механизм эксплуатации (уточнение владельца); иначе путь доставки программы атлету в v1 не закрыт (ревью №3, О-4-Б) |
|
||
| 5. Формула 1ПМ | **Эпли**: `1ПМ = w × (1 + reps/30)` | Стандарт индустрии; при reps ≤ 10 различие с Бжицки < 2 % |
|
||
| 6. UI телефона | **Jetpack Compose** + Material 3 (тёмная тема по умолчанию) | Актуальный стек Kotlin; крупные зоны касания — Min touch target 48 dp (Ф-3.6) |
|
||
| 7. Модели часов v1 | Таргет v1 — `fenix6x` (280×280, MIP, кнопки). Fenix 6 (260×260) и 6S (240×240) добавляются отдельными таргетами с собственными макетами (Ф-2.8) по фактическому парку (уточнение владельца 2026-10-17, изменение vision 1.5) | Якорные часы владельца — Fenix 6X; «одно разрешение 260×260» в ТЗ — описка исходника |
|
||
| 8. Сопоставление телефон ↔ .fit | Окно ± 15 минут по времени начала (Ф-8.4); при нескольких кандидатах — ближайший по времени | ✅ закрыт в requirements |
|
||
|
||
## 2. Форматы обмена
|
||
|
||
### 2.1. Конверт синхронизации (телефон → сервер)
|
||
|
||
`POST /api/v1/sync`, JSON-массив записей (пакет ≤ 50), каждая:
|
||
|
||
```json
|
||
{
|
||
"client_guid": "uuid-v4",
|
||
"entity": "workout | workout_set | exercise | program_version | profile | daily_log | break",
|
||
"op": "upsert",
|
||
"data": { ...схема сущности... }
|
||
}
|
||
```
|
||
|
||
Ответ: `{ "accepted": ["guid1", ...] }` (все или ничего — пакет
|
||
атомарен; 409 при конфликте версии программы — перезапись на телефоне
|
||
после перечитывания). Повтор пакета безопасен: сервер отвечает тем же
|
||
`accepted` по уже принятым GUID (идемпотентность, Ф-7.4). Р2-Б: PK
|
||
клиентских сущностей — UUID (генерирует телефон; строки «только .fit» —
|
||
сервер): ссылки дочерних записей — те же UUID, маппинг id не нужен.
|
||
|
||
### 2.2. Сущности `data` (ключевые поля)
|
||
|
||
- `exercise`: `exercise_id` (uuid), `name`, `group` (push|pull|legs|core|…),
|
||
`is_base` (bool), `is_active` (bool) — повторная отправка с
|
||
`is_active=false` — логическое удаление на сервере (З-41(а));
|
||
- `workout`: `workout_id` (uuid), `started_at`/`ended_at` (UTC ISO 8601),
|
||
`status` (active|finished|cancelled), `session_kind`
|
||
(normal|return|deload|test — Ф-14.5), `program_version_id` (nullable),
|
||
`avg_hr`;
|
||
- `workout_set`: `workout_id`, `exercise_id`, `seq`, `weight_kg`, `reps`,
|
||
`done_at`, `source` (voice|watch|edit), `in_plan` (bool), `rpe` (int? 1–10, З-7),
|
||
`knee_pain` (int? 0–10, только группа «ноги»);
|
||
- `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable);
|
||
- `break`: `kind` (NULL до ответа атлета), `source` (телефон передаёт
|
||
`manual` — объявление простоя заранее; `auto` проставляет сервер по
|
||
пропускам ≥ 4 дней, Ф-14.1), `started_on`, `planned_return`, `note`;
|
||
`ended_on` проставляет сервер первой сессией после возврата
|
||
(совпадает с `src.breaks`, З-17);
|
||
- `daily_log`: `date` (YYYY-MM-DD), `bp_sys`, `bp_dias`, `weight_kg`,
|
||
`hr_resting`, `wellbeing` (1–5 — Р1-А, поля v1; v2 добавит
|
||
`waist_cm`, `sleep_h`);
|
||
- `program_version`: JSON-схема программы из дизайна LLM-планирования
|
||
(разд. 2) + `athlete_id` (выводится из ключа, в теле не передаётся).
|
||
|
||
### 2.3. Канонический JSON тренировки (сервер, из .fit)
|
||
|
||
Из `FitParser` → `workout_facts.facts_json`: `session` (тип, старт,
|
||
длительность), `laps[]` (индекс, старт, длительность), `records[]`
|
||
(`t_offset_s`, `hr`), `sets[]` (упражнение/повторы/вес — если писались),
|
||
`fit_file_id`, `device`. Схема версионная: `"schema": "workout-facts/1"`.
|
||
|
||
### 2.4. CSV-выгрузка (AdminWeb)
|
||
|
||
Разделитель — `;`, кодировка UTF-8 c BOM (Excel), заголовок первой
|
||
строкой: `athlete;date;duration_min;avg_hr;sets;exercises;adherence_pct`.
|
||
Строка — тренировка; выборка = текущий фильтр таблицы.
|
||
|
||
## 3. Спецификация потребления Kit.Helpers
|
||
|
||
Библиотека — `3-app\1-lib\Kit.Helpers` (std1, net10.0, Npgsql 10);
|
||
потребление без изменений исходников (полная спецификация —
|
||
[kit-helpers-spec.md](kit-helpers-spec.md)):
|
||
|
||
- `Kit.Helpers.SqlHelperAbstract` (`SqlHelperPostgres`, fluent
|
||
`SqlHelperFluent`) — доступ к PostgreSQL (единственный путь к БД из
|
||
кода сервера; строка подключения — `Db.ConnectionString`; З-42 —
|
||
фактическое имя компонента);
|
||
- `Log.Logger` — файловые логи сервера (`Log.Path`, уровень `Info`);
|
||
в sync-контуре v1 используется `ILogger<SyncService>` (Kit `Log.Logger`
|
||
не настроен и без папки логов бросает — errata m04 №4; файловые логи —
|
||
последующие модули);
|
||
- `JsonHelper` — в v1 не потребляется (System.Text.Json напрямую, З-42);
|
||
- остальное (Cache, Http, Session) в v1 не потребляется.
|
||
|
||
## 4. Схема конфигурации и правила валидации
|
||
|
||
### 4.1. Телефон (Preferences)
|
||
|
||
| Параметр | Тип | Валидация |
|
||
|---|---|---|
|
||
| `Server.BaseUrl` | string | абсолютный https:// URL — иначе экран настроек не даёт сохранить |
|
||
| `Server.ApiKey` | string | непустой |
|
||
| `Sync.WifiOnly` | bool | — |
|
||
| `Hr.MaxBpm` | int? | 120–240 или пусто (расчёт по Танаке) |
|
||
| `Rest.DefaultSec` | int | 30–600 |
|
||
| `Safety.BpCancelSys / BpCancelDias` | int | умолчания 150/90 (документ метрик, Р3-А) |
|
||
| `Safety.BpCardioSys / BpCardioDias` | int | умолчания 140/90; ≤ BpCancel |
|
||
| `Safety.RestingHrSitting` | int | умолчание 90 |
|
||
| `Safety.HrStrengthMax` | int | 100–180 (умолчание 120, З-20) |
|
||
| `Safety.LightWeekHrRise` | int | умолчание 8 |
|
||
|
||
🛡 Р3-А: телефонные `Safety.*` — источник экрана допуска (офлайн);
|
||
серверные `Safety.*` (п. 4.2) — для серверных правил (`red_flags`);
|
||
совпадение значений — ответственность владельца конфигурации (общая
|
||
синхронизируемая сущность — кандидат v2).
|
||
|
||
### 4.2. Сервер (`appsettings.json`)
|
||
|
||
| Параметр | Тип | Валидация (startup, `--mode *`) |
|
||
|---|---|---|
|
||
| `Api.Key` | string | непустой; уникальность среди `Api.Athletes[].apiKey` |
|
||
| `Api.Athletes` | list | `{name}` непусто, `{apiKey}` непустой; дубликаты ключей — фатально |
|
||
| `Admin.Login / Admin.PasswordHash` | string | непустые; хэш — PBKDF2 (100k итераций), префикс `pbkdf2$` |
|
||
| `Db.ConnectionString` | string | тест подключения при старте — фатально при неудаче |
|
||
| `Garmin.Login/Password` | string | обязательны для `--mode fetch` (иначе код возврата 1) |
|
||
| `Garmin.DownloadDir / Import.Dir / Import.ArchiveDir` | path | создание при старте; недоступность — фатально |
|
||
| `Progression.BaseStepKg` | double | 0,5–10 |
|
||
| `Progression.IsoStepKg` | double | 0,25–5 (умолчание 1,0 — коридор метрик «+0,5–1 кг», решение владельца О-3-А, З-27) |
|
||
| `Progression.BackoffPercent` | int | 1–20 |
|
||
| `Deload.MaxWeeks` | int | 3–12 |
|
||
| `Safety.BpCancelSys / BpCancelDias` | int | 120–200 / 70–130 (умолчания 150/90) |
|
||
| `Safety.BpElevatedSys / BpElevatedDias` | int | умолчания 140/95 (З-37: переименовано из `BpCardio*`, Dias 95 по факту кода; проверка «≤ BpCancel» не применяется — коридоры АД документа метрик перекрываются, spec 01 m04) |
|
||
| `Safety.HrRestingDays` | int | 1–30 (умолчание 14) — окно базового пульса покоя |
|
||
| `Safety.HrBaselineMin` | int | 1–14 (умолчание 3) — минимум значений для базового |
|
||
| `Safety.WellbeingDays` | int | 1–7 (умолчание 2) — длина серии самочувствия |
|
||
| `Safety.LightDayCount` | int | 2–7 (умолчание 3) — порог отмены/лимит серии |
|
||
| `Safety.KneePainMin` | int | 1–5 (умолчание 1) |
|
||
| `Safety.KneeRepeatSessions` | int | 2–5 (умолчание 2) |
|
||
| `Progression.MissDaysForBreak` | int | 2–14 (умолчание 4, Ф-14.1) |
|
||
| `Progression.ReturnMinFactor / ReturnMaxFactor` | double | 0.3–0.9 / (0.5; 1] (умолчания 0.6/0.9) — границы коэффициента возврата |
|
||
| `Db.MigrationsDir` | path | каталог миграций `--mode migrate` (З-36); умолчание — migrations рядом с исполняемым файлом |
|
||
| `Log.Path / Log.Level` | — | уровень: Debug/Info/Error |
|
||
|
||
Примечания (З-37 п. 3): `Safety.RestingHrSitting`, `Safety.HrStrengthMax`,
|
||
`Safety.LightWeekHrRise` (разд. 4.1) — потребление — телефон (m09);
|
||
сервер в v1 не читает (мёртвых ключей в appsettings нет, Р3-А).
|
||
Регистр ключей — camelCase по факту конфигов (З-37 п. 5).
|
||
|
||
Валидация конфигурации выполняется при старте любого режима; ошибки —
|
||
в лог и stderr, код возврата 1 (раздел 2.3 requirements).
|
||
|
||
## 5. Технические решения по компонентам
|
||
|
||
- **`FitFetcher`**: реализация логина Garmin Connect (SSO-последовательность)
|
||
на C# — по контракту семьи библиотек `garminconnect` (подписи запросов,
|
||
OAuth1-заголовок); при блокировке — решение ОВ шага 7 (fallback: папка
|
||
импорта уже работает независимо). Пометка прототипа итерации 1.
|
||
- **`FitParser`**: официальный **Garmin FIT SDK for C#**; парсинг —
|
||
потоковый, файл читается один раз; сбой одного файла не останавливает
|
||
пакет (код возврата 2).
|
||
- **`SyncQueue`**: таблица Room `sync_pending(guid, entity, payload,
|
||
created_at)`; отправка — WorkManager (сетевое ограничение учитывает
|
||
`Sync.WifiOnly`); ретрай — экспоненциально, 1 мин → 64 мин.
|
||
- **`VoiceInput`**: Vosk-API (Kaldi) как service в процессе приложения;
|
||
грамматика — JSON-словарь `[«подход», «вес», «отдых», «на», «секунд»,
|
||
«с половиной», 0–999]`; числа — постобработка («сто двадцать» → 120;
|
||
«…с половиной» → +0,5 кг — решение владельца О-2-Б, З-26).
|
||
- **`AdminWeb`** (решение владельца 2026-10-17): отдельное Flutter
|
||
Web-приложение (`3-app\3-web\admin-web`); бэкенд — admin REST API в
|
||
`Server.WatchBody.Web` (`/api/admin/*`): логин (логин/пароль →
|
||
Bearer-токен сессии), атлеты (список/создание/смена ключа/
|
||
блокировка), тренировки, программы, метрики, CSV-выгрузка
|
||
(генерация CSV — на сервере, п. 2.4). Пароль — PBKDF2 (см. 4.2);
|
||
графики — штатные средства Flutter.
|
||
- **HTTPS**: обратный прокси (nginx/Caddy) терминирует TLS; Kestrel —
|
||
только localhost — вопрос развёртывания, вне кода.
|
||
|
||
## 6. UI/UX-стандарт (обязательные правила)
|
||
|
||
Источник — [ui-ux-recommendations.md](ui-ux-recommendations.md);
|
||
проверяемые требования:
|
||
|
||
1. **Часы:** семантика кнопок неизменна на всех экранах (UP/DOWN —
|
||
выбор, START — подтвердить, BACK — отмена); ключевое действие —
|
||
≤ 3 нажатий и ≤ 5 секунд; шрифт ключевых цифр ≥ 22–26 px
|
||
(`FONT_NUMBER_*`); предупреждения Safety закрываются только явной
|
||
кнопкой (не исчезают сами); вибро-паттерны различаются
|
||
(подход/отдых/предупреждение); планирование/графики/настройки на
|
||
часах запрещены.
|
||
2. **Телефон:** шрифт ≥ 16 pt, контраст WCAG AA (≥ 4.5:1), цели
|
||
касания ≥ 44 pt (требования Ф-3.6); действия одной руки — в нижней
|
||
трети экрана; пикеры с шагом 2,5 кг, умолчание — последний вес;
|
||
RPE/боль — сегмент-контролы; уведомления — только значимые;
|
||
настройка размера шрифта.
|
||
3. **Админ-консоль:** десктоп-first; подтверждение предложений —
|
||
дифф + «Принять/Отклонить/Править»; экспорт/бэкап одной кнопкой.
|
||
4. **Приёмка:** эвристическое ревью по 11 эвристикам (MDPI 2024) на
|
||
каждый экран; SUS ≥ 80 — порог релиза; приёмка часов — только на
|
||
реальном Fenix (симулятор — для разработки).
|
||
|
||
## 7. Чек-лист шага 7 (проверки прототипами)
|
||
|
||
1. Companion-канал: пульс/таймер доходят на телефон (ОВ-3) — итерация 1.
|
||
2. Логин Garmin Connect из C# (FitFetcher) — итерация 2.
|
||
3. Vosk: точность команд в шумном зале ≥ КУ-1 — итерация 2.
|
||
4. Сопоставление телефон ↔ .fit на реальной тренировке — итерация 3.
|