420 lines
13 KiB
Kotlin
420 lines
13 KiB
Kotlin
/**
|
|
* 自然写互动课堂智慧黑板端应用软件 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<String, GatewayInfo>()
|
|
|
|
/* ==================== 消息监听 ==================== */
|
|
|
|
/** 消息监听器 */
|
|
private val messageListeners = CopyOnWriteArrayList<GatewayMessageListener>()
|
|
|
|
/* ==================== 线程 ==================== */
|
|
|
|
/** 调度器 */
|
|
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<String, Any> = 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<GatewayInfo> = discoveredGateways.values.toList()
|
|
|
|
/**
|
|
* 停止并释放资源
|
|
*/
|
|
fun shutdown() {
|
|
isRunning.set(false)
|
|
scheduler.shutdown()
|
|
messageExecutor.shutdown()
|
|
changeState(GatewayConnectionState.DISCONNECTED)
|
|
Log.i(TAG, "网关连接器已关闭")
|
|
}
|
|
}
|