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:
@@ -0,0 +1,32 @@
|
||||
package pw.binom.viewmate.glasses
|
||||
|
||||
import android.app.Application
|
||||
|
||||
/** Конфигурация очков: адрес WS-сервера телефона. Креды не нужны — очки просто экран. */
|
||||
object GlassesConfig {
|
||||
// Эмулятор — 10.0.2.2 (localhost хоста); на устройстве — LAN-адрес телефона.
|
||||
const val HOST_URL = "ws://10.0.2.2:8080/ws/glasses"
|
||||
}
|
||||
|
||||
/**
|
||||
* Application очков: создаёт и запускает HostConnection (WS-клиент к телефону).
|
||||
* Синглтон-доступ через companion.
|
||||
*/
|
||||
class GlassesApp : Application() {
|
||||
|
||||
lateinit var hostConnection: HostConnection
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
hostConnection = HostConnection(GlassesConfig.HOST_URL)
|
||||
hostConnection.start()
|
||||
log("app", "HostConnection стартует: ${GlassesConfig.HOST_URL}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: GlassesApp
|
||||
private set
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package pw.binom.viewmate.glasses
|
||||
|
||||
/** Единый лог очков: простой println (работает и в unit-тестах). */
|
||||
fun log(tag: String, message: String) {
|
||||
println("[view-mate] $tag $message")
|
||||
}
|
||||
@@ -1,12 +1,53 @@
|
||||
package pw.binom.viewmate.glasses
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
class MainActivity : Activity() {
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Точка входа на очках: инициализация RayNeo SDK, режимы, связь.
|
||||
// Пока — заглушка, заполняется на следующем шаге.
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
ConnectionScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectionScreen() {
|
||||
val connection = GlassesApp.instance.hostConnection
|
||||
val connected by connection.connected.collectAsState()
|
||||
val status by connection.status.collectAsState()
|
||||
val messages by connection.messages.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("ViewMate Glasses", style = MaterialTheme.typography.headlineMedium)
|
||||
Text(if (connected) "подключено к телефону" else "нет соединения")
|
||||
Text(status, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("Последние сообщения:", style = MaterialTheme.typography.titleMedium)
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
items(messages) { msg ->
|
||||
Text(msg, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user