Files
view-mate/TASK-transport.md
subochev bbf7a020d4 Ассистент Порфирий: тула времени, сессии, чат-UI, голосовой цикл; BT-транспорт очки↔телефон
- 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) — зелёные
2026-08-22 16:31:04 +03:00

104 lines
8.2 KiB
Markdown
Raw Permalink 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.
# Абстракция транспорта связи очки↔телефон (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`, прогнать тесты, показать результат.