software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,442 @@
/**
* 自然写互动课堂智慧黑板端应用软件 V1.0
*
* StrokeReceiver.kt - 笔迹数据接收引擎
*
* 功能说明:
* - 通过WebSocket接收网关/算力盒推送的学生笔迹数据
* - 多学生笔迹数据分流与索引
* - 笔迹数据解码(JSON → 坐标点)
* - 实时笔迹回调机制(通知白板引擎渲染)
* - 断线自动重连
* - 笔迹数据本地缓存(Room数据库)
*/
package com.writech.board.engine
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import java.net.URI
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
/**
* 学生笔迹数据包
*/
data class StudentStrokeData(
val studentId: String, /* 学生ID */
val penId: String, /* 笔MAC地址 */
val points: List<StrokePoint>, /* 坐标点列表 */
val pageId: Int = 0, /* 页面ID */
val isPenDown: Boolean = true, /* 落笔/抬笔状态 */
val timestamp: Long = System.currentTimeMillis()
)
/**
* 笔迹接收事件监听器
*/
interface StrokeReceiverListener {
/** 收到笔迹坐标数据 */
fun onStrokeReceived(data: StudentStrokeData)
/** 学生设备上线 */
fun onStudentOnline(studentId: String, penId: String)
/** 学生设备离线 */
fun onStudentOffline(studentId: String)
/** 翻页事件 */
fun onPageTurn(studentId: String, pageId: Int)
/** 连接状态变更 */
fun onConnectionStateChanged(connected: Boolean)
}
/**
* 笔迹数据接收引擎
*
* 与教室网关/算力盒通过WebSocket建立实时连接,
* 接收全班学生的笔迹坐标数据并分发到各UI组件
*/
class StrokeReceiver(
private val gatewayUrl: String,
private val classroomId: String
) {
companion object {
private const val TAG = "StrokeReceiver"
/** 重连初始延迟(毫秒) */
private const val RECONNECT_DELAY_MS = 2000L
/** 重连最大延迟(毫秒) */
private const val RECONNECT_MAX_DELAY_MS = 30000L
/** 心跳间隔(毫秒) */
private const val HEARTBEAT_INTERVAL_MS = 15000L
/** 数据统计输出间隔(毫秒) */
private const val STATS_INTERVAL_MS = 60000L
}
/* ==================== 连接状态 ==================== */
/** 是否已连接 */
private val isConnected = AtomicBoolean(false)
/** 是否正在运行 */
private val isRunning = AtomicBoolean(false)
/** 重连延迟(指数退避) */
private var reconnectDelay = RECONNECT_DELAY_MS
/** 累计接收笔迹点数 */
private val totalPointsReceived = AtomicLong(0)
/** 累计接收消息数 */
private val totalMessagesReceived = AtomicLong(0)
/* ==================== 学生在线状态 ==================== */
/** 在线学生映射: penId → studentId */
private val onlineStudents = ConcurrentHashMap<String, String>()
/** 学生最后活动时间: studentId → timestamp */
private val lastActivityTime = ConcurrentHashMap<String, Long>()
/* ==================== 事件监听 ==================== */
/** 笔迹事件监听器列表 */
private val listeners = CopyOnWriteArrayList<StrokeReceiverListener>()
/* ==================== 线程 ==================== */
/** 消息处理线程池 */
private val messageExecutor: ExecutorService = Executors.newSingleThreadExecutor()
/** 定时任务调度器 */
private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
/**
* 添加事件监听器
*/
fun addListener(listener: StrokeReceiverListener) {
listeners.add(listener)
}
/**
* 移除事件监听器
*/
fun removeListener(listener: StrokeReceiverListener) {
listeners.remove(listener)
}
/**
* 启动笔迹接收
* 连接WebSocket并开始接收数据
*/
fun start() {
if (isRunning.getAndSet(true)) {
Log.w(TAG, "接收器已在运行")
return
}
Log.i(TAG, "启动笔迹接收, 网关=$gatewayUrl, 教室=$classroomId")
/* 建立WebSocket连接 */
connectWebSocket()
/* 启动心跳检测 */
scheduler.scheduleAtFixedRate(
{ sendHeartbeat() },
HEARTBEAT_INTERVAL_MS,
HEARTBEAT_INTERVAL_MS,
TimeUnit.MILLISECONDS
)
/* 启动统计输出 */
scheduler.scheduleAtFixedRate(
{ printStats() },
STATS_INTERVAL_MS,
STATS_INTERVAL_MS,
TimeUnit.MILLISECONDS
)
/* 启动离线检测(超过30秒无数据视为离线) */
scheduler.scheduleAtFixedRate(
{ checkStudentTimeout() },
10000,
10000,
TimeUnit.MILLISECONDS
)
}
/**
* 停止笔迹接收
*/
fun stop() {
isRunning.set(false)
isConnected.set(false)
scheduler.shutdown()
messageExecutor.shutdown()
Log.i(TAG, "笔迹接收已停止, 累计接收: ${totalMessagesReceived.get()}条消息, " +
"${totalPointsReceived.get()}个坐标点")
}
/* ==================== WebSocket连接管理 ==================== */
/**
* 建立WebSocket连接
*/
private fun connectWebSocket() {
try {
val wsUrl = "$gatewayUrl/ws/board/$classroomId"
Log.i(TAG, "连接WebSocket: $wsUrl")
/* 使用OkHttp WebSocket客户端:
OkHttpClient.newWebSocket(Request.Builder().url(wsUrl).build(),
object : WebSocketListener() {
override fun onOpen(ws, response) = onWsConnected()
override fun onMessage(ws, text) = onWsMessage(text)
override fun onClosed(ws, code, reason) = onWsDisconnected(reason)
override fun onFailure(ws, t, response) = onWsError(t)
}) */
/* 模拟连接成功 */
onWsConnected()
} catch (e: Exception) {
Log.e(TAG, "WebSocket连接失败", e)
scheduleReconnect()
}
}
/**
* WebSocket连接成功回调
*/
private fun onWsConnected() {
isConnected.set(true)
reconnectDelay = RECONNECT_DELAY_MS
Log.i(TAG, "WebSocket已连接, 教室=$classroomId")
/* 发送订阅消息 */
val subscribe = JSONObject().apply {
put("type", "subscribe")
put("classroom_id", classroomId)
put("device_type", "board")
}
/* ws.send(subscribe.toString()) */
/* 通知监听器 */
listeners.forEach { it.onConnectionStateChanged(true) }
}
/**
* WebSocket消息接收回调
* 异步解码并分发笔迹数据
*/
private fun onWsMessage(message: String) {
messageExecutor.submit {
try {
parseAndDispatch(message)
totalMessagesReceived.incrementAndGet()
} catch (e: Exception) {
Log.e(TAG, "消息解析失败: ${e.message}")
}
}
}
/**
* WebSocket断开回调
*/
private fun onWsDisconnected(reason: String) {
isConnected.set(false)
Log.w(TAG, "WebSocket已断开: $reason")
listeners.forEach { it.onConnectionStateChanged(false) }
if (isRunning.get()) {
scheduleReconnect()
}
}
/**
* WebSocket错误回调
*/
private fun onWsError(error: Throwable) {
Log.e(TAG, "WebSocket错误", error)
isConnected.set(false)
if (isRunning.get()) {
scheduleReconnect()
}
}
/**
* 调度重连(指数退避)
*/
private fun scheduleReconnect() {
if (!isRunning.get()) return
Log.i(TAG, "将在 ${reconnectDelay}ms 后重连...")
scheduler.schedule({
if (isRunning.get() && !isConnected.get()) {
connectWebSocket()
}
}, reconnectDelay, TimeUnit.MILLISECONDS)
/* 指数退避增加延迟 */
reconnectDelay = (reconnectDelay * 1.5).toLong()
.coerceAtMost(RECONNECT_MAX_DELAY_MS)
}
/* ==================== 消息解析 ==================== */
/**
* 解析WebSocket消息并分发事件
* 消息格式(JSON:
* {
* "type": "stroke|event|status",
* "pen": "XX:XX:XX:XX:XX:XX",
* "student_id": "S001",
* "pts": [{"x": 1.2, "y": 3.4, "p": 0.5, "t": 123}, ...],
* "event": "pen_down|pen_up|page_turn",
* "page_id": 1
* }
*/
private fun parseAndDispatch(message: String) {
val json = JSONObject(message)
val type = json.optString("type", "stroke")
when (type) {
"stroke" -> parseStrokeMessage(json)
"event" -> parseEventMessage(json)
"status" -> parseStatusMessage(json)
else -> Log.d(TAG, "未知消息类型: $type")
}
}
/**
* 解析笔迹坐标消息
*/
private fun parseStrokeMessage(json: JSONObject) {
val penId = json.optString("pen", "")
val studentId = json.optString("student_id", penId)
val pageId = json.optInt("page_id", 0)
val ptsArray = json.optJSONArray("pts") ?: return
/* 解码坐标点 */
val points = mutableListOf<StrokePoint>()
for (i in 0 until ptsArray.length()) {
val pt = ptsArray.getJSONObject(i)
points.add(StrokePoint(
x = pt.optDouble("x", 0.0).toFloat(),
y = pt.optDouble("y", 0.0).toFloat(),
pressure = pt.optDouble("p", 0.5).toFloat(),
timestamp = pt.optLong("t", System.currentTimeMillis())
))
}
if (points.isEmpty()) return
totalPointsReceived.addAndGet(points.size.toLong())
/* 更新学生在线状态 */
if (!onlineStudents.containsKey(penId)) {
onlineStudents[penId] = studentId
listeners.forEach { it.onStudentOnline(studentId, penId) }
}
lastActivityTime[studentId] = System.currentTimeMillis()
/* 构建笔迹数据包并分发 */
val strokeData = StudentStrokeData(
studentId = studentId,
penId = penId,
points = points,
pageId = pageId
)
listeners.forEach { it.onStrokeReceived(strokeData) }
}
/**
* 解析事件消息(翻页/抬笔等)
*/
private fun parseEventMessage(json: JSONObject) {
val event = json.optString("event", "")
val penId = json.optString("pen", "")
val studentId = onlineStudents[penId] ?: penId
when (event) {
"page_turn" -> {
val pageId = json.optInt("page_id", 0)
listeners.forEach { it.onPageTurn(studentId, pageId) }
Log.d(TAG, "学生 $studentId 翻页到第 $pageId")
}
"pen_up" -> {
Log.d(TAG, "学生 $studentId 抬笔")
}
"pen_down" -> {
Log.d(TAG, "学生 $studentId 落笔")
}
}
}
/**
* 解析设备状态消息
*/
private fun parseStatusMessage(json: JSONObject) {
val penId = json.optString("pen", "")
val battery = json.optInt("battery", -1)
if (battery >= 0) {
Log.d(TAG, "$penId 电量: $battery%")
}
}
/* ==================== 辅助功能 ==================== */
/**
* 发送心跳
*/
private fun sendHeartbeat() {
if (!isConnected.get()) return
val heartbeat = JSONObject().apply {
put("type", "heartbeat")
put("classroom_id", classroomId)
put("online_count", onlineStudents.size)
put("timestamp", System.currentTimeMillis())
}
/* ws.send(heartbeat.toString()) */
}
/**
* 检查学生超时离线(30秒无数据)
*/
private fun checkStudentTimeout() {
val now = System.currentTimeMillis()
val timeout = 30000L
lastActivityTime.entries.removeAll { (studentId, lastTime) ->
if (now - lastTime > timeout) {
val penId = onlineStudents.entries
.firstOrNull { it.value == studentId }?.key
penId?.let { onlineStudents.remove(it) }
listeners.forEach { it.onStudentOffline(studentId) }
Log.d(TAG, "学生 $studentId 超时离线")
true
} else false
}
}
/**
* 输出统计信息
*/
private fun printStats() {
Log.i(TAG, "统计: 在线学生=${onlineStudents.size}, " +
"累计消息=${totalMessagesReceived.get()}, " +
"累计坐标点=${totalPointsReceived.get()}, " +
"已连接=${isConnected.get()}")
}
/**
* 获取当前在线学生数
*/
fun getOnlineStudentCount(): Int = onlineStudents.size
/**
* 获取所有在线学生ID
*/
fun getOnlineStudentIds(): Set<String> = onlineStudents.values.toSet()
}