bbf7a020d4
- BT (RFCOMM SPP 00001105) вместо WiFi: BtGlassesTransport/BtServerTransport, реконнект 3с - PlaybackService (foreground, WAKE_LOCK) против морозов HONOR - Ассистент: AgentTools (TimeTool ISO 8601), TOOL_CALL-протокол, сессии SQLite (UUID) - Чат-UI: создание/сброс/переключение сессий, автообновление ленты 2с - LlmClient на прямом OkHttp (Koog давал пустой content на длинных историях) - Голосовой цикл: тройной тап на очках → STT → активная сессия → ShowText - Тесты: AgentToolsTest (3), GlassesServerTest переписан (6) — зелёные
104 lines
8.2 KiB
Markdown
104 lines
8.2 KiB
Markdown
# Абстракция транспорта связи очки↔телефон (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<DefaultWebSocketServerSession, Unit>, 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`, прогнать тесты, показать результат.
|