# Абстракция транспорта связи очки↔телефон (view-mate) ## Проблема Сейчас связь очки↔телефон жёстко завязана на **WiFi + WebSocket**: - очки: `GlassesWsClient` (Ktor WS-клиент) внутри `HostConnection`, discovery = mDNS (`MdnsClient`) + скан /24 (`PhoneScan`) - телефон: `GlassesServer` (Ktor CIO embeddedServer, WS на 0.0.0.0:8080/ws/glasses) + `GlassesHub` (сессии = `DefaultWebSocketServerSession`) WiFi на реальном железе нестабилен (пользователь: «wifi в рамках view-mate — неверное решение, блютус на много стабильнее»). Нужно сделать транспорт **абстрактным**, чтобы реализацию можно было подставить: **WifiTransport** (текущий WS) и **BluetoothTransport** (новый, RFCOMM/BluetoothSocket). ## Цель Выделить интерфейс транспорта, одинаковый для обеих сторон, и две реализации: 1. **WifiTransport** — обёртка над существующим кодом (без изменения логики) 2. **BluetoothTransport** — новая реализация на Android BluetoothSocket (RFCOMM) Протокол сообщений (HostToGlasses/GlassesToHost, kotlinx.serialization JSON) — **НЕ трогать** (пересылаются как JSON-строки, что WS, что BT-сокет). ## Текущая структура (не менять без нужды, только рефакторинг) **lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/** - `net/GlassesWsClient.kt` — WS-клиент очков: `suspend connect(onHostMessage, onConnected, onDisconnected)` (бесконечный цикл приёма с реконнектом 3с), `suspend send(msg: GlassesToHost)`, `close()` - `phone/GlassesSender.kt` — интерфейс телефона: `interface GlassesSender { suspend fun send(msg: HostToGlasses) }` - `protocol/` — HostToGlasses, GlassesToHost, Json (protocolJson), MediaCommand и т.д. **app-glasses/src/main/kotlin/pw/binom/viewmate/glasses/** - `HostConnection.kt` — обвязка: discovery (MdnsClient → PhoneScan), connectLoop, реконнект, Hello, статус-цикл 5с (buildStatus + PlaybackPosition), обработка HostToGlasses (handleHostMessage), reconnectResume-автоматика - `MdnsClient.kt`, `PhoneScan.kt` — WiFi-discovery **app-phone/src/main/kotlin/pw/binom/viewmate/phone/** - `GlassesServer.kt` — `GlassesHub` (ConcurrentHashMap, connected/gesturesReceived/batteryPercent, broadcast, обработка входящих: Hello→Welcome, жесты, позиция, GlassesOff→пауза) + `GlassesServer` (embeddedServer CIO + glassesServerModule + sender-адаптер GlassesSender поверх hub.broadcast) - `NsdPublisher.kt` — mDNS-публикация ## Задача ### 1. Интерфейсы транспорта (lib-core, commonMain) Создать `core/net/GlassesTransport.kt` (или аналогичный файл): ```kotlin /** Транспорт связи очки↔телефон. Реализации: WiFi (WS), Bluetooth (RFCOMM). */ interface GlassesTransport { val name: String // "wifi" / "bluetooth" suspend fun connect( onMessage: suspend (String) -> Unit, // входящее JSON-сообщение onConnected: suspend () -> Unit, onDisconnected: suspend () -> Unit, ) suspend fun send(json: String) fun close() } ``` Сообщения передавать **JSON-строками** (декодирование остаётся на стороне вызывающего — в HostConnection и GlassesHub, как сейчас). Это держит транспорт полностью независимым от протокола. Для телефона — интерфейс приёма подключений: ```kotlin /** Приёмник подключений (телефон). */ interface GlassesServerTransport { val name: String suspend fun accept( onMessage: suspend (connId: String, json: String) -> Unit, onConnected: suspend (connId: String) -> Unit, onDisconnected: suspend (connId: String) -> Unit, ) suspend fun send(connId: String, json: String) fun close() } ``` (connId — идентификатор сессии; для WiFi можно использовать адрес/хэш, для BT — MAC-адрес устройства.) ### 2. WifiTransport (рефакторинг существующего) - **Очки**: `WifiGlassesTransport` — обёртка над `GlassesWsClient`: connect пробрасывает колбэки, но строки (не декодированные объекты). Либо изменить `GlassesWsClient` на работу со строками — на твоё усмотрение, главное чтобы HostConnection перешёл на интерфейс. - **Телефон**: `WifiServerTransport` — обёртка над Ktor-сервером: существующий `glassesServerModule`/`GlassesHub` переводится на интерфейс (сессии — connId-строки вместо DefaultWebSocketServerSession). ### 3. BluetoothTransport (новая реализация) **Очки (клиент):** - `BtGlassesTransport` — подключается к телефону через BluetoothSocket (RFCOMM, SPP): `BluetoothAdapter.getDefaultAdapter()`, найти сервис/устройство (bonded devices + поиск), `createRfcommSocketToServiceRecord(SPP_UUID)`, connect, потоки: читать строки (по разделителю/длине), писать строки. - Discovery: bonded-устройства телефона (первый подходящий) или поиск (startDiscovery) — достаточно начать с bonded. - Пермишены: BLUETOOTH, BLUETOOTH_ADMIN (до Android 12), BLUETOOTH_CONNECT/BLUETOOTH_SCAN (Android 12+, runtime-запрос). **Телефон (сервер):** - `BtServerTransport` — BluetoothServerSocket (RFCOMM, SPP, тот же UUID), accept-цикл, потоки чтения/записи на каждое подключение, connId = MAC устройства. - Пермишены аналогично + FOREGROUND_SERVICE_CONNECTED_DEVICE (если нужно). ### 4. Соединение (фабрика/выбор) - В `HostConnection` и `GlassesServer` — параметр транспорта (интерфейс), по умолчанию WifiTransport (чтобы не сломать текущее поведение). Выбор: константа/конфиг/аргумент конструктора. **По умолчанию — WiFi**, Bluetooth включается флагом (пользователь будет переключать вручную после проверки). - Discovery (MdnsClient/PhoneScan/NsdPublisher) — оставить как часть WifiTransport; для BT — свой (bonded). ### 5. Сборка и тесты - Существующие тесты (GlassesServerTest, HostToGlassesTest, GlassesToHostTest, PhoneActionsTest и др.) должны остаться зелёными — поведение WiFi не меняется. - Добавить юнит-тесты на чистую логику (если есть что тестировать без Android: например, сериализация строк, выбор транспорта). ## Ограничения - **НЕ менять протокол** (HostToGlasses/GlassesToHost/Json) — только способ доставки. - НЕ ломать существующий WiFi-путь: он остаётся дефолтным и должен работать как раньше. - Kotlin, стиль проекта (детали в соседних файлах). - После рефакторинга — собрать `:app-phone:assembleDebug` и `:app-glasses:assembleDebug`, прогнать тесты, показать результат.