Внедрение rayneo-mercury: зависимость + MercuryPanel (BLE-тест: погасить/включить экран)
This commit is contained in:
@@ -94,6 +94,9 @@ dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(project(":lib-core"))
|
||||
|
||||
// RayNeo Mercury BLE protocol (RayNeo X2): pair/connect/команды с телефона
|
||||
implementation("pw.binom.mercury:rayneo-mercury:0.1.0-SNAPSHOT")
|
||||
|
||||
// Koog (JetBrains) — LLM-клиент (OpenAI-совместимый)
|
||||
implementation(libs.koog.openai.client)
|
||||
implementation(libs.koog.http.client.okhttp)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import pw.binom.mercury.DiscoveredGlasses
|
||||
import pw.binom.mercury.RayNeoMercury
|
||||
|
||||
/**
|
||||
* Мост к либе rayneo-mercury (протокол RayNeo Mercury по BLE).
|
||||
*
|
||||
* Минимум для полевого теста: скан → pair → connect → погасить/включить экран
|
||||
* (deepSuspend). События либы логируются и частично проксируются в [status].
|
||||
*
|
||||
* Кнопки экрана — только проверка транспорта: если очки принимают команду,
|
||||
* протокол жив, дальше можно вешать остальные команды (яркость, WiFi, магазин).
|
||||
*/
|
||||
object MercuryBridge {
|
||||
|
||||
private const val TAG = "MercuryBridge"
|
||||
|
||||
private var mercury: RayNeoMercury? = null
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
data class UiState(
|
||||
val connected: Boolean = false,
|
||||
val glassesName: String? = null,
|
||||
val statusText: String = "не подключено",
|
||||
val lastError: String? = null,
|
||||
)
|
||||
|
||||
private val _state = MutableStateFlow(UiState())
|
||||
val state: StateFlow<UiState> = _state.asStateFlow()
|
||||
|
||||
private var currentGlasses: DiscoveredGlasses? = null
|
||||
|
||||
/** Инициализация (вызывать из Application). */
|
||||
fun init(context: Context) {
|
||||
if (mercury != null) return
|
||||
mercury = RayNeoMercury(context.applicationContext, scope)
|
||||
scope.launch {
|
||||
mercury!!.events.collect { event ->
|
||||
when (event) {
|
||||
is RayNeoMercury.Event.Connected -> setStatus("подключено (BLE)")
|
||||
is RayNeoMercury.Event.Disconnected -> setStatus("отключено: ${event.cause}")
|
||||
is RayNeoMercury.Event.JsonMessage -> log("json", "${event.uri} ${event.json.take(200)}")
|
||||
is RayNeoMercury.Event.StreamPacket -> log("stream", "packet seq=${event.seq} type=${event.type} ${event.payload.size}B")
|
||||
is RayNeoMercury.Event.AppInstallStatus -> setStatus("app: ${event.pkg} status=${event.status} err=${event.errCode}")
|
||||
is RayNeoMercury.Event.AppInstallProgress -> log("app", "${event.pkg}: ${event.progress}%")
|
||||
is RayNeoMercury.Event.WirelessAdbAddress -> setStatus("ADB: ${event.address}")
|
||||
is RayNeoMercury.Event.LogcatLine -> log("logcat", event.line)
|
||||
is RayNeoMercury.Event.GeneralStatusUpdate -> log("status", "brightness=${event.status.brightness} deepSuspend=${event.status.deepSuspend}")
|
||||
is RayNeoMercury.Event.WifiScanResult -> log("wifi", "сетей: ${event.networks.size}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Прав на BLE нет — вернуть false; хост-приложение запрашивает само. */
|
||||
fun hasBlePermissions(context: Context): Boolean {
|
||||
val m = mercury ?: return false
|
||||
return m.hasPermissions() &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
/** Скан + pair (если нужно) + connect. Возвращает true при успехе. */
|
||||
suspend fun connectFirst(timeoutMillis: Long = 20_000): Boolean {
|
||||
val m = mercury ?: return false
|
||||
setStatus("сканирую очки…")
|
||||
val glasses = m.scanOnce(timeoutMillis)
|
||||
if (glasses == null) {
|
||||
setError("очки не найдены (скан $timeoutMillis мс)")
|
||||
return false
|
||||
}
|
||||
currentGlasses = glasses
|
||||
setStatus("найдены: ${glasses.name ?: glasses.address}")
|
||||
|
||||
val pairResult = m.pair(glasses)
|
||||
if (pairResult is pw.binom.mercury.pair.PairManager.PairingResult.Failed) {
|
||||
log("pair", "failed: ${pairResult.reason}")
|
||||
}
|
||||
|
||||
val ok = m.connect(glasses)
|
||||
if (!ok) setError("connect не удался")
|
||||
_state.value = _state.value.copy(connected = ok, glassesName = glasses.name)
|
||||
return ok
|
||||
}
|
||||
|
||||
suspend fun disconnect() {
|
||||
mercury?.disconnect()
|
||||
_state.value = _state.value.copy(connected = false)
|
||||
setStatus("отключено")
|
||||
}
|
||||
|
||||
/** Погасить экран очков (deepSuspend=1). */
|
||||
suspend fun screenOff(): Boolean {
|
||||
val ok = mercury?.setDeepSuspend(true) ?: false
|
||||
setStatus(if (ok) "команда «погасить экран» отправлена" else "ошибка отправки (нет связи?)")
|
||||
return ok
|
||||
}
|
||||
|
||||
/** Включить экран очков (deepSuspend=0). */
|
||||
suspend fun screenOn(): Boolean {
|
||||
val ok = mercury?.setDeepSuspend(false) ?: false
|
||||
setStatus(if (ok) "команда «включить экран» отправлена" else "ошибка отправки (нет связи?)")
|
||||
return ok
|
||||
}
|
||||
|
||||
private fun setStatus(text: String) {
|
||||
log("status", text)
|
||||
_state.value = _state.value.copy(statusText = text, lastError = null)
|
||||
}
|
||||
|
||||
private fun setError(text: String) {
|
||||
Log.e(TAG, text)
|
||||
_state.value = _state.value.copy(statusText = text, lastError = text)
|
||||
}
|
||||
|
||||
private fun log(tag: String, msg: String) {
|
||||
Log.d(TAG, "[$tag] $msg")
|
||||
}
|
||||
}
|
||||
@@ -425,6 +425,8 @@ class PhoneApp : Application() {
|
||||
PlaybackService.start(this)
|
||||
log("server", "playback-сервис запущен (foreground, WAKE_LOCK)")
|
||||
|
||||
MercuryBridge.init(this)
|
||||
|
||||
server.start()
|
||||
log("server", "WS-сервер запущен на ${PhoneConfig.SERVER_PORT}")
|
||||
DebugHttpServer.start(this)
|
||||
|
||||
@@ -360,6 +360,9 @@ private fun GlassesStatusCard(
|
||||
fg = if (playing) PhoneBlueText else PhoneMuted,
|
||||
)
|
||||
}
|
||||
|
||||
// Полевое испытание либы rayneo-mercury (BLE-протокол очков)
|
||||
MercuryPanel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package pw.binom.viewmate.phone.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import pw.binom.viewmate.phone.MercuryBridge
|
||||
|
||||
/**
|
||||
* Панель Mercury (BLE-протокол RayNeo X2) — полевое испытание либы rayneo-mercury.
|
||||
* Минимум: найти очки → подключить → погасить/включить экран (deepSuspend).
|
||||
*/
|
||||
@Composable
|
||||
fun MercuryPanel() {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state by MercuryBridge.state.collectAsState()
|
||||
val busy = false
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(PhoneCard)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "🛰 Mercury BLE (полевой тест)",
|
||||
color = PhoneText,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = state.statusText,
|
||||
color = if (state.lastError != null) PhoneRedText else PhoneMuted,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
if (state.connected) {
|
||||
Text(
|
||||
text = "очки: ${state.glassesName ?: "?"} · BLE готов",
|
||||
color = PhoneBlueText,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(top = 10.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
MercuryBridge.connectFirst()
|
||||
}
|
||||
},
|
||||
enabled = !busy,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PhoneBlue,
|
||||
contentColor = PhoneBlueText,
|
||||
),
|
||||
) {
|
||||
Text("подключить очки", fontSize = 12.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch { MercuryBridge.disconnect() }
|
||||
},
|
||||
enabled = state.connected,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PhoneSurface,
|
||||
contentColor = PhoneMuted,
|
||||
),
|
||||
) {
|
||||
Text("отключить", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(top = 8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch { MercuryBridge.screenOff() }
|
||||
},
|
||||
enabled = state.connected,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF2B2F36),
|
||||
contentColor = Color(0xFF9AA0A6),
|
||||
),
|
||||
) {
|
||||
Text("🌙 погасить экран", fontSize = 12.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch { MercuryBridge.screenOn() }
|
||||
},
|
||||
enabled = state.connected,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PhoneAccent,
|
||||
contentColor = Color(0xFF10251A),
|
||||
),
|
||||
) {
|
||||
Text("☀️ включить экран", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@ dependencyResolutionManagement {
|
||||
maven(url = "https://jitpack.io")
|
||||
mavenCentral()
|
||||
google()
|
||||
// Домашний Nexus: личные Android/KMP-библиотеки (rayneo-mercury и др.)
|
||||
maven(url = "http://192.168.76.117/repository/caffeine/") {
|
||||
isAllowInsecureProtocol = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user