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

8.2 KiB
Raw Permalink Blame History

Абстракция транспорта связи очки↔телефон (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 (или аналогичный файл):

/** Транспорт связи очки↔телефон. Реализации: 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, как сейчас). Это держит транспорт полностью независимым от протокола.

Для телефона — интерфейс приёма подключений:

/** Приёмник подключений (телефон). */
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, прогнать тесты, показать результат.