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

6.2 KiB
Raw Blame History

Спецификация 01: WatchTransport + HrBleClient (BLE Heart Rate Service)

Источники: module_reqs разд. 1.1/1 (транспорт-интерфейс, решение 6 — мультичасовая инфраструктура), ОВ-3 errata, Ф-3.1–3.2. Единица 1.

1. Транспорт-интерфейс (платформо-независимый)

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 (чистая функция)

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, не блок.