/** * 自然写互动课堂智慧黑板端应用软件 V1.0 * * GatewayConnector.kt - 网关WebSocket连接管理 * * 功能说明: * - mDNS自动发现教室网关设备 * - WebSocket连接管理(心跳/重连/消息路由) * - 笔迹数据流接收与分发 * - 课堂控制指令发送 * - 网关状态监控 */ package com.writech.board.network import android.content.Context import android.net.nsd.NsdManager import android.net.nsd.NsdServiceInfo import android.util.Log import org.json.JSONObject import java.util.concurrent.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger /** * 网关设备信息 */ data class GatewayInfo( val gatewayId: String, /* 网关唯一ID */ val host: String, /* IP地址 */ val port: Int, /* WebSocket端口 */ val onlinePenCount: Int = 0, /* 在线笔数量 */ val firmwareVersion: String = "", /* 固件版本 */ val signalStrength: Int = 0, /* WiFi信号强度 */ val lastHeartbeat: Long = System.currentTimeMillis() ) /** * 网关连接状态 */ enum class GatewayConnectionState { DISCONNECTED, /* 未连接 */ DISCOVERING, /* 正在发现 */ CONNECTING, /* 连接中 */ CONNECTED, /* 已连接 */ RECONNECTING /* 重连中 */ } /** * 网关消息类型 */ object GatewayMessageType { const val STROKE = "stroke" /* 笔迹数据 */ const val EVENT = "event" /* 设备事件 */ const val STATUS = "status" /* 网关状态 */ const val COMMAND_ACK = "cmd_ack" /* 命令应答 */ const val HEARTBEAT = "heartbeat" /* 心跳 */ } /** * 网关消息回调接口 */ interface GatewayMessageListener { fun onGatewayMessage(type: String, payload: JSONObject) fun onGatewayStateChanged(state: GatewayConnectionState, info: GatewayInfo?) } /** * 网关连接管理器 * * 负责: * 1. 通过mDNS自动发现同一教室网关 * 2. 建立WebSocket长连接 * 3. 双向消息收发 * 4. 自动重连机制 */ class GatewayConnector(private val context: Context) { companion object { private const val TAG = "GatewayConnector" /** mDNS服务类型 */ private const val MDNS_SERVICE_TYPE = "_writech-gw._tcp." /** 心跳间隔 */ private const val HEARTBEAT_INTERVAL_MS = 15000L /** 重连基础延迟 */ private const val RECONNECT_BASE_DELAY_MS = 3000L /** 最大重连延迟 */ private const val RECONNECT_MAX_DELAY_MS = 60000L /** 心跳超时时间 */ private const val HEARTBEAT_TIMEOUT_MS = 45000L } /* ==================== 连接状态 ==================== */ /** 当前连接状态 */ var connectionState = GatewayConnectionState.DISCONNECTED private set /** 当前连接的网关信息 */ var currentGateway: GatewayInfo? = null private set /** 是否正在运行 */ private val isRunning = AtomicBoolean(false) /** 重连尝试次数 */ private val reconnectAttempts = AtomicInteger(0) /** 最后收到心跳的时间 */ @Volatile private var lastHeartbeatReceived: Long = 0 /* ==================== 发现到的网关列表 ==================== */ /** 已发现的网关设备 */ private val discoveredGateways = ConcurrentHashMap() /* ==================== 消息监听 ==================== */ /** 消息监听器 */ private val messageListeners = CopyOnWriteArrayList() /* ==================== 线程 ==================== */ /** 调度器 */ private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(2) /** 消息处理 */ private val messageExecutor: ExecutorService = Executors.newSingleThreadExecutor() /** NSD管理器 */ private var nsdManager: NsdManager? = null /** * 注册消息监听器 */ fun addMessageListener(listener: GatewayMessageListener) { messageListeners.add(listener) } /** * 移除消息监听器 */ fun removeMessageListener(listener: GatewayMessageListener) { messageListeners.remove(listener) } /* ==================== mDNS发现 ==================== */ /** * 启动mDNS网关设备发现 */ fun startDiscovery() { isRunning.set(true) changeState(GatewayConnectionState.DISCOVERING) nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager val discoveryListener = object : NsdManager.DiscoveryListener { override fun onDiscoveryStarted(serviceType: String) { Log.i(TAG, "mDNS发现已启动: $serviceType") } override fun onServiceFound(serviceInfo: NsdServiceInfo) { Log.d(TAG, "发现服务: ${serviceInfo.serviceName}") if (serviceInfo.serviceType.contains("writech-gw")) { resolveService(serviceInfo) } } override fun onServiceLost(serviceInfo: NsdServiceInfo) { Log.d(TAG, "服务丢失: ${serviceInfo.serviceName}") discoveredGateways.remove(serviceInfo.serviceName) } override fun onDiscoveryStopped(serviceType: String) { Log.i(TAG, "mDNS发现已停止") } override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { Log.e(TAG, "mDNS发现启动失败: errorCode=$errorCode") } override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { Log.e(TAG, "mDNS发现停止失败: errorCode=$errorCode") } } try { nsdManager?.discoverServices(MDNS_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener) } catch (e: Exception) { Log.e(TAG, "启动mDNS发现失败", e) } } /** * 解析mDNS服务详情(获取IP和端口) */ private fun resolveService(serviceInfo: NsdServiceInfo) { nsdManager?.resolveService(serviceInfo, object : NsdManager.ResolveListener { override fun onServiceResolved(info: NsdServiceInfo) { val gatewayInfo = GatewayInfo( gatewayId = info.serviceName, host = info.host?.hostAddress ?: "", port = info.port ) discoveredGateways[info.serviceName] = gatewayInfo Log.i(TAG, "网关解析成功: ${gatewayInfo.gatewayId} " + "@ ${gatewayInfo.host}:${gatewayInfo.port}") /* 自动连接第一个发现的网关 */ if (connectionState == GatewayConnectionState.DISCOVERING) { connectToGateway(gatewayInfo) } } override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { Log.e(TAG, "网关解析失败: ${serviceInfo.serviceName}, errorCode=$errorCode") } }) } /* ==================== WebSocket连接 ==================== */ /** * 连接到指定网关 */ fun connectToGateway(gateway: GatewayInfo) { changeState(GatewayConnectionState.CONNECTING) val wsUrl = "ws://${gateway.host}:${gateway.port}/ws/board" Log.i(TAG, "连接网关: $wsUrl") try { /* OkHttpClient.newWebSocket( Request.Builder().url(wsUrl).build(), createWebSocketListener()) */ /* 模拟连接成功 */ onWebSocketConnected(gateway) } catch (e: Exception) { Log.e(TAG, "连接网关失败", e) scheduleReconnect() } } /** * WebSocket连接成功 */ private fun onWebSocketConnected(gateway: GatewayInfo) { currentGateway = gateway lastHeartbeatReceived = System.currentTimeMillis() reconnectAttempts.set(0) changeState(GatewayConnectionState.CONNECTED) /* 发送认证消息 */ sendAuthMessage() /* 启动心跳 */ startHeartbeat() Log.i(TAG, "已连接到网关: ${gateway.gatewayId}") } /** * 发送设备认证消息 */ private fun sendAuthMessage() { val auth = JSONObject().apply { put("type", "auth") put("device_type", "board") put("device_id", "BOARD-${System.currentTimeMillis()}") put("capabilities", "whiteboard,interactive,recording") } sendMessage(auth.toString()) } /** * 发送WebSocket消息 */ fun sendMessage(message: String) { if (connectionState != GatewayConnectionState.CONNECTED) { Log.w(TAG, "未连接状态无法发送消息") return } /* ws.send(message) */ Log.d(TAG, "发送消息: ${message.take(100)}...") } /** * 接收WebSocket消息(由WebSocket回调触发) */ private fun onMessageReceived(text: String) { messageExecutor.submit { try { val json = JSONObject(text) val type = json.optString("type", "") when (type) { GatewayMessageType.HEARTBEAT -> { lastHeartbeatReceived = System.currentTimeMillis() } GatewayMessageType.STATUS -> { updateGatewayStatus(json) } else -> { /* 分发给所有监听器 */ messageListeners.forEach { it.onGatewayMessage(type, json) } } } } catch (e: Exception) { Log.e(TAG, "消息处理失败: ${e.message}") } } } /** * 更新网关状态信息 */ private fun updateGatewayStatus(json: JSONObject) { currentGateway = currentGateway?.copy( onlinePenCount = json.optInt("online_pens", 0), firmwareVersion = json.optString("firmware", ""), signalStrength = json.optInt("wifi_rssi", 0), lastHeartbeat = System.currentTimeMillis() ) Log.d(TAG, "网关状态更新: 在线笔=${currentGateway?.onlinePenCount}") } /* ==================== 心跳与重连 ==================== */ /** * 启动心跳定时器 */ private fun startHeartbeat() { scheduler.scheduleAtFixedRate({ if (connectionState == GatewayConnectionState.CONNECTED) { /* 发送心跳 */ val hb = JSONObject().apply { put("type", "heartbeat") put("timestamp", System.currentTimeMillis()) } sendMessage(hb.toString()) /* 检查心跳超时 */ if (System.currentTimeMillis() - lastHeartbeatReceived > HEARTBEAT_TIMEOUT_MS) { Log.w(TAG, "网关心跳超时, 触发重连") onConnectionLost() } } }, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS) } /** * 连接丢失处理 */ private fun onConnectionLost() { changeState(GatewayConnectionState.RECONNECTING) scheduleReconnect() } /** * 调度重连(指数退避) */ private fun scheduleReconnect() { if (!isRunning.get()) return val attempt = reconnectAttempts.incrementAndGet() val delay = (RECONNECT_BASE_DELAY_MS * Math.pow(1.5, attempt.toDouble()).toLong()) .coerceAtMost(RECONNECT_MAX_DELAY_MS) Log.i(TAG, "将在 ${delay}ms 后重连 (第${attempt}次)") scheduler.schedule({ currentGateway?.let { connectToGateway(it) } }, delay, TimeUnit.MILLISECONDS) } /* ==================== 课堂控制指令 ==================== */ /** * 发送课堂控制指令 */ fun sendClassroomCommand(command: String, params: Map = emptyMap()) { val msg = JSONObject().apply { put("type", "command") put("command", command) params.forEach { (k, v) -> put(k, v) } put("timestamp", System.currentTimeMillis()) } sendMessage(msg.toString()) Log.i(TAG, "发送课堂指令: $command") } /* ==================== 状态管理 ==================== */ private fun changeState(newState: GatewayConnectionState) { connectionState = newState messageListeners.forEach { it.onGatewayStateChanged(newState, currentGateway) } } /** * 获取已发现的网关列表 */ fun getDiscoveredGateways(): List = discoveredGateways.values.toList() /** * 停止并释放资源 */ fun shutdown() { isRunning.set(false) scheduler.shutdown() messageExecutor.shutdown() changeState(GatewayConnectionState.DISCONNECTED) Log.i(TAG, "网关连接器已关闭") } }