diff --git a/station_broadcast.lua b/station_broadcast.lua index e91e4a1..466c66d 100644 --- a/station_broadcast.lua +++ b/station_broadcast.lua @@ -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) - mon.write("NO TRAIN AT PLATFORM") + 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()