Mercury: verify handshake echo, autolock endpoint, ringInfo-safe status

This commit is contained in:
2026-08-25 00:54:31 +03:00
parent b40e44c653
commit 4556d3b6e4
2 changed files with 85 additions and 22 deletions
@@ -61,7 +61,11 @@ object DebugHttpServer {
val parts = requestLine.split(" ") val parts = requestLine.split(" ")
if (parts.size < 2) return if (parts.size < 2) return
val method = parts[0] val method = parts[0]
val path = parts[1] val rawPath = parts[1]
val path = rawPath.substringBefore("?")
val queryParams = rawPath.substringAfter("?", "").split("&")
.filter { it.isNotEmpty() }
.associate { it.substringBefore("=") to it.substringAfter("=") }
// Тело (до Content-Length) // Тело (до Content-Length)
var contentLength = 0 var contentLength = 0
@@ -141,6 +145,12 @@ object DebugHttpServer {
response = """{"ok":$ok,"status":${jsonEscape(MercuryBridge.state.value.statusText)}}""" response = """{"ok":$ok,"status":${jsonEscape(MercuryBridge.state.value.statusText)}}"""
code = 200 code = 200
} }
method == "POST" && path.startsWith("/mercury/autolock") -> {
val time = queryParams["time"]?.toIntOrNull() ?: 10
val ok = kotlinx.coroutines.runBlocking { MercuryBridge.autoLockTime(time) }
response = """{"ok":$ok,"status":${jsonEscape(MercuryBridge.state.value.statusText)}}"""
code = 200
}
else -> { else -> {
response = """{"error":"нет такого пути: $method $path"}""" response = """{"error":"нет такого пути: $method $path"}"""
code = 404 code = 404
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.jsonObject
import pw.binom.mercury.DiscoveredGlasses import pw.binom.mercury.DiscoveredGlasses
import pw.binom.mercury.RayNeoMercury import pw.binom.mercury.RayNeoMercury
@@ -43,16 +44,38 @@ object MercuryBridge {
private var currentGlasses: DiscoveredGlasses? = null private var currentGlasses: DiscoveredGlasses? = null
/** Инициализация (вызывать из Application). */ /** Инициализация (вызывать из Application). Стартует GATT-сервер: очки сами нас найдут. */
fun init(context: Context) { fun init(context: Context) {
if (mercury != null) return if (mercury != null) return
mercury = RayNeoMercury(context.applicationContext, scope) mercury = RayNeoMercury(context.applicationContext, scope)
scope.launch { scope.launch {
mercury!!.events.collect { event -> mercury!!.events.collect { event ->
when (event) { when (event) {
is RayNeoMercury.Event.Connected -> setStatus("подключено (BLE)") is RayNeoMercury.Event.Connected -> {
is RayNeoMercury.Event.Disconnected -> setStatus("отключено: ${event.cause}") _state.value = _state.value.copy(connected = true)
is RayNeoMercury.Event.JsonMessage -> log("json", "${event.uri} ${event.json.take(200)}") setStatus("подключено (BLE)")
}
is RayNeoMercury.Event.Disconnected -> {
_state.value = _state.value.copy(connected = false)
setStatus("отключено: ${event.cause}")
}
is RayNeoMercury.Event.JsonMessage -> {
log("json", "${event.uri} ${event.json.take(200)}")
// Очки шлют ble.request.* и ждут ble.response.* с тем же id.
// Без ответа они рвут соединение — отвечаем эхом cmd/payload.
val uri = event.uri
val id = event.id
if (uri != null && id != null && uri.startsWith("ble.request.") &&
uri != "ble.request.general.status" && uri != "ble.request.general.settings"
) {
val respUri = uri.replace("ble.request.", "ble.response.")
val payload = parsePayload(event.json)
scope.launch {
mercury?.send(pw.binom.mercury.model.PayloadMessage(respUri, id, payload))
log("resp", "answered $uri -> $respUri")
}
}
}
is RayNeoMercury.Event.StreamPacket -> log("stream", "packet seq=${event.seq} type=${event.type} ${event.payload.size}B") 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.AppInstallStatus -> setStatus("app: ${event.pkg} status=${event.status} err=${event.errCode}")
is RayNeoMercury.Event.AppInstallProgress -> log("app", "${event.pkg}: ${event.progress}%") is RayNeoMercury.Event.AppInstallProgress -> log("app", "${event.pkg}: ${event.progress}%")
@@ -63,6 +86,9 @@ object MercuryBridge {
} }
} }
} }
val started = mercury!!.startServer()
log("server", if (started) "GATT-сервер + реклама запущены" else "GATT-сервер НЕ стартовал")
setStatus(if (started) "рекламирую сервис… жду очки" else "ошибка старта GATT-сервера")
} }
/** Прав на BLE нет — вернуть false; хост-приложение запрашивает само. */ /** Прав на BLE нет — вернуть false; хост-приложение запрашивает само. */
@@ -73,27 +99,39 @@ object MercuryBridge {
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} }
/** Скан + pair (если нужно) + connect. Возвращает true при успехе. */ /**
suspend fun connectFirst(timeoutMillis: Long = 20_000): Boolean { * Подключение: гарантирует, что GATT-сервер рекламируется, и ждёт, пока
* очки сами подключатся к нам (они действуют как GATT-клиент). Если очки
* всё же рекламируют — дополнительно пробуем скан-путь (клиент).
*/
suspend fun connectFirst(timeoutMillis: Long = 25_000): Boolean {
val m = mercury ?: return false val m = mercury ?: return false
setStatus("сканирую очки…") if (!m.isServerRunning && !m.startServer()) {
val glasses = m.scanOnce(timeoutMillis) setError("не удалось запустить GATT-сервер")
if (glasses == null) {
setError("очки не найдены (скан $timeoutMillis мс)")
return false return false
} }
currentGlasses = glasses setStatus("рекламирую сервис… жду очки")
setStatus("найдены: ${glasses.name ?: glasses.address}") val deadline = System.currentTimeMillis() + timeoutMillis
while (System.currentTimeMillis() < deadline) {
val pairResult = m.pair(glasses) if (m.isAnyConnected) {
if (pairResult is pw.binom.mercury.pair.PairManager.PairingResult.Failed) { _state.value = _state.value.copy(connected = true)
log("pair", "failed: ${pairResult.reason}") setStatus("подключено (BLE)")
return true
} }
// На случай, если очки рекламируются сами — клиентский путь.
val ok = m.connect(glasses) val glasses = m.scanOnce(3000)
if (!ok) setError("connect не удался") if (glasses != null) {
_state.value = _state.value.copy(connected = ok, glassesName = glasses.name) setStatus("найдены: ${glasses.name ?: glasses.address}, подключаюсь…")
return ok currentGlasses = glasses
if (m.connect(glasses, 8000)) {
_state.value = _state.value.copy(connected = true, glassesName = glasses.name)
setStatus("подключено (клиент)")
return true
}
}
}
setError("очки не подключились за ${timeoutMillis / 1000} с")
return false
} }
suspend fun disconnect() { suspend fun disconnect() {
@@ -116,6 +154,13 @@ object MercuryBridge {
return ok return ok
} }
/** Автоблокировка экрана через N секунд; -1 = никогда. */
suspend fun autoLockTime(time: Int): Boolean {
val ok = mercury?.setAutoLockTime(time) ?: false
setStatus(if (ok) "auto_lock_time=$time отправлен" else "ошибка отправки (нет связи?)")
return ok
}
private fun setStatus(text: String) { private fun setStatus(text: String) {
log("status", text) log("status", text)
_state.value = _state.value.copy(statusText = text, lastError = null) _state.value = _state.value.copy(statusText = text, lastError = null)
@@ -126,6 +171,14 @@ object MercuryBridge {
_state.value = _state.value.copy(statusText = text, lastError = text) _state.value = _state.value.copy(statusText = text, lastError = text)
} }
private fun parsePayload(json: String): pw.binom.mercury.model.Payload? = try {
val obj = kotlinx.serialization.json.Json.parseToJsonElement(json).jsonObject
val el = obj["payload"] ?: return null
kotlinx.serialization.json.Json.decodeFromJsonElement(pw.binom.mercury.model.Payload.serializer(), el)
} catch (_: Exception) {
null
}
private fun log(tag: String, msg: String) { private fun log(tag: String, msg: String) {
Log.d(TAG, "[$tag] $msg") Log.d(TAG, "[$tag] $msg")
} }