/** * 自然写互动课堂电视端应用软件 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() /** 设备发现监听器 */ private val listeners = CopyOnWriteArrayList() /** 主线程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 { 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, "设备发现服务已释放") } }