feat(display): 列车进站后在屏幕右侧显示时刻表信息

- 新增 Utils.extractAllStationsFromSchedule() 从时刻表提取所有站点
- 重构 Display.render() 实现左右分栏布局
- 列车存在且屏幕宽度>=50时启用右侧时刻表面板
- 使用颜色区分:已过站(浅灰)/当前站(绿色高亮+>)/未到站(白)/终点站(黄色+[END])
- 版本号升级至 v1.0.64
This commit is contained in:
2026-08-16 11:45:37 +08:00
parent b74eb252e3
commit 71f6800a2a
+158 -60
View File
@@ -1,6 +1,6 @@
-- 1. Configuration & Constants -- 1. Configuration & Constants
local APP_VERSION_TEXT = "v1.0.63" local APP_VERSION_TEXT = "v1.0.64"
local CONFIG = { local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group", CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -1000,6 +1000,34 @@ function Utils.getNextStation(schedule, currentStationName)
return nil return nil
end end
function Utils.extractAllStationsFromSchedule(schedule)
if not schedule or not schedule.entries then return {} end
local maxIdx = 0
for k, _ in pairs(schedule.entries) do
if type(k) == "number" and k > maxIdx then maxIdx = k end
end
local stations = {}
for i = 1, maxIdx do
local entry = schedule.entries[i]
if entry and entry.instruction then
local inst = entry.instruction
if inst.id == "create:station" or inst.id == "create:destination" then
local text = (inst.data and inst.data.text) or inst.text
if type(text) == "string" and text ~= "" then
table.insert(stations, {
index = i,
name = text,
isDestination = inst.id == "create:destination"
})
end
end
end
end
return stations
end
-- 5. Audio System -- 5. Audio System
@@ -2459,15 +2487,22 @@ function Display.render()
local mon = Peripherals.primaryMonitor local mon = Peripherals.primaryMonitor
if not mon then return end if not mon then return end
local w, h = mon.getSize() local w, h = mon.getSize()
local contentBottom = 4
mon.clear() mon.clear()
mon.setCursorPos(1,1)
mon.setTextColor(COLORS.INFO) local hasSchedulePanel = State.currentTrain ~= nil
mon.write("STATION: " .. State.stationName .. " " .. APP_VERSION_TEXT) local leftW = w
mon.setCursorPos(1,2) local sepX = nil
mon.write("CODE: " .. tostring(State.stationCode) .. " P:" .. tostring(State.stationPlatform) .. " " .. tostring(State.stationIdentityMode)) local rightX = nil
mon.setCursorPos(1,3) local rightW = 0
mon.write("TIME: " .. os.date("%H:%M:%S"))
if hasSchedulePanel and w >= 50 then
rightW = math.min(math.floor(w * 0.4), 40)
leftW = w - rightW - 1
sepX = leftW + 1
rightX = sepX + 1
leftW = math.max(leftW, 20)
rightW = math.max(rightW, 15)
end
local function classifyAudioSource(source, priority) local function classifyAudioSource(source, priority)
local s = tostring(source or ""):lower() local s = tostring(source or ""):lower()
@@ -2477,18 +2512,48 @@ function Display.render()
return "Special Announcement" return "Special Announcement"
end end
local function trunc(str) local function truncL(str)
str = tostring(str or "") str = tostring(str or "")
if #str > w then return str:sub(1, w) end if #str > leftW then return str:sub(1, leftW) end
return str return str
end end
mon.setCursorPos(1,4) local function truncR(str)
str = tostring(str or "")
if #str > rightW then return str:sub(1, rightW) end
return str
end
local function drawLeft(y, text, color)
if y < 1 or y > h then return end
mon.setCursorPos(1, y)
mon.setTextColor(color or COLORS.INFO)
mon.write(truncL(text))
end
local function drawRight(y, text, color)
if not rightX or y < 1 or y > h then return end
mon.setCursorPos(rightX, y)
mon.setTextColor(color or COLORS.INFO)
mon.write(truncR(text))
end
if sepX then
for y = 1, h do
mon.setCursorPos(sepX, y)
mon.setTextColor(colors.gray)
mon.write("|")
end
end
drawLeft(1, "STATION: " .. State.stationName .. " " .. APP_VERSION_TEXT, COLORS.INFO)
drawLeft(2, "CODE: " .. tostring(State.stationCode) .. " P:" .. tostring(State.stationPlatform) .. " " .. tostring(State.stationIdentityMode), COLORS.INFO)
drawLeft(3, "TIME: " .. os.date("%H:%M:%S"), COLORS.INFO)
if State.currentPlaying then if State.currentPlaying then
local p = tonumber(State.currentPlaying.priority) or 0 local p = tonumber(State.currentPlaying.priority) or 0
local kind = classifyAudioSource(State.currentPlaying.source, p) local kind = classifyAudioSource(State.currentPlaying.source, p)
mon.setTextColor(COLORS.SUCCESS) drawLeft(4, "AUDIO: " .. kind .. " Playing (P=" .. tostring(p) .. ", Q=" .. tostring(#State.audioQueue) .. ")", COLORS.SUCCESS)
mon.write(trunc("AUDIO: " .. kind .. " Playing (P=" .. tostring(p) .. ", Q=" .. tostring(#State.audioQueue) .. ")"))
elseif State.audioRx then elseif State.audioRx then
local p = tonumber(State.audioRx.priority) or 0 local p = tonumber(State.audioRx.priority) or 0
local kind = classifyAudioSource(State.audioRx.source, p) local kind = classifyAudioSource(State.audioRx.source, p)
@@ -2496,65 +2561,105 @@ function Display.render()
local total = tonumber(State.audioRx.expectedBytes) or 0 local total = tonumber(State.audioRx.expectedBytes) or 0
local pct = "" local pct = ""
if total > 0 then pct = " " .. tostring(math.floor((rx / total) * 100)) .. "%" end if total > 0 then pct = " " .. tostring(math.floor((rx / total) * 100)) .. "%" end
mon.setTextColor(COLORS.DEBUG) drawLeft(4, "AUDIO: " .. kind .. " Receiving" .. pct .. " (Q=" .. tostring(#State.audioQueue) .. ")", COLORS.DEBUG)
mon.write(trunc("AUDIO: " .. kind .. " Receiving" .. pct .. " (Q=" .. tostring(#State.audioQueue) .. ")"))
else else
mon.setTextColor(COLORS.INFO) drawLeft(4, "AUDIO: FREE (Q=" .. tostring(#State.audioQueue) .. ")", COLORS.INFO)
mon.write(trunc("AUDIO: FREE (Q=" .. tostring(#State.audioQueue) .. ")"))
end end
-- Current Train local contentBottom = 4
if State.currentTrain then if State.currentTrain then
mon.setCursorPos(1,5) drawLeft(5, "CURRENT TRAIN:", COLORS.SUCCESS)
mon.setTextColor(COLORS.SUCCESS) drawLeft(6, State.currentTrain.id, COLORS.INFO)
mon.write("CURRENT TRAIN:") drawLeft(7, "Dest: " .. State.currentTrain.dest, COLORS.INFO)
mon.setTextColor(COLORS.INFO)
mon.setCursorPos(1,6)
mon.write(State.currentTrain.id)
mon.setCursorPos(1,7)
mon.write("Dest: " .. State.currentTrain.dest)
-- Countdown
if State.currentTrain.departureTime then if State.currentTrain.departureTime then
local remaining = math.max(0, State.currentTrain.departureTime - os.epoch("utc") / 1000) local remaining = math.max(0, State.currentTrain.departureTime - os.epoch("utc") / 1000)
mon.setCursorPos(1, 9)
if remaining > 0 then if remaining > 0 then
mon.setTextColor(COLORS.WARNING) drawLeft(9, string.format("Departing in: %02d:%02d", math.floor(remaining/60), math.floor(remaining%60)), COLORS.WARNING)
mon.write(string.format("Departing in: %02d:%02d", math.floor(remaining/60), math.floor(remaining%60)))
else else
mon.setTextColor(COLORS.ERROR) drawLeft(9, "Departing...", COLORS.ERROR)
mon.write("Departing...")
end end
contentBottom = 9 contentBottom = 9
else else
contentBottom = 7 contentBottom = 7
end end
if rightX then
drawRight(1, "SCHEDULE:", COLORS.SUCCESS)
drawRight(2, "Train: " .. State.currentTrain.id, COLORS.INFO)
local scheduleStations = Utils.extractAllStationsFromSchedule(State.currentTrain.schedule)
local currentStationBase = Utils.getBaseName(State.stationName)
local currentStationCode = Utils.getStationCode(State.stationName)
local passedCurrent = false
local stationLineY = 4
local maxStationLines = h - 3
for idx, st in ipairs(scheduleStations) do
if stationLineY > h then break end
local stBase = Utils.getBaseName(st.name)
local stCode = Utils.getStationCode(st.name)
local isCurrentStation = false
if currentStationBase and currentStationBase ~= "" and stBase == currentStationBase then
isCurrentStation = true
elseif currentStationCode and currentStationCode ~= "" and stCode == currentStationCode then
isCurrentStation = true
elseif Utils.isFuzzyMatch(State.stationName, st.name) then
isCurrentStation = true
end
local prefix = " "
local lineColor = COLORS.INFO
if isCurrentStation then
prefix = "> "
lineColor = COLORS.SUCCESS
passedCurrent = true
elseif passedCurrent then
prefix = " "
else
prefix = "* "
lineColor = colors.lightGray
end
local suffix = ""
if st.isDestination then
suffix = " [END]"
lineColor = COLORS.WARNING
end
local displayName = st.name
if isCurrentStation then
displayName = displayName .. " <<"
end
drawRight(stationLineY, prefix .. displayName .. suffix, lineColor)
stationLineY = stationLineY + 1
end
if #scheduleStations == 0 then
drawRight(4, "No station data", COLORS.WARNING)
end
end
else else
mon.setCursorPos(1,5)
mon.setTextColor(COLORS.WARNING)
if Peripherals.station then if Peripherals.station then
mon.write("NO TRAIN AT PLATFORM") drawLeft(5, "NO TRAIN AT PLATFORM", COLORS.WARNING)
else else
mon.write("SLAVE NODE MODE") drawLeft(5, "SLAVE NODE MODE", COLORS.WARNING)
end end
contentBottom = 5 contentBottom = 5
end end
-- Upcoming Trains
local logsStartY = h local logsStartY = h
local upcomingStartY = contentBottom + 1 local upcomingStartY = contentBottom + 1
if Peripherals.blockReader or Peripherals.createTarget then if Peripherals.blockReader or Peripherals.createTarget then
local hasUpcoming = State.upcomingTrains and #State.upcomingTrains > 0 local hasUpcoming = State.upcomingTrains and #State.upcomingTrains > 0
local maxUpcomingRows = math.max(0, h - upcomingStartY) drawLeft(upcomingStartY, "UPCOMING TRAINS:", COLORS.DEBUG)
mon.setTextColor(COLORS.DEBUG)
mon.setCursorPos(1, upcomingStartY)
mon.write("UPCOMING TRAINS:")
if hasUpcoming then if hasUpcoming then
mon.setTextColor(COLORS.INFO) local visibleCount = math.min(3, #State.upcomingTrains)
local visibleCount = math.min(3, #State.upcomingTrains, math.max(0, maxUpcomingRows - 1))
for i = 1, visibleCount do for i = 1, visibleCount do
local lineY = upcomingStartY + i
if lineY > h then break end
local t = State.upcomingTrains[i] local t = State.upcomingTrains[i]
mon.setCursorPos(1, upcomingStartY + i)
local suffix = "" local suffix = ""
if t.cancelled then if t.cancelled then
suffix = " [CANCEL]" suffix = " [CANCEL]"
@@ -2570,32 +2675,25 @@ function Display.render()
end end
local etaLabel = tostring(t.etaLabel or (tostring(t.eta or 0) .. "s")) local etaLabel = tostring(t.etaLabel or (tostring(t.eta or 0) .. "s"))
local lineText = string.format("%s%s->%s ETA:%s%s", pfx, t.trainName, t.dest, etaLabel, suffix) local lineText = string.format("%s%s->%s ETA:%s%s", pfx, t.trainName, t.dest, etaLabel, suffix)
if #lineText > w then lineText = lineText:sub(1, w) end drawLeft(lineY, lineText, COLORS.INFO)
mon.write(lineText)
end end
logsStartY = math.max(logsStartY, upcomingStartY + visibleCount + 1) logsStartY = math.max(logsStartY, upcomingStartY + visibleCount + 1)
else else
mon.setTextColor(COLORS.WARNING)
local msgY = upcomingStartY + 1 local msgY = upcomingStartY + 1
if msgY <= h then if msgY <= h then
mon.setCursorPos(1, msgY) drawLeft(msgY, "NONE / WAITING DATA", COLORS.WARNING)
mon.write("NONE / WAITING DATA")
logsStartY = math.max(logsStartY, msgY + 1) logsStartY = math.max(logsStartY, msgY + 1)
else
logsStartY = math.max(logsStartY, msgY)
end end
end end
end end
-- Logs
local maxLogLines = math.min(3, math.max(0, h - logsStartY + 1)) local maxLogLines = math.min(3, math.max(0, h - logsStartY + 1))
for i = 1, maxLogLines do for i = 1, maxLogLines do
local logIdx = i + State.logScrollOffset local logIdx = i + State.logScrollOffset
local entry = State.logs[logIdx] local entry = State.logs[logIdx]
if entry then if entry then
mon.setCursorPos(1, logsStartY + i - 1) local lineY = logsStartY + i - 1
mon.setTextColor(COLORS[entry.level] or colors.white) drawLeft(lineY, entry.text, COLORS[entry.level] or colors.white)
mon.write(entry.text:sub(1, w))
end end
end end
end end