Mercury: verify handshake echo, autolock endpoint, ringInfo-safe status
This commit is contained in:
@@ -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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setError("очки не подключились за ${timeoutMillis / 1000} с")
|
||||||
val ok = m.connect(glasses)
|
return false
|
||||||
if (!ok) setError("connect не удался")
|
|
||||||
_state.value = _state.value.copy(connected = ok, glassesName = glasses.name)
|
|
||||||
return ok
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user