feat(app-glasses): авто-обнаружение телефона — mDNS-клиент + скан /24 (RayNeo mDNS мёртв)
- MdnsClient: свой mDNS (UDP 5353, PTR _viewmate._tcp, парсер со сжатием 0xC0), join на всех Wi-Fi-интерфейсах, MulticastLock - PhoneScan: план Б — TCP-скан своей /24 :8080 батчами, детект по WS-handshake (101), таймаут 5с - HostConnection: цепочка mDNS → scan /24 (пропуск на эмуляторе) → fallback 10.0.2.2 - app-phone: NsdPublisher (_viewmate._tcp, порт 8080) + NEARBY_WIFI_DEVICES - Тесты 27/0 glasses (MdnsClientTest, PhoneScanTest), 67/0 phone - Железо (RayNeo X2 + HONOR): mDNS multicast режется WiFi-стеком очков («ни одного пакета за 15с»), скан /24 нашёл телефон за 1.2с, ws://192.168.76.121:8080, hello→welcome, статусы идут
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
|
||||
/**
|
||||
* Публикация WS-сервера телефона через mDNS (NSD): очки находят телефон
|
||||
* авто-обнаружением по `_viewmate._tcp`. Регистрация/разрегистрация
|
||||
* обёрнуты в try/catch — NSD не должен валить приложение (на эмуляторе
|
||||
* mDNS может отсутствовать, на Android 13+ registerService может требовать
|
||||
* NEARBY_WIFI_DEVICES и кидать SecurityException).
|
||||
*/
|
||||
class NsdPublisher(context: Context) {
|
||||
|
||||
private val nsdManager: NsdManager? =
|
||||
runCatching {
|
||||
context.applicationContext.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||
}.getOrNull()
|
||||
|
||||
private val registrationListener = object : NsdManager.RegistrationListener {
|
||||
override fun onServiceRegistered(info: NsdServiceInfo) {
|
||||
log("nsd", "NSD: сервис опубликован ${info.serviceType}:${info.port}")
|
||||
}
|
||||
|
||||
override fun onRegistrationFailed(info: NsdServiceInfo, errorCode: Int) {
|
||||
log("nsd", "NSD: публикация не удалась: код $errorCode")
|
||||
}
|
||||
|
||||
override fun onServiceUnregistered(info: NsdServiceInfo) {
|
||||
log("nsd", "NSD: сервис снят с публикации")
|
||||
}
|
||||
|
||||
override fun onUnregistrationFailed(info: NsdServiceInfo, errorCode: Int) {
|
||||
log("nsd", "NSD: снятие с публикации не удалось: код $errorCode")
|
||||
}
|
||||
}
|
||||
|
||||
private var registered = false
|
||||
|
||||
/** Опубликовать сервис `_viewmate._tcp:[port]` (не должен бросать). */
|
||||
fun publish(serviceName: String = "viewmate", serviceType: String = "_viewmate._tcp", port: Int = PhoneConfig.SERVER_PORT) {
|
||||
val nsdManager = nsdManager ?: run {
|
||||
log("nsd", "NSD: NsdManager недоступен — публикация пропущена")
|
||||
return
|
||||
}
|
||||
if (registered) return
|
||||
val serviceInfo = NsdServiceInfo().apply {
|
||||
this.serviceName = serviceName
|
||||
this.serviceType = serviceType
|
||||
this.port = port
|
||||
}
|
||||
try {
|
||||
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
|
||||
registered = true
|
||||
log("nsd", "NSD: публикация $serviceType:$port запущена ($serviceName)")
|
||||
} catch (e: Exception) {
|
||||
log("nsd", "NSD: публикация не удалась: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/** Снять сервис с публикации (при остановке сервера / завершении приложения). */
|
||||
fun unpublish() {
|
||||
val nsdManager = nsdManager ?: return
|
||||
if (!registered) return
|
||||
try {
|
||||
nsdManager.unregisterService(registrationListener)
|
||||
registered = false
|
||||
} catch (e: Exception) {
|
||||
log("nsd", "NSD: снятие с публикации не удалось: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ class PhoneApp : Application() {
|
||||
/** WS-сервер для очков. */
|
||||
val server: GlassesServer = GlassesServer(port = PhoneConfig.SERVER_PORT, state = state)
|
||||
|
||||
/** Публикация сервера в локальной сети (mDNS `_viewmate._tcp`) для авто-обнаружения очками. */
|
||||
val nsd: NsdPublisher by lazy { NsdPublisher(this) }
|
||||
|
||||
/** HTTP-клиенты бэкендов (Jellyfin-каталог, media-mirror). */
|
||||
val jellyfin: JellyfinClient = JellyfinClient(PhoneConfig.JELLYFIN_URL, PhoneConfig.JELLYFIN_API_KEY)
|
||||
val mirror: MirrorClient = MirrorClient(PhoneConfig.MIRROR_URL, PhoneConfig.MIRROR_API_KEY)
|
||||
@@ -53,6 +56,7 @@ class PhoneApp : Application() {
|
||||
|
||||
server.start()
|
||||
log("server", "WS-сервер запущен на ${PhoneConfig.SERVER_PORT}")
|
||||
nsd.publish()
|
||||
|
||||
// каждый PlaybackPosition от очков (5с) → синхронизация аудио-плеера
|
||||
server.hub.onPlaybackPosition = { positionMs, playing -> audioSync.sync(positionMs, playing) }
|
||||
@@ -100,6 +104,12 @@ class PhoneApp : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
nsd.unpublish()
|
||||
server.stop()
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: PhoneApp
|
||||
private set
|
||||
|
||||
Reference in New Issue
Block a user