# Технические требования: WatchBody («Феникс 6X») v1 > Шаг 5 процесса разработки (`std1\_process\process.md`). > Источники: [vision.md](../1-reqs/vision.md) v1.2, > [requirements.md](../1-reqs/requirements.md) (утверждён, раздел 7 — > открытые вопросы), [architecture.md](../2-arch/architecture.md) > (утверждена), дизайн LLM-планирования, документ форматов. > Версия: черновик 1 (2026-10-17). ## 1. Решения открытых вопросов (закрытие раздела 7 requirements) | # ОВ | Решение | Обоснование | |---|---|---| | 1. Максимальная ЧСС | Формула Танаки: `HRmax = 208 − 0,7 × возраст`; ручной ввод `Hr.MaxBpm` перекрывает расчёт | Точнее для 45+ (профиль пользователя); ручной ввод — окончательная истина | | 2. Офлайн-STT | **Vosk** (малая русская модель ~50 МБ) + узкая грамматика команд (Ф-1.2: числа и слова «подход/вес/отдых/секунд»); 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`; внутри сервера планировщика нет | Проще, надёжнее, не дублирует средства ОС | | 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 | Таргет `fenix6` (Fenix 6/6S/6X, включая Pro/Sapphire — одно разрешение 260×260); добавление моделей — новыми таргетами по фактическому парку (шаг 7) | Якорное устройство ТЗ; 6S/6X отличаются размером корпуса, не экраном | | 8. Сопоставление телефон ↔ .fit | Окно ± 15 минут по времени начала (Ф-8.4); при нескольких кандидатах — ближайший по времени | ✅ закрыт в requirements | ## 2. Форматы обмена ### 2.1. Конверт синхронизации (телефон → сервер) `POST /api/v1/sync`, JSON-массив записей (пакет ≤ 50), каждая: ```json { "client_guid": "uuid-v4", "entity": "workout | exercise | program_version | profile | workout_set | set_note", "op": "upsert", "data": { ...схема сущности... } } ``` Ответ: `{ "accepted": ["guid1", ...] }` (все или ничего — пакет атомарен; 409 при конфликте версии программы — перезапись на телефоне после перечитывания). Повтор пакета безопасен: сервер отвечает тем же `accepted` по уже принятым GUID (идемпотентность, Ф-7.4). ### 2.2. Сущности `data` (ключевые поля) - `exercise`: `exercise_id` (uuid), `name`, `group` (push|pull|legs|core|…), `is_base` (bool), `is_active` (bool); - `workout`: `workout_id` (uuid), `started_at`/`ended_at` (UTC ISO 8601), `status` (active|finished|cancelled), `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); - `profile`: `sex`, `birth_year`, `weight_kg`, `hr_max` (nullable); - `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)): - `Db.Postgres.SqlServerHelper` — доступ к PostgreSQL (единственный путь к БД из кода сервера; строка подключения — `Db.ConnectionString`); - `Log.Logger` — файловые логи сервера (`Log.Path`, уровень `Info`); - `JsonHelper` — сериализация фактов/программ; - остальное (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 | ### 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.BackoffPercent` | int | 1–20 | | `Deload.MaxWeeks` | int | 3–12 | | `Log.Path / Log.Level` | — | уровень: Debug/Info/Error | Валидация конфигурации выполняется при старте любого режима; ошибки — в лог и 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). - **`AdminWeb`**: Razor Pages, сессия по cookie после логина; пароль — PBKDF2 (см. 4.2); графики — Chart.js (локальный ассет, без CDN). - **HTTPS**: обратный прокси (nginx/Caddy) терминирует TLS; Kestrel — только localhost — вопрос развёртывания, вне кода. ## 6. Чек-лист шага 7 (проверки прототипами) 1. Companion-канал: пульс/таймер доходят на телефон (ОВ-3) — итерация 1. 2. Логин Garmin Connect из C# (FitFetcher) — итерация 2. 3. Vosk: точность команд в шумном зале ≥ КУ-1 — итерация 2. 4. Сопоставление телефон ↔ .fit на реальной тренировке — итерация 3.