VEU.WathBody/1-docs/4-modules/m12-watch-channel/api_specs/03-watch-channel-facade.md

105 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Спецификация 03: WatchChannel — фасад канала
Источники: module_reqs разд. 1 (фасад, жизненный цикл, решения 4/5),
Ф-3.1/3.2, Ф-13.2, инвариант 3 (канал не пишет в БД). Единицы 3, 8.
## 1. Состояние
```kotlin
enum class WatchStatus { NOT_SELECTED, NO_PERMISSION, BT_OFF,
SCANNING, CONNECTED, NO_SIGNAL }
data class WatchChannelState(
val status: WatchStatus,
val hr: Int?, // свежий сэмпл (≤10 с) или null
val battery: Int?, // последний Battery-событие или null
)
```
Маппинг статусов Ф-3.1 (цвет — только дублирование 🛡):
| WatchStatus | Цвет | Текст статус-строки |
|---|---|---|
| CONNECTED + hr | зелёный | «часы: связь есть (заряд N %)» |
| CONNECTED без свежего hr | серый | «часы: подключены, нет сигнала» |
| NO_SIGNAL (после потери) | красный | «часы: нет связи» |
| SCANNING / CONNECTING | серый | «часы: поиск…» |
| NOT_SELECTED | серый | «часы: не выбраны» |
| BT_OFF / NO_PERMISSION | серый | «Bluetooth выключен» / «нет разрешения» |
## 2. Класс
```kotlin
class WatchChannel(
private val transport: WatchTransport, // HrBleClient (прод)
private val store: HrSampleStore,
) {
val state: StateFlow<WatchChannelState>
fun bind(workoutStartedAtMs: Long?) // экран Тренировка (есть дата) / Допуск (null)
fun unbind()
fun avgHr(nowMs: Long): Int? // среднее окна тренировки
fun freshHr(nowMs: Long, maxAgeMs: Long = 30_000): Int? // допуск Ф-13.2
}
```
- `bind`: подписка на `transport.events`; сэмплы → `store.add`;
Battery → state; статусы → state (приоритет блокировок:
NO_PERMISSION/BT_OFF > NOT_SELECTED > события транспорта);
затем `transport.start(address)` если адрес задан и разрешения
есть; повторный `bind` — no-op;
- `NOT_SELECTED`: адрес пуст (`Watch.HrDeviceAddress` = "") —
транспорт не стартует, статус «не выбраны» (в зале чужие
датчики — авто-подключение к первому найденному запрещено,
решение 5 гейта 1.3);
- `unbind`: `transport.stop()`; буфер НЕ чистится (среднее нужно
до финишированной записи — `clear` по факту финиша из VM);
- `avgHr`: `store.average(workoutStartedAtMs, nowMs)` — VM
передаёт в `WorkoutCore.finishWorkout(id, avgHr)` (цепочка
m09: `workout.avg_hr` → sync `avg_hr`; контракт не меняется);
- `freshHr`: последний сэмпл ≤30 с — для кнопки «взять с часов»
на «Допуске»;
- **инвариант 3**: фасад ничего не пишет в LocalDb/sync; единственная
точка записи — существующий `finishWorkout(avgHr)` со стороны VM.
## 3. Разрешения и адаптер (статусы без транспорта)
Проверки на каждый `bind` (и на возврат в foreground):
- 12+: не выданы `BLUETOOTH_SCAN`+`BLUETOOTH_CONNECT` →
`NO_PERMISSION` (UI показывает кнопку запроса, спец 04);
- 8–11: нет `ACCESS_FINE_LOCATION` → `NO_PERMISSION`;
- адаптер выключен → `BT_OFF` (при включении — интент из
спец 01 §2 перезапускает скан).
## 4. Интеграция (точки m10/m09)
- VM «Тренировки»: `DisposableEffect { bind(workout?.startedAtMs);
onDispose { unbind() } }`; статус-строка/пульс/линия — state +
`store.series(lastSetAtMs, now)`; `finishWorkout` вызывается
с `avgHr(now)`; после финиша `store.clear()`;
- VM «Допуска»: `bind(null)` на экране; кнопка «взять с часов»
активна при `freshHr != null`; `unbind` на выходе;
- контейнер (WatchBodyApp): `watchChannel` — singleton;
`HrBleClient` инжектится как `WatchTransport` (подмена фейком
в тестах; будущие транспорты — той же инъекцией, инвариант 6).
## 5. Настройка
`Watch.HrDeviceAddress: String` (DataStore, умолчание `""`):
BLE-адрес выбранного устройства; запись из блока «Часы» в
«Настройках» (спец 04); сброс — запись `""`. Валидация адреса
не требуется (несуществующий адрес = вечный SCANNING, читается
статусом).
## 6. Тесты (pure JVM, фейк WatchTransport)
- переходы: start(SCANNING)→CONNECTED→Sample→state.hr;
- потеря: CONNECTED→LOST→SCANNING, state «нет связи» → «поиск…»;
- NOT_SELECTED при пустом адресе — transport.start не вызывается;
- NO_PERMISSION/BT_OFF перекрывают события транспорта;
- устаревание hr (>10 с) — hr=null при CONNECTED;
- avgHr по окну тренировки (сэмплы до старта исключены);
freshHr 30 с — граница;
- unbind вызывает transport.stop; clear после финиша — из VM
(ручной сценарий).