diff --git a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/DebugHttpServer.kt b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/DebugHttpServer.kt index 0a26f60..e55d888 100644 --- a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/DebugHttpServer.kt +++ b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/DebugHttpServer.kt @@ -61,7 +61,11 @@ object DebugHttpServer { val parts = requestLine.split(" ") if (parts.size < 2) return 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) var contentLength = 0 @@ -141,6 +145,12 @@ object DebugHttpServer { response = """{"ok":$ok,"status":${jsonEscape(MercuryBridge.state.value.statusText)}}""" 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 -> { response = """{"error":"нет такого пути: $method $path"}""" code = 404 diff --git a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/MercuryBridge.kt b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/MercuryBridge.kt index d96db9b..5344d46 100644 --- a/app-phone/src/main/kotlin/pw/binom/viewmate/phone/MercuryBridge.kt +++ b/app-phone/src/main/kotlin/pw/binom/viewmate/phone/MercuryBridge.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.serialization.json.jsonObject import pw.binom.mercury.DiscoveredGlasses import pw.binom.mercury.RayNeoMercury @@ -43,16 +44,38 @@ object MercuryBridge { private var currentGlasses: DiscoveredGlasses? = null - /** Инициализация (вызывать из Application). */ + /** Инициализация (вызывать из Application). Стартует GATT-сервер: очки сами нас найдут. */ 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.Connected -> { + _state.value = _state.value.copy(connected = true) + 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.AppInstallStatus -> setStatus("app: ${event.pkg} status=${event.status} err=${event.errCode}") 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; хост-приложение запрашивает само. */ @@ -73,27 +99,39 @@ object MercuryBridge { 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 - setStatus("сканирую очки…") - val glasses = m.scanOnce(timeoutMillis) - if (glasses == null) { - setError("очки не найдены (скан $timeoutMillis мс)") + if (!m.isServerRunning && !m.startServer()) { + setError("не удалось запустить GATT-сервер") 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}") + setStatus("рекламирую сервис… жду очки") + val deadline = System.currentTimeMillis() + timeoutMillis + while (System.currentTimeMillis() < deadline) { + if (m.isAnyConnected) { + _state.value = _state.value.copy(connected = true) + setStatus("подключено (BLE)") + return true + } + // На случай, если очки рекламируются сами — клиентский путь. + val glasses = m.scanOnce(3000) + if (glasses != null) { + setStatus("найдены: ${glasses.name ?: glasses.address}, подключаюсь…") + currentGlasses = glasses + if (m.connect(glasses, 8000)) { + _state.value = _state.value.copy(connected = true, glassesName = glasses.name) + setStatus("подключено (клиент)") + return true + } + } } - - val ok = m.connect(glasses) - if (!ok) setError("connect не удался") - _state.value = _state.value.copy(connected = ok, glassesName = glasses.name) - return ok + setError("очки не подключились за ${timeoutMillis / 1000} с") + return false } suspend fun disconnect() { @@ -116,6 +154,13 @@ object MercuryBridge { 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) { log("status", text) _state.value = _state.value.copy(statusText = text, lastError = null) @@ -126,6 +171,14 @@ object MercuryBridge { _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) { Log.d(TAG, "[$tag] $msg") }