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
+154 -56
View File
@@ -1,6 +1,6 @@
-- 1. Configuration & Constants
local APP_VERSION_TEXT = "v1.0.63"
local APP_VERSION_TEXT = "v1.0.64"
local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -1000,6 +1000,34 @@ function Utils.getNextStation(schedule, currentStationName)
return nil
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
@@ -2459,15 +2487,22 @@ function Display.render()
local mon = Peripherals.primaryMonitor
if not mon then return end
local w, h = mon.getSize()
local contentBottom = 4
mon.clear()
mon.setCursorPos(1,1)
mon.setTextColor(COLORS.INFO)
mon.write("STATION: " .. State.stationName .. " " .. APP_VERSION_TEXT)
mon.setCursorPos(1,2)
mon.write("CODE: " .. tostring(State.stationCode) .. " P:" .. tostring(State.stationPlatform) .. " " .. tostring(State.stationIdentityMode))
mon.setCursorPos(1,3)
mon.write("TIME: " .. os.date("%H:%M:%S"))
local hasSchedulePanel = State.currentTrain ~= nil
local leftW = w
local sepX = nil
local rightX = nil
local rightW = 0
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 s = tostring(source or ""):lower()
@@ -2477,18 +2512,48 @@ function Display.render()
return "Special Announcement"
end
local function trunc(str)
local function truncL(str)
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
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
local p = tonumber(State.currentPlaying.priority) or 0
local kind = classifyAudioSource(State.currentPlaying.source, p)
mon.setTextColor(COLORS.SUCCESS)
mon.write(trunc("AUDIO: " .. kind .. " Playing (P=" .. tostring(p) .. ", Q=" .. tostring(#State.audioQueue) .. ")"))
drawLeft(4, "AUDIO: " .. kind .. " Playing (P=" .. tostring(p) .. ", Q=" .. tostring(#State.audioQueue) .. ")", COLORS.SUCCESS)
elseif State.audioRx then
local p = tonumber(State.audioRx.priority) or 0
local kind = classifyAudioSource(State.audioRx.source, p)
@@ -2496,65 +2561,105 @@ function Display.render()
local total = tonumber(State.audioRx.expectedBytes) or 0
local pct = ""
if total > 0 then pct = " " .. tostring(math.floor((rx / total) * 100)) .. "%" end
mon.setTextColor(COLORS.DEBUG)
mon.write(trunc("AUDIO: " .. kind .. " Receiving" .. pct .. " (Q=" .. tostring(#State.audioQueue) .. ")"))
drawLeft(4, "AUDIO: " .. kind .. " Receiving" .. pct .. " (Q=" .. tostring(#State.audioQueue) .. ")", COLORS.DEBUG)
else
mon.setTextColor(COLORS.INFO)
mon.write(trunc("AUDIO: FREE (Q=" .. tostring(#State.audioQueue) .. ")"))
drawLeft(4, "AUDIO: FREE (Q=" .. tostring(#State.audioQueue) .. ")", COLORS.INFO)
end
-- Current Train
local contentBottom = 4
if State.currentTrain then
mon.setCursorPos(1,5)
mon.setTextColor(COLORS.SUCCESS)
mon.write("CURRENT TRAIN:")
mon.setTextColor(COLORS.INFO)
mon.setCursorPos(1,6)
mon.write(State.currentTrain.id)
mon.setCursorPos(1,7)
mon.write("Dest: " .. State.currentTrain.dest)
drawLeft(5, "CURRENT TRAIN:", COLORS.SUCCESS)
drawLeft(6, State.currentTrain.id, COLORS.INFO)
drawLeft(7, "Dest: " .. State.currentTrain.dest, COLORS.INFO)
-- Countdown
if State.currentTrain.departureTime then
local remaining = math.max(0, State.currentTrain.departureTime - os.epoch("utc") / 1000)
mon.setCursorPos(1, 9)
if remaining > 0 then
mon.setTextColor(COLORS.WARNING)
mon.write(string.format("Departing in: %02d:%02d", math.floor(remaining/60), math.floor(remaining%60)))
drawLeft(9, string.format("Departing in: %02d:%02d", math.floor(remaining/60), math.floor(remaining%60)), COLORS.WARNING)
else
mon.setTextColor(COLORS.ERROR)
mon.write("Departing...")
drawLeft(9, "Departing...", COLORS.ERROR)
end
contentBottom = 9
else
contentBottom = 7
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
mon.setCursorPos(1,5)
mon.setTextColor(COLORS.WARNING)
if Peripherals.station then
mon.write("NO TRAIN AT PLATFORM")
drawLeft(5, "NO TRAIN AT PLATFORM", COLORS.WARNING)
else
mon.write("SLAVE NODE MODE")
drawLeft(5, "SLAVE NODE MODE", COLORS.WARNING)
end
contentBottom = 5
end
-- Upcoming Trains
local logsStartY = h
local upcomingStartY = contentBottom + 1
if Peripherals.blockReader or Peripherals.createTarget then
local hasUpcoming = State.upcomingTrains and #State.upcomingTrains > 0
local maxUpcomingRows = math.max(0, h - upcomingStartY)
mon.setTextColor(COLORS.DEBUG)
mon.setCursorPos(1, upcomingStartY)
mon.write("UPCOMING TRAINS:")
drawLeft(upcomingStartY, "UPCOMING TRAINS:", COLORS.DEBUG)
if hasUpcoming then
mon.setTextColor(COLORS.INFO)
local visibleCount = math.min(3, #State.upcomingTrains, math.max(0, maxUpcomingRows - 1))
local visibleCount = math.min(3, #State.upcomingTrains)
for i = 1, visibleCount do
local lineY = upcomingStartY + i
if lineY > h then break end
local t = State.upcomingTrains[i]
mon.setCursorPos(1, upcomingStartY + i)
local suffix = ""
if t.cancelled then
suffix = " [CANCEL]"
@@ -2570,32 +2675,25 @@ function Display.render()
end
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)
if #lineText > w then lineText = lineText:sub(1, w) end
mon.write(lineText)
drawLeft(lineY, lineText, COLORS.INFO)
end
logsStartY = math.max(logsStartY, upcomingStartY + visibleCount + 1)
else
mon.setTextColor(COLORS.WARNING)
local msgY = upcomingStartY + 1
if msgY <= h then
mon.setCursorPos(1, msgY)
mon.write("NONE / WAITING DATA")
drawLeft(msgY, "NONE / WAITING DATA", COLORS.WARNING)
logsStartY = math.max(logsStartY, msgY + 1)
else
logsStartY = math.max(logsStartY, msgY)
end
end
end
-- Logs
local maxLogLines = math.min(3, math.max(0, h - logsStartY + 1))
for i = 1, maxLogLines do
local logIdx = i + State.logScrollOffset
local entry = State.logs[logIdx]
if entry then
mon.setCursorPos(1, logsStartY + i - 1)
mon.setTextColor(COLORS[entry.level] or colors.white)
mon.write(entry.text:sub(1, w))
local lineY = logsStartY + i - 1
drawLeft(lineY, entry.text, COLORS[entry.level] or colors.white)
end
end
end