Files
2026-03-22 15:24:40 +08:00

373 lines
13 KiB
Kotlin
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自然写互动课堂电视端应用软件 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, "设备发现服务已释放")
}
}