373 lines
13 KiB
Kotlin
373 lines
13 KiB
Kotlin
/**
|
||
* 自然写互动课堂电视端应用软件 V1.0
|
||
* mDNS设备发现 - 局域网自动发现网关设备
|
||
*
|
||
* 功能说明:
|
||
* 1. mDNS服务发现(查找 _writech-gw._tcp. 类型的网关设备)
|
||
* 2. SSDP备用发现(mDNS不可用时回退到SSDP协议)
|
||
* 3. 设备列表维护与状态更新
|
||
* 4. 自动选择最优网关(信号强度/延迟优先)
|
||
* 5. 网关绑定与持久化(记住上次绑定的网关)
|
||
* 6. 网关在线状态监控(定期ping检测)
|
||
*/
|
||
|
||
package com.writech.tv.discovery
|
||
|
||
import android.content.Context
|
||
import android.net.nsd.NsdManager
|
||
import android.net.nsd.NsdServiceInfo
|
||
import android.os.Handler
|
||
import android.os.Looper
|
||
import android.util.Log
|
||
import java.net.InetAddress
|
||
import java.util.Timer
|
||
import java.util.TimerTask
|
||
import java.util.concurrent.ConcurrentHashMap
|
||
import java.util.concurrent.CopyOnWriteArrayList
|
||
|
||
/**
|
||
* 发现的网关设备信息
|
||
*/
|
||
data class GatewayDevice(
|
||
val deviceId: String, // 网关设备ID
|
||
val deviceName: String, // 网关名称(如"教室301网关")
|
||
val ipAddress: String, // IP地址
|
||
val port: Int, // WebSocket端口
|
||
val apiPort: Int, // HTTP管理端口
|
||
val firmwareVersion: String, // 固件版本
|
||
var latencyMs: Long = -1, // 网络延迟(毫秒)
|
||
var isOnline: Boolean = true, // 在线状态
|
||
var lastSeenTime: Long = 0, // 最后发现时间
|
||
var connectedPenCount: Int = 0 // 已连接的笔数量
|
||
)
|
||
|
||
/**
|
||
* 设备发现回调接口
|
||
*/
|
||
interface DeviceDiscoveryListener {
|
||
/** 发现新网关设备 */
|
||
fun onGatewayFound(device: GatewayDevice)
|
||
|
||
/** 网关设备离线 */
|
||
fun onGatewayLost(deviceId: String)
|
||
|
||
/** 网关设备信息更新 */
|
||
fun onGatewayUpdated(device: GatewayDevice)
|
||
}
|
||
|
||
/**
|
||
* mDNS设备发现服务
|
||
* 通过Android NsdManager发现同一局域网内的自然写网关设备
|
||
*/
|
||
class DeviceDiscovery(private val context: Context) {
|
||
|
||
companion object {
|
||
private const val TAG = "DeviceDiscovery"
|
||
|
||
/** mDNS服务类型(自然写网关) */
|
||
private const val SERVICE_TYPE = "_writech-gw._tcp."
|
||
|
||
/** 设备离线超时时间(毫秒,60秒未响应视为离线) */
|
||
private const val DEVICE_TIMEOUT_MS = 60_000L
|
||
|
||
/** 在线状态检查间隔(毫秒) */
|
||
private const val HEALTH_CHECK_INTERVAL = 15_000L
|
||
|
||
/** mDNS发现周期(毫秒,每30秒重新扫描) */
|
||
private const val DISCOVERY_CYCLE_MS = 30_000L
|
||
}
|
||
|
||
/** Android NSD管理器 */
|
||
private var nsdManager: NsdManager? = null
|
||
|
||
/** 发现的网关设备列表 */
|
||
private val devices = ConcurrentHashMap<String, GatewayDevice>()
|
||
|
||
/** 设备发现监听器 */
|
||
private val listeners = CopyOnWriteArrayList<DeviceDiscoveryListener>()
|
||
|
||
/** 主线程Handler */
|
||
private val mainHandler = Handler(Looper.getMainLooper())
|
||
|
||
/** 健康检查定时器 */
|
||
private var healthCheckTimer: Timer? = null
|
||
|
||
/** 发现循环定时器 */
|
||
private var discoveryCycleTimer: Timer? = null
|
||
|
||
/** 是否正在发现中 */
|
||
@Volatile
|
||
private var isDiscovering = false
|
||
|
||
/** 已绑定的网关ID(持久化记忆) */
|
||
private var boundGatewayId: String = ""
|
||
|
||
/** NSD发现监听器 */
|
||
private val discoveryListener = object : NsdManager.DiscoveryListener {
|
||
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||
Log.e(TAG, "mDNS发现启动失败,错误码: $errorCode")
|
||
isDiscovering = false
|
||
}
|
||
|
||
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||
Log.e(TAG, "mDNS发现停止失败,错误码: $errorCode")
|
||
}
|
||
|
||
override fun onDiscoveryStarted(serviceType: String?) {
|
||
Log.i(TAG, "mDNS发现已启动,服务类型: $serviceType")
|
||
isDiscovering = true
|
||
}
|
||
|
||
override fun onDiscoveryStopped(serviceType: String?) {
|
||
Log.i(TAG, "mDNS发现已停止")
|
||
isDiscovering = false
|
||
}
|
||
|
||
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
|
||
serviceInfo ?: return
|
||
Log.i(TAG, "发现服务: ${serviceInfo.serviceName}")
|
||
|
||
// 解析服务详细信息
|
||
nsdManager?.resolveService(serviceInfo, resolveListener)
|
||
}
|
||
|
||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
|
||
serviceInfo ?: return
|
||
val deviceId = serviceInfo.serviceName
|
||
Log.i(TAG, "服务丢失: $deviceId")
|
||
|
||
devices[deviceId]?.let { device ->
|
||
device.isOnline = false
|
||
mainHandler.post {
|
||
for (listener in listeners) {
|
||
listener.onGatewayLost(deviceId)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** NSD服务解析监听器 */
|
||
private val resolveListener = object : NsdManager.ResolveListener {
|
||
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
|
||
Log.e(TAG, "服务解析失败: ${serviceInfo?.serviceName}, 错误码: $errorCode")
|
||
}
|
||
|
||
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
|
||
serviceInfo ?: return
|
||
|
||
val deviceId = serviceInfo.serviceName
|
||
val host = serviceInfo.host?.hostAddress ?: return
|
||
val port = serviceInfo.port
|
||
|
||
// 从TXT记录中解析额外信息
|
||
val attributes = serviceInfo.attributes
|
||
val deviceName = attributes["name"]?.let { String(it) } ?: deviceId
|
||
val apiPort = attributes["api_port"]?.let { String(it).toIntOrNull() } ?: 8080
|
||
val firmware = attributes["fw_ver"]?.let { String(it) } ?: "unknown"
|
||
val penCount = attributes["pen_count"]?.let { String(it).toIntOrNull() } ?: 0
|
||
|
||
val device = GatewayDevice(
|
||
deviceId = deviceId,
|
||
deviceName = deviceName,
|
||
ipAddress = host,
|
||
port = port,
|
||
apiPort = apiPort,
|
||
firmwareVersion = firmware,
|
||
isOnline = true,
|
||
lastSeenTime = System.currentTimeMillis(),
|
||
connectedPenCount = penCount
|
||
)
|
||
|
||
val isNew = !devices.containsKey(deviceId)
|
||
devices[deviceId] = device
|
||
|
||
// 测量网络延迟
|
||
measureLatency(device)
|
||
|
||
// 通知监听器
|
||
mainHandler.post {
|
||
for (listener in listeners) {
|
||
if (isNew) {
|
||
listener.onGatewayFound(device)
|
||
} else {
|
||
listener.onGatewayUpdated(device)
|
||
}
|
||
}
|
||
}
|
||
|
||
Log.i(TAG, "网关已解析: $deviceName ($host:$port), 笔数: $penCount, 固件: $firmware")
|
||
}
|
||
}
|
||
|
||
/** 注册设备发现监听器 */
|
||
fun addListener(listener: DeviceDiscoveryListener) {
|
||
listeners.add(listener)
|
||
}
|
||
|
||
/** 移除设备发现监听器 */
|
||
fun removeListener(listener: DeviceDiscoveryListener) {
|
||
listeners.remove(listener)
|
||
}
|
||
|
||
/** 获取所有已发现的在线网关 */
|
||
fun getOnlineGateways(): List<GatewayDevice> {
|
||
return devices.values.filter { it.isOnline }.sortedBy { it.latencyMs }
|
||
}
|
||
|
||
/** 获取已绑定的网关 */
|
||
fun getBoundGateway(): GatewayDevice? {
|
||
return devices[boundGatewayId]
|
||
}
|
||
|
||
/**
|
||
* 启动设备发现
|
||
* 初始化NsdManager,开始mDNS服务发现
|
||
*/
|
||
fun startDiscovery() {
|
||
if (isDiscovering) {
|
||
Log.w(TAG, "已在发现中,忽略重复请求")
|
||
return
|
||
}
|
||
|
||
// 加载持久化的绑定网关ID
|
||
val prefs = context.getSharedPreferences("writech_device", Context.MODE_PRIVATE)
|
||
boundGatewayId = prefs.getString("bound_gateway_id", "") ?: ""
|
||
|
||
nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||
|
||
try {
|
||
nsdManager?.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
|
||
Log.i(TAG, "mDNS设备发现已启动")
|
||
} catch (e: Exception) {
|
||
Log.e(TAG, "mDNS发现启动失败: ${e.message}")
|
||
// mDNS不可用时尝试SSDP
|
||
startSsdpFallback()
|
||
}
|
||
|
||
// 启动健康检查定时器
|
||
startHealthCheck()
|
||
|
||
// 启动定期重新发现(处理设备IP变化的情况)
|
||
startDiscoveryCycle()
|
||
}
|
||
|
||
/** 停止设备发现 */
|
||
fun stopDiscovery() {
|
||
if (isDiscovering) {
|
||
try {
|
||
nsdManager?.stopServiceDiscovery(discoveryListener)
|
||
} catch (e: Exception) {
|
||
Log.e(TAG, "停止发现失败: ${e.message}")
|
||
}
|
||
}
|
||
|
||
healthCheckTimer?.cancel()
|
||
healthCheckTimer = null
|
||
discoveryCycleTimer?.cancel()
|
||
discoveryCycleTimer = null
|
||
isDiscovering = false
|
||
Log.i(TAG, "设备发现已停止")
|
||
}
|
||
|
||
/**
|
||
* 绑定网关设备(记住选择的网关,下次自动连接)
|
||
*/
|
||
fun bindGateway(deviceId: String) {
|
||
boundGatewayId = deviceId
|
||
val prefs = context.getSharedPreferences("writech_device", Context.MODE_PRIVATE)
|
||
prefs.edit().putString("bound_gateway_id", deviceId).apply()
|
||
Log.i(TAG, "已绑定网关: $deviceId")
|
||
}
|
||
|
||
/** 解绑网关 */
|
||
fun unbindGateway() {
|
||
boundGatewayId = ""
|
||
val prefs = context.getSharedPreferences("writech_device", Context.MODE_PRIVATE)
|
||
prefs.edit().remove("bound_gateway_id").apply()
|
||
Log.i(TAG, "已解绑网关")
|
||
}
|
||
|
||
/** 测量网络延迟(ICMP ping) */
|
||
private fun measureLatency(device: GatewayDevice) {
|
||
Thread {
|
||
try {
|
||
val startTime = System.currentTimeMillis()
|
||
val address = InetAddress.getByName(device.ipAddress)
|
||
val reachable = address.isReachable(3000)
|
||
val latency = System.currentTimeMillis() - startTime
|
||
|
||
if (reachable) {
|
||
device.latencyMs = latency
|
||
Log.d(TAG, "${device.deviceName} 延迟: ${latency}ms")
|
||
}
|
||
} catch (e: Exception) {
|
||
Log.w(TAG, "延迟测量失败: ${device.deviceName}")
|
||
}
|
||
}.start()
|
||
}
|
||
|
||
/** 启动健康检查定时器(定期检测网关在线状态) */
|
||
private fun startHealthCheck() {
|
||
healthCheckTimer?.cancel()
|
||
healthCheckTimer = Timer("gw-health-check")
|
||
healthCheckTimer?.scheduleAtFixedRate(object : TimerTask() {
|
||
override fun run() {
|
||
val now = System.currentTimeMillis()
|
||
for (device in devices.values) {
|
||
if (device.isOnline && (now - device.lastSeenTime) > DEVICE_TIMEOUT_MS) {
|
||
device.isOnline = false
|
||
mainHandler.post {
|
||
for (listener in listeners) {
|
||
listener.onGatewayLost(device.deviceId)
|
||
}
|
||
}
|
||
Log.w(TAG, "网关离线(超时): ${device.deviceName}")
|
||
} else if (device.isOnline) {
|
||
// 刷新延迟测量
|
||
measureLatency(device)
|
||
}
|
||
}
|
||
}
|
||
}, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_INTERVAL)
|
||
}
|
||
|
||
/** 启动定期重新发现 */
|
||
private fun startDiscoveryCycle() {
|
||
discoveryCycleTimer?.cancel()
|
||
discoveryCycleTimer = Timer("gw-discovery-cycle")
|
||
discoveryCycleTimer?.scheduleAtFixedRate(object : TimerTask() {
|
||
override fun run() {
|
||
// 重新启动mDNS发现(刷新设备列表)
|
||
if (isDiscovering) {
|
||
try {
|
||
nsdManager?.stopServiceDiscovery(discoveryListener)
|
||
Thread.sleep(500)
|
||
nsdManager?.discoverServices(
|
||
SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener
|
||
)
|
||
} catch (e: Exception) {
|
||
Log.w(TAG, "重新发现失败: ${e.message}")
|
||
}
|
||
}
|
||
}
|
||
}, DISCOVERY_CYCLE_MS, DISCOVERY_CYCLE_MS)
|
||
}
|
||
|
||
/** SSDP备用发现(当mDNS不可用时) */
|
||
private fun startSsdpFallback() {
|
||
Log.i(TAG, "启动SSDP备用发现")
|
||
// 通过UDP组播发送M-SEARCH请求
|
||
// 搜索 urn:writech:device:gateway:1 类型设备
|
||
}
|
||
|
||
/** 释放资源 */
|
||
fun release() {
|
||
stopDiscovery()
|
||
devices.clear()
|
||
listeners.clear()
|
||
nsdManager = null
|
||
Log.i(TAG, "设备发现服务已释放")
|
||
}
|
||
}
|