VEU.WathBody/1-docs/4-modules/m12-watch-channel/api_specs/01-watch-transport-ble.md

111 lines
6.2 KiB
Markdown
Raw 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.

# Спецификация 01: WatchTransport + HrBleClient (BLE Heart Rate Service)
Источники: module_reqs разд. 1.1/1 (транспорт-интерфейс, решение 6 —
мультичасовая инфраструктура), ОВ-3 errata, Ф-3.1–3.2. Единица 1.
## 1. Транспорт-интерфейс (платформо-независимый)
```kotlin
enum class TransportStatus {
IDLE, // не запущен (stop/до start)
SCANNING, // ищем устройство
CONNECTING, // gatt-подключение/обнаружение сервисов
CONNECTED, // подписка активна, сэмплы идут
LOST, // связь была и потеряна — автопереподключение
BT_OFF, // Bluetooth выключен
NO_PERMISSION, // нет требуемых разрешений (см. §6)
}
sealed interface TransportEvent {
data class Status(val status: TransportStatus) : TransportEvent
data class Sample(val bpm: Int, val atMs: Long) : TransportEvent
data class Battery(val percent: Int) : TransportEvent
}
interface WatchTransport {
val events: kotlinx.coroutines.flow.SharedFlow<TransportEvent>
fun start(deviceAddress: String) // адрес выбранного устройства
fun stop()
}
```
- интерфейс не содержит ничего BLE/Garmin-специфичного: будущие
транспорты (CIQ 5+ companion, ANT+) реализуют его без правок
фасада и UI (инвариант 6);
- события `Sample` приходят только в `CONNECTED`; `Battery` —
опционально (не все устройства exposing Battery Service);
- `LOST` сопровождается автоматическим повторным `SCANNING` —
фасад показывает «потерю», затем снова «скан».
## 2. BLE-реализация `HrBleClient`
Только стандартные профили GATT: Heart Rate Service
`0000180d-0000-1000-8000-00805f9b34fb` (Measurement
`0x2A37`, indications), Battery Service `0x180F` (Level `0x2A19`).
Никаких фильтров по имени/производителю — любое устройство
со стандартной трансляцией пульса (часы/строп) совместимо.
- **скан**: `ScanFilter` по UUID сервиса 0x180D; найден адрес
`deviceAddress` → подключение; не найден за 60 с → остаёмся
в `SCANNING` (продолжаем искать, статус не меняется);
- **подключение**: `connectGatt(autoConnect = false)`; при первой
ошибке — повтор с `autoConnect = true` (системное переподключение);
обнаружение сервисов → подписка indications на 0x2A37;
- **батарея**: если сервис 0x180F обнаружен — чтение 0x2A19 однократно
+ подписка notifications (если CCC поддерживается); нет сервиса —
событие `Battery` не приходит вовсе (прочерк, Ф-3.1);
- **потеря**: `onConnectionStateChange(DISCONNECTED)` после
`CONNECTED` → `LOST` → повторный скан с паузами 1/5/30 с
(далее каждые 30 с), пока не вызван `stop()`;
- **выключение Bluetooth**: `BT_OFF`; системный интент включения
(receiver при активном `start`) → автоматический переход
в `SCANNING`;
- `stop()` закрывает GATT и скан; повторный `start` начинается
с `SCANNING`.
## 3. Разбор пакета 0x2A37 — `HrPacketParser` (чистая функция)
```kotlin
object HrPacketParser {
data class HrPacket(val bpm: Int, val energyPresent: Boolean, val rrCount: Int)
fun parse(bytes: ByteArray): HrPacket?
}
```
- байт 0 — флаги: бит 0 — формат пульса (0 → uint8, 1 → uint16);
бит 1 — есть Energy Expended (uint16, пропускается); бит 2 —
есть RR-интервалы (n × uint16 в 1/1024 с, пропускаются);
- пульс вне 30–250 — пакет игнорируется (склейка/глюки датчика);
`null` — слишком короткий пакет;
- прочие флаги (зарезервированные биты) не разбираются.
## 4. Потоки и контекст
- колбэки BluetoothGatt доставляются на главном потоке; `events` —
`SharedFlow` ( replay 0, extraBufferCapacity 16 ), эмит без
приостановки (`tryEmit`); фасад — единственный слушатель;
- класс — Android-зависимый (только обёртка API); логика разбора —
в `HrPacketParser` (JVM), выбора пауз переподключения —
`ReconnectPolicy` (JVM: nextDelayMs(попытка) → 1000, 5000, 30000,
30000…).
## 5. Тесты (pure JVM)
- `HrPacketParser`: uint8/uint16, с Energy, с RR, с обоими,
короткий пакет → null, bpm 0/29/251 → пропуск, bpm 30/250 —
границы приняты;
- `ReconnectPolicy`: последовательность 1/5/30/30 с;
- интерфейсные события фасада — на фейке `WatchTransport` (спец 03).
## 6. Разрешения (статусы NO_PERMISSION/BT_OFF выставляет фасад)
| Android | Скан/подключение | Runtime |
|---|---|---|
| 12+ (31+) | `BLUETOOTH_SCAN` (neverForLocation) + `BLUETOOTH_CONNECT` | да |
| 8–11 (26–30) | `BLUETOOTH` + `BLUETOOTH_ADMIN` (манифест) + `ACCESS_FINE_LOCATION` | да (локация) |
Манифест: перечислены обе группы (`ACCESS_FINE_LOCATION`
без `maxSdkVersion` не ограничиваем — используется и другими
частями; `usesPermissionFlags="neverForLocation"` на SCAN).
Запрос — UI (спец 04); отказ → `NO_PERMISSION`, не блок.