- 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) — зелёные
8.2 KiB
Абстракция транспорта связи очки↔телефон (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).
Цель
Выделить интерфейс транспорта, одинаковый для обеих сторон, и две реализации:
- WifiTransport — обёртка над существующим кодом (без изменения логики)
- 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, прогнать тесты, показать результат.