feat(A1+B1): 引入站台多主机识别、音频线缆分组互斥令牌、本地Modem仲裁、线缆分组管理WebUI
服务端 A1 改动(CloudServer/server.js): - stations 改为 nodes[] 多连接存储,同站台名的多台 CC 主机归入同一组(纯 stationName 精确分组) - 新增 getStationMaster():hasStationPeripheral + lastSeen 加权选 Master,音频只下发给 Master - 新增 audioCableGroups 数据结构(持久化到 server-data.json) - 新增 acquireCableToken/releaseCableToken:同线缆组全局互斥,高优先级发送 audio_preempt 抢占 - 新增 7 路由 REST API:/api/audio-cable-groups CRUD + 批量/单个站台成员管理 + PUT /stations/:name/cable-group 直接改组 - sendAudioControl 给同站组所有节点全量下发(stop/状态同步) - init_state / state_update 携带 audioCableGroups 给前端 - WS close 自动 removeStationWs 清理节点 + 释放令牌 客户端 B1 改动(station_broadcast.lua v1.0.99): - Peripherals 新增 wiredModems 扫描(排除无线),自动 rednet.open - 新增 LocalBus 命名空间: - station_hello 每 10s 广播 + 30s 超时清理邻居 - 同站台名自动组 stationGroup,按 hasStationPeripheral + nodeId 升序选 Master - 新增 Utils.isLocalStationMaster(),所有列车上报(arrival/approaching/alert/departure/schedule_sync)仅 Master 执行,彻底解决重复广播 - acquireLocalToken:audio_probe -> 80ms 收响应 -> 必要时 audio_preempt 抢占 -> token_grant - 令牌持有期间每 2s 广播 audio_playing 心跳,30s 超时自动判死回收 - Audio.playerLoop 在 playItem 前获取本地令牌,失败则跳过该音频 - Audio.stop() 自动释放本地令牌 - WS 新增下行处理:audio_preempt / cable_token_denied / station_group_state - station_identity.cfg 支持 audio_cable_group= 字段,saveStationIdentity 写回 - Network.register/heartbeat 携带 cableGroup 供服务端回填分组 Web 管理端(CloudServer/public/cablemanager.html,单文件无外部依赖): - 极简硬边风格,对齐 listmanager.html 配色和字体 - 分组网格卡片:色标/名称/描述/成员数/当前令牌持有者 + 编辑删除 - 新增/编辑 Modal:名称+描述+8色预设色板+自定义#RRGGBB - 分组详情双栏 Dual List:左栏全部站台(搜索+在线状态+在其他组标签)/ 右栏成员,中间批量添加移除按钮 - 顶部快捷操作:站台下拉+组下拉+确认,直接 PUT /stations/:name/cable-group - WebSocket 监听 state_update 实时刷新,Toast 提示反馈
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+479
-4
@@ -153,6 +153,38 @@ function Peripherals.init()
|
||||
end
|
||||
end
|
||||
Peripherals.blockReader = blockReader
|
||||
|
||||
Peripherals.wiredModems = {}
|
||||
pcall(function()
|
||||
local sides = { "back", "top", "bottom", "left", "right", "front" }
|
||||
for _, side in ipairs(sides) do
|
||||
if peripheral.isPresent(side) and peripheral.getType(side) == "modem" then
|
||||
local m = peripheral.wrap(side)
|
||||
if m and not m.isWireless() then
|
||||
table.insert(Peripherals.wiredModems, { side = side, modem = m })
|
||||
pcall(function() rednet.open(side) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
if peripheral.getNames then
|
||||
for _, name in ipairs(peripheral.getNames()) do
|
||||
local already = false
|
||||
for _, wm in ipairs(Peripherals.wiredModems) do
|
||||
if wm.side == name then already = true; break end
|
||||
end
|
||||
if not already and peripheral.getType(name) == "modem" then
|
||||
local m = peripheral.wrap(name)
|
||||
if m and type(m.isWireless) == "function" and not m.isWireless() then
|
||||
table.insert(Peripherals.wiredModems, { side = name, modem = m })
|
||||
pcall(function() rednet.open(name) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
if #Peripherals.wiredModems > 0 then
|
||||
Utils.log("INFO", "Wired modem(s) detected for LocalBus: x" .. tostring(#Peripherals.wiredModems))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -203,7 +235,17 @@ function Utils.loadStationIdentity()
|
||||
return data
|
||||
end
|
||||
|
||||
function Utils.saveStationIdentity(stationCode, stationPlatform, departureBeep)
|
||||
function Utils.saveStationIdentity(stationCode, stationPlatform, departureBeep, cableGroupOverride)
|
||||
local existing = Utils.loadStationIdentity() or {}
|
||||
local cableGroup = cableGroupOverride
|
||||
if cableGroup == nil then cableGroup = existing.audio_cable_group or "" end
|
||||
if type(stationCode) == "table" then
|
||||
local tbl = stationCode
|
||||
stationCode = tbl.station_code or tbl.stationCode
|
||||
stationPlatform = tbl.station_platform or tbl.stationPlatform
|
||||
departureBeep = tbl.departure_beep or tbl.departureBeep
|
||||
cableGroup = tbl.audio_cable_group or tbl.cableGroup or cableGroup
|
||||
end
|
||||
local handle = fs.open(CONFIG.STATION_IDENTITY_FILE, "w")
|
||||
if not handle then
|
||||
return false
|
||||
@@ -213,6 +255,7 @@ function Utils.saveStationIdentity(stationCode, stationPlatform, departureBeep)
|
||||
"station_code=" .. Utils.trim(stationCode or ""),
|
||||
"station_platform=" .. Utils.trim(stationPlatform or ""),
|
||||
"departure_beep=" .. Utils.trim(departureBeep or "fc1"),
|
||||
"audio_cable_group=" .. Utils.trim(cableGroup or ""),
|
||||
""
|
||||
}, "\n"))
|
||||
handle.close()
|
||||
@@ -555,6 +598,15 @@ function Utils.refreshStationIdentity()
|
||||
State.stationPlatform = identity.stationPlatform
|
||||
State.departureBeep = identity.departureBeep
|
||||
State.stationIdentityMode = identity.mode
|
||||
local manual = Utils.loadStationIdentity() or {}
|
||||
State.localCableGroup = Utils.trim(manual.audio_cable_group or "")
|
||||
end
|
||||
|
||||
function Utils.isLocalStationMaster()
|
||||
if not LocalBus or not LocalBus.stationGroup or not LocalBus.stationGroup.masterNodeId then
|
||||
return true
|
||||
end
|
||||
return LocalBus.stationGroup.masterNodeId == LocalBus.nodeId
|
||||
end
|
||||
|
||||
function Utils.getStationCode(name)
|
||||
@@ -1184,6 +1236,13 @@ function Audio.stop(mode, flushBelow)
|
||||
State.audioQueue = kept
|
||||
end
|
||||
|
||||
if LocalBus and LocalBus.tokenHolder then
|
||||
local cg = LocalBus.tokenHolder.cableGroup
|
||||
local sn = LocalBus.tokenHolder.stationName
|
||||
local aid = LocalBus.tokenHolder.audioId
|
||||
LocalBus.releaseLocalToken(cg, sn, aid)
|
||||
end
|
||||
|
||||
os.queueEvent("audio_interrupt", State.interruptSeq)
|
||||
end
|
||||
|
||||
@@ -2017,8 +2076,25 @@ function Audio.playerLoop()
|
||||
os.sleep(0.05)
|
||||
else
|
||||
local item = table.remove(State.audioQueue, 1)
|
||||
local cableGroup = tostring(State.localCableGroup or "")
|
||||
local audioId = tostring(item.audioId or ("local-" .. tostring(os.clock())))
|
||||
local priority = tonumber(item.priority) or 0
|
||||
|
||||
local tokenOk = true
|
||||
if LocalBus and LocalBus.enabled and cableGroup ~= "" then
|
||||
tokenOk = LocalBus.acquireLocalToken(cableGroup, State.stationName, audioId, priority, 1500)
|
||||
end
|
||||
|
||||
if not tokenOk then
|
||||
Utils.log("WARN", "Audio playback skipped: local cable token denied for audioId=" .. audioId)
|
||||
State.currentPlaying = nil
|
||||
State.currentPriority = -1
|
||||
if LocalBus and LocalBus.tokenHolder then
|
||||
LocalBus.releaseLocalToken(cableGroup, State.stationName, audioId)
|
||||
end
|
||||
else
|
||||
State.currentPlaying = item
|
||||
State.currentPriority = tonumber(item.priority) or 0
|
||||
State.currentPriority = priority
|
||||
|
||||
local ok, err = pcall(Audio.playItem, item)
|
||||
if not ok then
|
||||
@@ -2027,6 +2103,11 @@ function Audio.playerLoop()
|
||||
|
||||
State.currentPlaying = nil
|
||||
State.currentPriority = -1
|
||||
|
||||
if LocalBus and LocalBus.tokenHolder then
|
||||
LocalBus.releaseLocalToken(cableGroup, State.stationName, audioId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2249,6 +2330,381 @@ function AutoUpdate.checkOnBoot()
|
||||
return ok
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- 5.5 LocalBus (Wired Modem / RedNet local arbitration & station group)
|
||||
-- ============================================================================
|
||||
local LocalBus = {}
|
||||
LocalBus.nodeId = tostring(os.getComputerID() or math.floor(math.random() * 1e9))
|
||||
LocalBus.enabled = false
|
||||
LocalBus.neighbors = {}
|
||||
LocalBus.stationGroup = { members = {}, masterNodeId = nil, groupKey = nil }
|
||||
LocalBus.tokenHolder = nil
|
||||
LocalBus.tokenQueue = {}
|
||||
LocalBus.lastHelloSentAt = 0
|
||||
LocalBus.lastPlayingAt = 0
|
||||
LocalBus.pendingProbeResponses = {}
|
||||
|
||||
function LocalBus._hasWiredModem()
|
||||
return Peripherals.wiredModems and #Peripherals.wiredModems > 0
|
||||
end
|
||||
|
||||
function LocalBus._broadcast(msg)
|
||||
if not msg or type(msg) ~= "table" then return end
|
||||
msg.nodeId = LocalBus.nodeId
|
||||
msg.ts = msg.ts or os.epoch("utc")
|
||||
local raw = textutils.serialize(msg)
|
||||
if LocalBus._hasWiredModem() then
|
||||
pcall(function()
|
||||
if rednet then rednet.broadcast(raw, "station_bus") end
|
||||
end)
|
||||
for _, wm in ipairs(Peripherals.wiredModems) do
|
||||
pcall(function()
|
||||
if wm.modem and type(wm.modem.transmit) == "function" then
|
||||
wm.modem.transmit(7420, 7420, raw)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LocalBus.init()
|
||||
LocalBus.enabled = LocalBus._hasWiredModem()
|
||||
Utils.refreshStationIdentity()
|
||||
LocalBus.stationGroup = {
|
||||
members = { { nodeId = LocalBus.nodeId, self = true, stationName = State.stationName, hasStationPeripheral = State.hasStationPeripheral } },
|
||||
masterNodeId = LocalBus.nodeId,
|
||||
groupKey = nil
|
||||
}
|
||||
if LocalBus.enabled then
|
||||
Utils.log("INFO", "LocalBus enabled via wired modem(s): nodeId=" .. LocalBus.nodeId .. " cableGroup=" .. tostring(State.localCableGroup or "(none)"))
|
||||
end
|
||||
end
|
||||
|
||||
function LocalBus._sendHello()
|
||||
LocalBus._broadcast({
|
||||
type = "station_hello",
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = State.stationPlatform,
|
||||
cableGroup = tostring(State.localCableGroup or ""),
|
||||
hasStationPeripheral = not not State.hasStationPeripheral,
|
||||
isMaster = Utils.isLocalStationMaster()
|
||||
})
|
||||
end
|
||||
|
||||
function LocalBus._cleanupNeighbors(now)
|
||||
local cutoff = (now or os.epoch("utc")) - 30000
|
||||
local changed = false
|
||||
for nid, nb in pairs(LocalBus.neighbors) do
|
||||
if (nb.ts or 0) < cutoff then
|
||||
LocalBus.neighbors[nid] = nil
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then LocalBus._rebuildStationGroup() end
|
||||
end
|
||||
|
||||
function LocalBus._rebuildStationGroup()
|
||||
local myName = Utils.trim(State.stationName or "")
|
||||
local members = { {
|
||||
nodeId = LocalBus.nodeId,
|
||||
self = true,
|
||||
stationName = myName,
|
||||
hasStationPeripheral = not not State.hasStationPeripheral,
|
||||
cableGroup = tostring(State.localCableGroup or ""),
|
||||
lastSeen = os.epoch("utc")
|
||||
} }
|
||||
for _, nb in pairs(LocalBus.neighbors) do
|
||||
if nb and type(nb) == "table" and Utils.trim(nb.stationName or "") == myName then
|
||||
table.insert(members, {
|
||||
nodeId = nb.nodeId,
|
||||
self = false,
|
||||
stationName = nb.stationName,
|
||||
hasStationPeripheral = not not nb.hasStationPeripheral,
|
||||
cableGroup = nb.cableGroup or "",
|
||||
lastSeen = nb.ts or 0
|
||||
})
|
||||
end
|
||||
end
|
||||
table.sort(members, function(a, b)
|
||||
if a.hasStationPeripheral ~= b.hasStationPeripheral then
|
||||
return a.hasStationPeripheral and true or false
|
||||
end
|
||||
return tostring(a.nodeId) < tostring(b.nodeId)
|
||||
end)
|
||||
local master = members[1]
|
||||
LocalBus.stationGroup = {
|
||||
members = members,
|
||||
masterNodeId = master and master.nodeId or LocalBus.nodeId,
|
||||
groupKey = myName,
|
||||
memberCount = #members
|
||||
}
|
||||
end
|
||||
|
||||
function LocalBus._sameStation(msg)
|
||||
if not msg or not msg.stationName then return false end
|
||||
local a = Utils.trim(State.stationName or "")
|
||||
local b = Utils.trim(msg.stationName or "")
|
||||
return a ~= "" and a == b
|
||||
end
|
||||
|
||||
function LocalBus._sameGroup(msg)
|
||||
if not msg then return false end
|
||||
local a = Utils.trim(State.localCableGroup or "")
|
||||
local b = Utils.trim(msg.cableGroup or "")
|
||||
if a == "" or b == "" then return false end
|
||||
return a == b
|
||||
end
|
||||
|
||||
function LocalBus.releaseLocalToken(cableGroup, stationName, audioId)
|
||||
if not LocalBus.tokenHolder then return end
|
||||
local match = true
|
||||
if cableGroup and cableGroup ~= "" and LocalBus.tokenHolder.cableGroup ~= cableGroup then match = false end
|
||||
if audioId and LocalBus.tokenHolder.audioId and LocalBus.tokenHolder.audioId ~= audioId then match = false end
|
||||
if match then
|
||||
LocalBus._broadcast({
|
||||
type = "audio_release",
|
||||
stationName = State.stationName,
|
||||
cableGroup = LocalBus.tokenHolder.cableGroup,
|
||||
audioId = LocalBus.tokenHolder.audioId
|
||||
})
|
||||
LocalBus.tokenHolder = nil
|
||||
end
|
||||
end
|
||||
|
||||
function LocalBus._sendPlayingHeartbeat(force)
|
||||
if not LocalBus.tokenHolder then return end
|
||||
local now = os.epoch("utc")
|
||||
if force or (now - LocalBus.lastPlayingAt) >= 2000 then
|
||||
LocalBus._broadcast({
|
||||
type = "audio_playing",
|
||||
stationName = State.stationName,
|
||||
cableGroup = LocalBus.tokenHolder.cableGroup,
|
||||
audioId = LocalBus.tokenHolder.audioId,
|
||||
priority = LocalBus.tokenHolder.priority,
|
||||
startedAt = LocalBus.tokenHolder.startedAt
|
||||
})
|
||||
LocalBus.lastPlayingAt = now
|
||||
end
|
||||
end
|
||||
|
||||
function LocalBus._collectProbeResponses(cableGroup, audioId, waitMs)
|
||||
LocalBus.pendingProbeResponses = { playing = {}, requests = {} }
|
||||
local started = os.clock()
|
||||
local timeout = started + (waitMs / 1000)
|
||||
while os.clock() < timeout do
|
||||
local sid, msg, proto
|
||||
if rednet then
|
||||
pcall(function() sid, msg, proto = rednet.receive("station_bus", 0.02) end)
|
||||
end
|
||||
if type(msg) == "string" then
|
||||
local ok, tbl = pcall(textutils.unserialize, msg)
|
||||
if ok and type(tbl) == "table" then LocalBus._handleIncoming(tbl) end
|
||||
end
|
||||
os.sleep(0.01)
|
||||
end
|
||||
return LocalBus.pendingProbeResponses
|
||||
end
|
||||
|
||||
function LocalBus._handleIncoming(tbl)
|
||||
if not tbl or type(tbl) ~= "table" or not tbl.type then return end
|
||||
if tbl.nodeId == LocalBus.nodeId then return end
|
||||
if tbl.type == "station_hello" then
|
||||
LocalBus.neighbors[tostring(tbl.nodeId)] = tbl
|
||||
LocalBus._rebuildStationGroup()
|
||||
elseif tbl.type == "audio_probe" or tbl.type == "audio_playing" or tbl.type == "audio_token_grant" then
|
||||
if LocalBus._sameGroup(tbl) or LocalBus._sameStation(tbl) then
|
||||
if tbl.type == "audio_playing" then
|
||||
table.insert(LocalBus.pendingProbeResponses.playing, tbl)
|
||||
elseif tbl.type == "audio_probe" then
|
||||
table.insert(LocalBus.pendingProbeResponses.requests, tbl)
|
||||
if LocalBus.tokenHolder then
|
||||
LocalBus._broadcast({
|
||||
type = "audio_playing",
|
||||
stationName = State.stationName,
|
||||
cableGroup = LocalBus.tokenHolder.cableGroup,
|
||||
audioId = LocalBus.tokenHolder.audioId,
|
||||
priority = LocalBus.tokenHolder.priority,
|
||||
startedAt = LocalBus.tokenHolder.startedAt,
|
||||
targetProbeId = tostring(tbl.nodeId)
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif tbl.type == "audio_preempt" then
|
||||
if tostring(tbl.targetNodeId or "") == LocalBus.nodeId and LocalBus.tokenHolder then
|
||||
local newP = tonumber(tbl.newPriority) or 0
|
||||
Utils.log("WARN", "LocalBus audio preempted by node " .. tostring(tbl.nodeId) .. " reason=" .. tostring(tbl.reason or ""))
|
||||
Audio.stop("current", newP)
|
||||
LocalBus.releaseLocalToken(nil, nil, nil)
|
||||
end
|
||||
elseif tbl.type == "audio_release" then
|
||||
if LocalBus._sameGroup(tbl) then
|
||||
LocalBus.tokenQueue = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LocalBus.acquireLocalToken(cableGroup, stationName, audioId, priority, timeoutMs)
|
||||
if not LocalBus.enabled then return true end
|
||||
cableGroup = Utils.trim(cableGroup or "")
|
||||
priority = tonumber(priority) or 0
|
||||
if cableGroup == "" then return true end
|
||||
|
||||
local attempts = 0
|
||||
while attempts < 2 do
|
||||
attempts = attempts + 1
|
||||
LocalBus._broadcast({
|
||||
type = "audio_probe",
|
||||
stationName = stationName or State.stationName,
|
||||
cableGroup = cableGroup,
|
||||
audioId = audioId,
|
||||
priority = priority
|
||||
})
|
||||
local responses = LocalBus._collectProbeResponses(cableGroup, audioId, 80)
|
||||
local conflictHolder = nil
|
||||
for _, p in ipairs(responses.playing or {}) do
|
||||
local holderPrio = tonumber(p.priority) or 0
|
||||
if holderPrio >= priority then
|
||||
if not conflictHolder or holderPrio > (tonumber(conflictHolder.priority) or 0) then
|
||||
conflictHolder = p
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, r in ipairs(responses.requests or {}) do
|
||||
if LocalBus._sameStation(r) then
|
||||
local rPrio = tonumber(r.priority) or 0
|
||||
local myId = tostring(LocalBus.nodeId)
|
||||
local otherId = tostring(r.nodeId)
|
||||
if rPrio >= priority and otherId < myId then
|
||||
conflictHolder = r
|
||||
end
|
||||
end
|
||||
end
|
||||
if not conflictHolder then
|
||||
LocalBus.tokenHolder = {
|
||||
cableGroup = cableGroup,
|
||||
stationName = stationName or State.stationName,
|
||||
audioId = audioId,
|
||||
priority = priority,
|
||||
startedAt = os.epoch("utc"),
|
||||
attempts = attempts
|
||||
}
|
||||
LocalBus._broadcast({
|
||||
type = "audio_token_grant",
|
||||
requesterNodeId = LocalBus.nodeId,
|
||||
stationName = State.stationName,
|
||||
cableGroup = cableGroup,
|
||||
audioId = audioId,
|
||||
priority = priority
|
||||
})
|
||||
LocalBus._sendPlayingHeartbeat(true)
|
||||
return true
|
||||
else
|
||||
local holderPrio = tonumber(conflictHolder.priority) or 0
|
||||
if priority > holderPrio then
|
||||
LocalBus._broadcast({
|
||||
type = "audio_preempt",
|
||||
targetNodeId = tostring(conflictHolder.nodeId),
|
||||
stationName = State.stationName,
|
||||
cableGroup = cableGroup,
|
||||
audioId = audioId,
|
||||
reason = "higher_priority",
|
||||
newPriority = priority
|
||||
})
|
||||
os.sleep(0.15)
|
||||
else
|
||||
local waited = 0
|
||||
while waited < (timeoutMs or 1500) / 1000 do
|
||||
if not LocalBus.tokenHolder then break end
|
||||
os.sleep(0.05)
|
||||
waited = waited + 0.05
|
||||
end
|
||||
if not LocalBus.tokenHolder then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
if attempts >= 2 then
|
||||
Utils.log("WARN", "LocalBus token acquire failed: cableGroup=" .. cableGroup .. " audioId=" .. tostring(audioId))
|
||||
return false
|
||||
end
|
||||
LocalBus.tokenHolder = {
|
||||
cableGroup = cableGroup,
|
||||
stationName = stationName or State.stationName,
|
||||
audioId = audioId,
|
||||
priority = priority,
|
||||
startedAt = os.epoch("utc"),
|
||||
attempts = attempts
|
||||
}
|
||||
LocalBus._sendPlayingHeartbeat(true)
|
||||
return true
|
||||
end
|
||||
|
||||
function LocalBus._cleanupExpiredTokens(now)
|
||||
now = now or os.epoch("utc")
|
||||
if LocalBus.tokenHolder then
|
||||
local elapsed = now - (LocalBus.tokenHolder.startedAt or now)
|
||||
if elapsed > 30000 then
|
||||
Utils.log("WARN", "LocalBus token expired (30s), releasing: " .. tostring(LocalBus.tokenHolder.audioId))
|
||||
LocalBus.releaseLocalToken(nil, nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LocalBus._pullOnce(timeoutSec)
|
||||
timeoutSec = timeoutSec or 0.02
|
||||
if LocalBus.enabled and rednet then
|
||||
local sid, msg, proto
|
||||
pcall(function() sid, msg, proto = rednet.receive("station_bus", timeoutSec) end)
|
||||
if type(msg) == "string" then
|
||||
local ok, tbl = pcall(textutils.unserialize, msg)
|
||||
if ok and type(tbl) == "table" then
|
||||
LocalBus._handleIncoming(tbl)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, wm in ipairs(Peripherals.wiredModems or {}) do
|
||||
local got, side, ch, repCh, payload
|
||||
pcall(function()
|
||||
if wm.modem and type(os.pullEvent) == "function" then
|
||||
local ev = { os.pullEvent("modem_message", timeoutSec / 2) }
|
||||
if ev and ev[1] == "modem_message" then
|
||||
side, ch, repCh, payload = ev[2], ev[3], ev[4], ev[5]
|
||||
got = (side == wm.side and ch == 7420)
|
||||
end
|
||||
end
|
||||
end)
|
||||
if got and type(payload) == "string" then
|
||||
local ok2, tbl2 = pcall(textutils.unserialize, payload)
|
||||
if ok2 and type(tbl2) == "table" then
|
||||
LocalBus._handleIncoming(tbl2)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function LocalBus.loop()
|
||||
LocalBus.init()
|
||||
LocalBus._sendHello()
|
||||
LocalBus.lastHelloSentAt = os.epoch("utc")
|
||||
|
||||
while true do
|
||||
local now = os.epoch("utc")
|
||||
if (now - LocalBus.lastHelloSentAt) >= 10000 then
|
||||
LocalBus._sendHello()
|
||||
LocalBus.lastHelloSentAt = now
|
||||
end
|
||||
LocalBus._cleanupNeighbors(now)
|
||||
LocalBus._cleanupExpiredTokens(now)
|
||||
LocalBus._sendPlayingHeartbeat(false)
|
||||
pcall(function() LocalBus._pullOnce(0.05) end)
|
||||
os.sleep(0.05)
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. Network System
|
||||
-- ============================================================================
|
||||
@@ -2285,7 +2741,8 @@ function Network.register()
|
||||
stationName = State.stationName,
|
||||
stationCode = State.stationCode,
|
||||
stationPlatform = State.stationPlatform,
|
||||
stationVersion = APP_VERSION_TEXT
|
||||
stationVersion = APP_VERSION_TEXT,
|
||||
cableGroup = tostring(State.localCableGroup or "")
|
||||
})
|
||||
end
|
||||
|
||||
@@ -2449,6 +2906,22 @@ function Network.messageLoop()
|
||||
Utils.log("ERROR", "Station update failed: " .. tostring(updateErr))
|
||||
end
|
||||
end
|
||||
elseif data.type == "audio_preempt" then
|
||||
local flushBelow = tonumber(data.priority) or 0
|
||||
local mode = tostring(data.mode or "current")
|
||||
local reason = tostring(data.reason or "server_preempt")
|
||||
Utils.log("WARN", "Audio preempted by server: reason=" .. reason .. " priority>=" .. tostring(flushBelow))
|
||||
Audio.stop(mode, flushBelow)
|
||||
if LocalBus and LocalBus.tokenHolder then
|
||||
LocalBus.releaseLocalToken(nil, nil, nil)
|
||||
end
|
||||
elseif data.type == "cable_token_denied" then
|
||||
Utils.log("WARN", "Server cable token denied: group=" .. tostring(data.cableGroupId or "") .. " holder=" .. tostring(data.holderStationName or "") .. " reason=" .. tostring(data.reason or ""))
|
||||
elseif data.type == "station_group_state" then
|
||||
if data.masterNodeId and LocalBus and not LocalBus.enabled then
|
||||
LocalBus.stationGroup = LocalBus.stationGroup or {}
|
||||
LocalBus.stationGroup.masterNodeId = tostring(data.masterNodeId)
|
||||
end
|
||||
end
|
||||
else
|
||||
if State.audioRx then
|
||||
@@ -2530,7 +3003,9 @@ function Network.heartbeat()
|
||||
identityMode = State.stationIdentityMode,
|
||||
upcomingTrains = upcomingSummary,
|
||||
currentTrain = currentTrainInfo,
|
||||
upcomingCount = #(State.upcomingTrains or {})
|
||||
upcomingCount = #(State.upcomingTrains or {}),
|
||||
cableGroup = tostring(State.localCableGroup or ""),
|
||||
tickRate = State.tickRate
|
||||
})
|
||||
end
|
||||
os.sleep(10)
|
||||
|
||||
Reference in New Issue
Block a user