111 lines
6.2 KiB
Markdown
111 lines
6.2 KiB
Markdown
# Спецификация 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`, не блок.
|