feat(station): support manual identity for slave nodes

Allow station_broadcast to run without a train station peripheral on the back, add a local station_identity.cfg override for station name/code/platform, and display the active station code, platform, and identity mode so slave nodes can be grouped to a platform manually.
This commit is contained in:
2026-08-01 07:51:59 +08:00
parent 304d413482
commit 6315e1eaa1
+149 -14
View File
@@ -2,7 +2,7 @@
-- ============================================================================
-- 1. Configuration & Constants
-- ============================================================================
local APP_VERSION_TEXT = "v1.0.6"
local APP_VERSION_TEXT = "v1.0.7"
local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -12,6 +12,7 @@ local CONFIG = {
AUDIO_STATION = "Audio/Station/",
DISK_AUDIO_BASE = "disk/Audio/",
DISK_AUDIO_STATION = "disk/Audio/Station/",
STATION_IDENTITY_FILE = "station_identity.cfg",
AUDIO_VOLUME = 3,
DEBUG = true
}
@@ -35,6 +36,8 @@ local State = {
stationName = "Unknown",
stationCode = "UNK",
stationPlatform = "1",
stationIdentityMode = "auto",
hasStationPeripheral = false,
registered = false,
stationList = {},
currentTrain = nil,
@@ -54,8 +57,14 @@ local Peripherals = {}
function Peripherals.init()
-- Station Peripheral
Peripherals.station = peripheral.wrap("back", "train_station")
or error("Train station peripheral required on 'back'")
local back = peripheral.wrap("back")
if back and peripheral.getType("back") == "train_station" then
Peripherals.station = back
State.hasStationPeripheral = true
else
Peripherals.station = nil
State.hasStationPeripheral = false
end
-- Monitors
local monitors = { peripheral.find("monitor") }
if #monitors == 0 then error("At least one monitor is required") end
@@ -84,6 +93,112 @@ function Utils.log(level, message)
end
end
function Utils.trim(value)
return tostring(value or ""):match("^%s*(.-)%s*$")
end
function Utils.writeStationIdentityTemplate()
if fs.exists(CONFIG.STATION_IDENTITY_FILE) then return end
local handle = fs.open(CONFIG.STATION_IDENTITY_FILE, "w")
if not handle then return end
handle.write(table.concat({
"# Manual station identity override for slave nodes",
"# Leave a value empty to keep auto detection when available",
"station_name=",
"station_code=",
"station_platform=",
""
}, "\n"))
handle.close()
end
function Utils.loadStationIdentity()
Utils.writeStationIdentityTemplate()
if not fs.exists(CONFIG.STATION_IDENTITY_FILE) then
return {}
end
local handle = fs.open(CONFIG.STATION_IDENTITY_FILE, "r")
if not handle then
return {}
end
local content = handle.readAll() or ""
handle.close()
local data = {}
for line in content:gmatch("[^\r\n]+") do
local clean = Utils.trim(line)
if clean ~= "" and not clean:match("^#") then
local key, value = clean:match("^([%w_]+)%s*=%s*(.-)%s*$")
if key then
data[key:lower()] = Utils.trim(value)
end
end
end
return data
end
function Utils.resolveStationIdentity()
local manual = Utils.loadStationIdentity()
local physicalName = nil
if Peripherals.station and type(Peripherals.station.getStationName) == "function" then
physicalName = Utils.trim(Peripherals.station.getStationName() or "Unknown")
if physicalName == "" then physicalName = nil end
end
local stationCode = manual.station_code
local stationPlatform = manual.station_platform
local stationName = manual.station_name
if not stationName or stationName == "" then
stationName = physicalName
end
if (not stationCode or stationCode == "") and stationName and stationName ~= "" then
stationCode = Utils.getStationCode(stationName) or Utils.getBroadcastStationCode(stationName)
end
if not stationPlatform or stationPlatform == "" then
stationPlatform = Utils.getPlatformNumber(stationName or physicalName, "1")
end
if not stationName or stationName == "" then
if stationCode and stationCode ~= "" then
stationName = string.upper(stationCode) .. " Platform " .. tostring(stationPlatform or "1")
else
stationName = "Manual Station"
end
end
if not stationCode or stationCode == "" then
stationCode = Utils.getStationCode(stationName) or Utils.getBroadcastStationCode(stationName)
end
local hasManualIdentity =
(manual.station_name and manual.station_name ~= "") or
(manual.station_code and manual.station_code ~= "") or
(manual.station_platform and manual.station_platform ~= "")
local identityMode = "auto"
if hasManualIdentity then
identityMode = "manual"
elseif not Peripherals.station then
identityMode = "manual-fallback"
end
return {
stationName = stationName,
stationCode = stationCode or "UNK",
stationPlatform = tostring(stationPlatform or "1"),
mode = identityMode
}
end
function Utils.refreshStationIdentity()
local identity = Utils.resolveStationIdentity()
State.stationName = identity.stationName
State.stationCode = identity.stationCode
State.stationPlatform = identity.stationPlatform
State.stationIdentityMode = identity.mode
end
function Utils.getStationCode(name)
if not name then return nil end
name = tostring(name)
@@ -1304,10 +1419,7 @@ function Network.send(data)
end
function Network.register()
State.stationName = (Peripherals.station.getStationName() or "Unknown"):match("^%s*(.-)%s*$") -- Trim
-- Try to get a clean code from name, fallback to broadcast format
State.stationCode = Utils.getStationCode(State.stationName) or Utils.getBroadcastStationCode(State.stationName)
State.stationPlatform = Utils.getPlatformNumber(State.stationName, "1")
Utils.refreshStationIdentity()
Network.send({
type = "station_register",
@@ -1493,6 +1605,7 @@ end
function Network.heartbeat()
while true do
if State.ws then
Utils.refreshStationIdentity()
Network.send({
type = "heartbeat",
stationName = State.stationName,
@@ -1518,6 +1631,8 @@ function Display.render()
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 function classifyAudioSource(source, priority)
@@ -1534,7 +1649,7 @@ function Display.render()
return str
end
mon.setCursorPos(1,3)
mon.setCursorPos(1,4)
if State.currentPlaying then
local p = tonumber(State.currentPlaying.priority) or 0
local kind = classifyAudioSource(State.currentPlaying.source, p)
@@ -1556,19 +1671,19 @@ function Display.render()
-- Current Train
if State.currentTrain then
mon.setCursorPos(1,4)
mon.setCursorPos(1,5)
mon.setTextColor(COLORS.SUCCESS)
mon.write("CURRENT TRAIN:")
mon.setTextColor(COLORS.INFO)
mon.setCursorPos(1,5)
mon.write(State.currentTrain.id)
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
local remaining = math.max(0, State.currentTrain.departureTime - os.epoch("utc") / 1000)
mon.setCursorPos(1, 8)
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)))
@@ -1578,9 +1693,13 @@ function Display.render()
end
end
else
mon.setCursorPos(1,4)
mon.setCursorPos(1,5)
mon.setTextColor(COLORS.WARNING)
if Peripherals.station then
mon.write("NO TRAIN AT PLATFORM")
else
mon.write("SLAVE NODE MODE")
end
end
-- Logs
@@ -1604,7 +1723,7 @@ function Display.renderTerminal()
term.setBackgroundColor(colors.gray)
term.setTextColor(colors.white)
term.clearLine()
local title = " Station Broadcast Console (" .. State.stationName .. " / " .. APP_VERSION_TEXT .. ") "
local title = " Station Broadcast Console (" .. State.stationName .. " " .. tostring(State.stationCode) .. "/P" .. tostring(State.stationPlatform) .. " / " .. APP_VERSION_TEXT .. ") "
term.setCursorPos(math.floor((w - #title)/2) + 1, 1)
term.write(title)
term.setBackgroundColor(colors.black)
@@ -1711,6 +1830,14 @@ local Main = {}
function Main.loop()
while true do
if not Peripherals.station then
State.currentTrain = nil
Display.render()
Display.renderTerminal()
os.sleep(0.5)
goto continue
end
local hasTrain = Peripherals.station.isTrainPresent()
if hasTrain then
@@ -1741,6 +1868,7 @@ function Main.loop()
Display.render()
Display.renderTerminal()
os.sleep(0.5)
::continue::
end
end
@@ -1749,7 +1877,14 @@ end
-- ============================================================================
Peripherals.init()
Audio.loadStationList()
Utils.refreshStationIdentity()
Utils.log("INFO", "StationBroadcast version " .. APP_VERSION_TEXT)
if Peripherals.station then
Utils.log("INFO", "Train station peripheral detected on back")
else
Utils.log("WARN", "No train station peripheral on back; using manual slave mode")
Utils.log("INFO", "Edit " .. CONFIG.STATION_IDENTITY_FILE .. " to set station_name/station_code/station_platform")
end
parallel.waitForAny(
function()