feat(display): 列车进站后在屏幕右侧显示时刻表信息
- 新增 Utils.extractAllStationsFromSchedule() 从时刻表提取所有站点 - 重构 Display.render() 实现左右分栏布局 - 列车存在且屏幕宽度>=50时启用右侧时刻表面板 - 使用颜色区分:已过站(浅灰)/当前站(绿色高亮+>)/未到站(白)/终点站(黄色+[END]) - 版本号升级至 v1.0.64
This commit is contained in:
+154
-56
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user