feat: сетевая часть для app-phone (WS-сервер) и app-glasses (WS-клиент), PhoneState/PhoneActions → lib-core

lib-core:
- core/phone/: PhoneState, PhoneActions, GlassesSender (интерфейс) — перенесены из app-host
- core/net/: GlassesWsClient — KMP WS-клиент очков (реконнект, Hello, send GlassesToHost)
- GlassesWsClientTest + PhoneActionsTest (перенесён)

app-phone (Android):
- GlassesServer: Ktor WS-сервер (CIO) 0.0.0.0:8080/ws/glasses, принимает очки, Welcome из стейта
- PhoneApp (Application): клиенты, стейт, действия, сервер
- PhoneConfig: креды Jellyfin/Mirror
- MainActivity: статус сервера (порт, счётчик подключений)
- GlassesServerTest: hello→welcome, gesture

app-glasses (Android):
- GlassesApp (Application), HostConnection: WS-клиент к телефону, Hello, реконнект, обработка HostToGlasses (PlayVideo/SetMode и т.д.), StateFlow для UI
- MainActivity: статус подключения

app-host: PhoneActions/PhoneState удалены (теперь lib-core), адаптирован под GlassesSender

171 тест, 0 провалов; app-host жив (catalog/watch/status работают через lib-core)
This commit is contained in:
Hermes Agent
2026-08-11 15:06:07 +03:00
parent f0f1f71575
commit 298d8e0b69
27 changed files with 1165 additions and 118 deletions
@@ -0,0 +1,102 @@
package pw.binom.viewmate.glasses
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import pw.binom.viewmate.core.net.GlassesWsClient
import pw.binom.viewmate.core.protocol.AssistantStateMsg
import pw.binom.viewmate.core.protocol.ChatSessionMsg
import pw.binom.viewmate.core.protocol.GlassesStatus
import pw.binom.viewmate.core.protocol.Hello
import pw.binom.viewmate.core.protocol.HostToGlasses
import pw.binom.viewmate.core.protocol.MediaCommandMsg
import pw.binom.viewmate.core.protocol.PlayVideo
import pw.binom.viewmate.core.protocol.PlaybackPosition
import pw.binom.viewmate.core.protocol.SetMode
import pw.binom.viewmate.core.protocol.ShowText
import pw.binom.viewmate.core.protocol.Welcome
/**
* Обвязка над [GlassesWsClient] для UI очков:
* реконнект, Hello при подключении, периодический статус/позиция,
* обработка входящих HostToGlasses в StateFlow (пока — лог).
*/
class HostConnection(
private val url: String,
) {
private val client = GlassesWsClient(url)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val _connected = MutableStateFlow(false)
val connected: StateFlow<Boolean> = _connected.asStateFlow()
private val _status = MutableStateFlow("не подключено")
val status: StateFlow<String> = _status.asStateFlow()
private val _messages = MutableStateFlow<List<String>>(emptyList())
val messages: StateFlow<List<String>> = _messages.asStateFlow()
fun start() {
scope.launch {
client.connect(
onHostMessage = { msg -> handleHostMessage(msg) },
onConnected = {
_connected.value = true
_status.value = "подключено: $url"
addMessage("соединение установлено")
client.send(Hello(appVersion = "0.1.0"))
},
onDisconnected = {
_connected.value = false
_status.value = "нет соединения (реконнект через 3с)"
},
)
}
scope.launch {
while (isActive) {
delay(5_000)
// статус/позиция — заглушки (реальные батарея/память/таймкод — позже)
client.send(GlassesStatus(batteryPercent = 80, storageUsedGb = 10.5, storageTotalGb = 32.0))
client.send(PlaybackPosition(positionMs = 0, playing = false))
}
}
}
private suspend fun handleHostMessage(msg: HostToGlasses) {
when (msg) {
is Welcome -> {
_connected.value = true
addMessage("Welcome: режим ${msg.mode}, сессия ${msg.sessionId ?: "-"}")
}
is SetMode -> addMessage("SetMode: ${msg.mode}")
is PlayVideo -> addMessage("получено видео: «${msg.title}» — ${msg.videoUrl}")
is MediaCommandMsg -> addMessage("MediaCommand: ${msg.command} (seekMs=${msg.seekMs})")
is AssistantStateMsg -> addMessage("AssistantState: ${msg.state} (${msg.recordingSeconds}с)")
is ShowText -> addMessage("ShowText: ${msg.text}")
is ChatSessionMsg -> addMessage("ChatSession: ${msg.sessionId} — ${msg.title}")
}
}
private fun addMessage(text: String) {
log("glasses", text)
_messages.value = (_messages.value + "» $text").takeLast(50)
}
fun stop() {
client.close()
scope.cancel()
}
}