feat(server): 实施 A1 方案 - 多连接存储、站台分组 Master 选举、audioCableGroup 互斥令牌机制
- stations 存储改造:stations[name] = { name, code, platform, audioCableGroup, version, lastSeen, nodes: [] } 结构,每个 node 含 ws/hasStationPeripheral/stationPeripheralSide/hasBlockReader/hasCreateTarget/identityMode/nodeId/wsId/connectedAt
- 新增 audioCableGroups:{ id, name, color, description, stationNames: [] } 分组,及 audioCableTokens 互斥令牌 Map(cableGroupId -> { holderStationName, holderAudioId, holderPriority, acquiredAt, cancelToken })
- 新增核心函数:upsertStationNode(按 wsId 去重更新 node、聚合计时表级 schedule)、removeStationWs(WS 关闭时清理 node + 释放令牌 + 下发 group_state)、getStationMaster(hasStationPeripheral=1000加权 + lastSeen小数加权 两轮选举 OPEN ws)、getAllStationConnections、getGroupKeyForStation(纯 stationName 分组,不再 code@@platform)、acquireCableToken(无持者直给/同 holder 更新/高优先级 audio_preempt + 200ms 延迟抢占/低优先级拒绝+denied 消息)、releaseCableToken
- 发送策略改造:sendAudioControl 遍历组内所有节点 ws 全发(同名站同步 stop 等指令);sendAudioBufferToStation/sendLiveWavChunksToStation 只发送 master ws,发送前后 acquire/release token,durationSec 后 setTimeout 延迟释放,audio_start 消息新增 cableGroupId 字段
- enqueueStationAudioQueue 增加 cableGrp 兜底释放;loadData 兼容旧 stations 单值结构导入 nodes=[] 数组新结构 + audioCableGroups 双向回填;saveData 剥离 nodes 中 ws 对象后保存,新增 audioCableGroups 字段持久化
- WS 层改造:wss close 事件调用 removeStationWs;console_connect init_state 增加 audioCableGroups 数组;handleStationRegister/handleHeartbeat/handleScheduleSync 改为调用 upsertStationNode;station_register_ack 返回 nodeId/audioCableGroup
- broadcastUpdate state_update 增加 audioCableGroups 字段;新增 WS 下行 audio_preempt / cable_token_granted / cable_token_denied / station_group_state 四种消息类型
- getStationGroups 完全重写:按 stationName 精确分组,返回 members/nodes 数组(每个 node 带 nodeId/wsId/online/isMaster/connectedAt/lastSeen/hasStationPeripheral/hasBlockReader/hasCreateTarget/identityMode/tickRate/memberTags),增加 masterNodeId、audioCableGroup{id,name,color}、cableGroupId 字段;resolveStationGroup memberNames 返回 nodeId 列表
- REST API 新增 9 个路由:GET/POST/PUT/DELETE /api/audio-cable-groups,POST /api/audio-cable-groups/:id/stations(批量加站台,含 prev cableGroup 自动清理)、DELETE /api/audio-cable-groups/:id/stations/:stationName、PUT /api/stations/:name/cable-group(stations.audioCableGroup 与 group.stationNames 双向同步),stations DELETE 自动同步 cable group 清理
- 兼容性:旧 stations[name].ws 统一改为 getStationMaster(name).ws;TTS、HLS、temp-broadcast、音乐播放、version 管理等功能不受破坏;Lua 客户端版本号提升至 v1.0.99
This commit is contained in:
+91
-77
@@ -1,6 +1,6 @@
|
||||
|
||||
-- 1. Configuration & Constants
|
||||
local APP_VERSION_TEXT = "v1.0.98"
|
||||
local APP_VERSION_TEXT = "v1.0.99"
|
||||
|
||||
local CONFIG = {
|
||||
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
||||
@@ -2539,6 +2539,7 @@ end
|
||||
|
||||
function Network.sendScheduleSync()
|
||||
if not State.ws then return end
|
||||
if not Utils.isLocalStationMaster() then return end
|
||||
local upcomingTrainsPayload = {}
|
||||
if State.upcomingTrains and type(State.upcomingTrains) == "table" then
|
||||
for _, t in ipairs(State.upcomingTrains) do
|
||||
@@ -3555,19 +3556,21 @@ function Main.loop()
|
||||
if isPendingAlertReady(alertKey, now) and not State.announcedAlerts[alertKey] then
|
||||
State.announcedAlerts[alertKey] = now
|
||||
Utils.log("INFO", string.format("Train Alert: %s (ETA: %ds, Delay: %dm, Cancelled: %s, Reason: %s)", trainName, etaSeconds, displayDelayMinutes, tostring(cancelled), tostring(reason or "")))
|
||||
Network.send({
|
||||
type = "train_alert",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform,
|
||||
trainId = trainName,
|
||||
destination = destination,
|
||||
eta = tostring(etaSeconds) .. "s",
|
||||
etaSeconds = etaSeconds,
|
||||
delay = displayDelayMinutes,
|
||||
cancelled = cancelled,
|
||||
reason = reason
|
||||
})
|
||||
if Utils.isLocalStationMaster() then
|
||||
Network.send({
|
||||
type = "train_alert",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform,
|
||||
trainId = trainName,
|
||||
destination = destination,
|
||||
eta = tostring(etaSeconds) .. "s",
|
||||
etaSeconds = etaSeconds,
|
||||
delay = displayDelayMinutes,
|
||||
cancelled = cancelled,
|
||||
reason = reason
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3590,19 +3593,21 @@ function Main.loop()
|
||||
if currentPending and (now - (currentPending.since or now)) >= stableMs and not State.announcedApproaching[announceKey] then
|
||||
State.announcedApproaching[announceKey] = now
|
||||
Utils.log("INFO", string.format("Train Approaching: %s (ETA: %.1fs, Delay: %dm)", trainName, etaSeconds, delayMinutes))
|
||||
Network.send({
|
||||
type = "train_approaching",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform,
|
||||
trainId = trainName,
|
||||
destination = destination,
|
||||
eta = string.format("%.1fs", etaSeconds),
|
||||
etaSeconds = etaSeconds,
|
||||
delay = displayDelayMinutes,
|
||||
cancelled = cancelled,
|
||||
reason = reason
|
||||
})
|
||||
if Utils.isLocalStationMaster() then
|
||||
Network.send({
|
||||
type = "train_approaching",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform,
|
||||
trainId = trainName,
|
||||
destination = destination,
|
||||
eta = string.format("%.1fs", etaSeconds),
|
||||
etaSeconds = etaSeconds,
|
||||
delay = displayDelayMinutes,
|
||||
cancelled = cancelled,
|
||||
reason = reason
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3717,17 +3722,19 @@ function Main.loop()
|
||||
if isPendingAlertReady(alertKey, now) and not State.announcedAlerts[alertKey] then
|
||||
State.announcedAlerts[alertKey] = now
|
||||
Utils.log("INFO", string.format("Train Delay Alert: %s (ETA: %ds, Delay: %dm)", tName, etaSeconds, delay))
|
||||
Network.send({
|
||||
type = "train_alert",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform or State.stationPlatform or "1",
|
||||
trainId = tName,
|
||||
destination = dest,
|
||||
eta = etaStr,
|
||||
etaSeconds = etaSeconds,
|
||||
delay = delay
|
||||
})
|
||||
if Utils.isLocalStationMaster() then
|
||||
Network.send({
|
||||
type = "train_alert",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform or State.stationPlatform or "1",
|
||||
trainId = tName,
|
||||
destination = dest,
|
||||
eta = etaStr,
|
||||
etaSeconds = etaSeconds,
|
||||
delay = delay
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3750,17 +3757,19 @@ function Main.loop()
|
||||
if currentPending and (now - (currentPending.since or now)) >= stableMs and not State.announcedApproaching[announceKey] then
|
||||
State.announcedApproaching[announceKey] = now
|
||||
Utils.log("INFO", string.format("Train Approaching: %s (ETA: %ds, Delay: %dm)", tName, etaSeconds, delay))
|
||||
Network.send({
|
||||
type = "train_approaching",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform or State.stationPlatform or "1",
|
||||
trainId = tName,
|
||||
destination = dest,
|
||||
eta = etaStr,
|
||||
etaSeconds = etaSeconds,
|
||||
delay = delay
|
||||
})
|
||||
if Utils.isLocalStationMaster() then
|
||||
Network.send({
|
||||
type = "train_approaching",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = platform or State.stationPlatform or "1",
|
||||
trainId = tName,
|
||||
destination = dest,
|
||||
eta = etaStr,
|
||||
etaSeconds = etaSeconds,
|
||||
delay = delay
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3829,39 +3838,43 @@ function Main.loop()
|
||||
departureSent = false
|
||||
}
|
||||
|
||||
Network.send({
|
||||
type = "train_arrival",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = State.stationPlatform,
|
||||
trainId = id,
|
||||
destination = dest,
|
||||
destCode = destCode,
|
||||
nextStation = nextStation,
|
||||
nextCode = nextCode,
|
||||
lineCode = lineCode,
|
||||
departureTime = State.currentTrain.departureTime,
|
||||
departureBeep = State.departureBeep
|
||||
})
|
||||
if Utils.isLocalStationMaster() then
|
||||
Network.send({
|
||||
type = "train_arrival",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = State.stationPlatform,
|
||||
trainId = id,
|
||||
destination = dest,
|
||||
destCode = destCode,
|
||||
nextStation = nextStation,
|
||||
nextCode = nextCode,
|
||||
lineCode = lineCode,
|
||||
departureTime = State.currentTrain.departureTime,
|
||||
departureBeep = State.departureBeep
|
||||
})
|
||||
end
|
||||
else
|
||||
local now = os.epoch("utc") / 1000
|
||||
-- 增加 1.5 秒网络与预缓冲补偿,确保音频结束时列车刚好发车
|
||||
local triggerOffset = (State.currentTrain.departureDuration or 10) + 1.5
|
||||
if not State.currentTrain.departureSent and (State.currentTrain.departureTime - now) <= triggerOffset then
|
||||
State.currentTrain.departureSent = true
|
||||
Network.send({
|
||||
type = "train_departure",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = State.stationPlatform,
|
||||
trainId = State.currentTrain.id,
|
||||
destination = State.currentTrain.dest,
|
||||
destCode = State.currentTrain.destCode,
|
||||
nextStation = State.currentTrain.nextStation,
|
||||
nextCode = State.currentTrain.nextCode,
|
||||
lineCode = State.currentTrain.lineCode,
|
||||
departureBeep = State.departureBeep
|
||||
})
|
||||
if Utils.isLocalStationMaster() then
|
||||
Network.send({
|
||||
type = "train_departure",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = State.stationPlatform,
|
||||
trainId = State.currentTrain.id,
|
||||
destination = State.currentTrain.dest,
|
||||
destCode = State.currentTrain.destCode,
|
||||
nextStation = State.currentTrain.nextStation,
|
||||
nextCode = State.currentTrain.nextCode,
|
||||
lineCode = State.currentTrain.lineCode,
|
||||
departureBeep = State.departureBeep
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -3945,6 +3958,7 @@ parallel.waitForAny(
|
||||
Network.heartbeat,
|
||||
Network.syncFiles,
|
||||
Input.listener,
|
||||
Main.loop
|
||||
Main.loop,
|
||||
LocalBus.loop
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user