diff --git a/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java b/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java index 81876d5..63c6688 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java @@ -3,14 +3,16 @@ package nl.andrewl.railsignalapi.service; import lombok.RequiredArgsConstructor; import nl.andrewl.railsignalapi.dao.ComponentRepository; import nl.andrewl.railsignalapi.live.ComponentDownlinkService; +import nl.andrewl.railsignalapi.live.dto.ComponentDataMessage; import nl.andrewl.railsignalapi.live.dto.ErrorMessage; import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage; import nl.andrewl.railsignalapi.live.websocket.AppUpdateService; import nl.andrewl.railsignalapi.model.component.Switch; -import nl.andrewl.railsignalapi.model.component.SwitchConfiguration; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + /** * Service for managing switches. */ @@ -24,17 +26,20 @@ public class SwitchService { @Transactional public void onSwitchUpdate(SwitchUpdateMessage msg) { - switchRepository.findById(msg.cId).ifPresent(sw -> { - sw.getPossibleConfigurations().stream() - .filter(c -> c.getId().equals(msg.activeConfigId)) - .findFirst() - .ifPresentOrElse(config -> { + Optional optionalSwitch = switchRepository.findById(msg.cId); + if (optionalSwitch.isPresent()) { + Switch sw = optionalSwitch.get(); + for (var config : sw.getPossibleConfigurations()) { + if (config.getId().equals(msg.activeConfigId)) { sw.setActiveConfiguration(config); switchRepository.save(sw); - appUpdateService.sendComponentUpdate(sw.getRailSystem().getId(), sw.getId()); - }, () -> { - downlinkService.sendMessage(new ErrorMessage(sw.getId(), "Invalid active config id.")); - }); - }); + appUpdateService.sendUpdate(sw.getRailSystem().getId(), new ComponentDataMessage(sw)); + return; + } + } + downlinkService.sendMessage(new ErrorMessage(msg.cId, "Invalid config id.")); + } else { + downlinkService.sendMessage(new ErrorMessage(msg.cId, "Unknown switch.")); + } } } diff --git a/src/main/resources/driver/cc/driver.lua b/src/main/resources/driver/cc/driver.lua index 3abc8a1..0519441 100644 --- a/src/main/resources/driver/cc/driver.lua +++ b/src/main/resources/driver/cc/driver.lua @@ -1,235 +1,268 @@ -local VERSION = "1.0.2" +-- Rail Signal CC:Tweaked Driver +local VERSION = "1.0.0" --- Global component states -local components = {} +-- Global config. Will be loaded at start of script. +local config = {} +-- Global websocket reference +local ws = nil --- Global signal-indexed state maps. -local lastTrainOverheadStates = {} -local lastTrainOverheadDataObjs = {} -local trainScanTimers = {} - --- Determines if a train is currently traversing the given signal. -local function trainOverhead(signal) - return redstone.getInput(signal.redstoneInputSide) +local function loadConfig() + local configFile = io.open("rs_config.tbl", "r") + if not configFile then + return nil + end + local txt = configFile:read("*a") + local cfg = textutils.unserialize(txt) + configFile:close() + return cfg end --- Determines if a train was over the given signal the last time we checked. -local function wasTrainOverheadPreviously(signal) - return lastTrainOverheadStates[signal.id] == true +local function fetchJson(endpoint) + local response, msg, r = http.get({ + url = config.apiUrl .. endpoint, + method = "GET" + }) + if response then + return textutils.unserialiseJSON(response.readAll()) + else + print("Error: " .. r.getResponseCode()) + return nil + end end -local function getTrainData(signal) - local det = peripheral.wrap(signal.detector) - if det == nil then - print("Error: Signal " .. signal.id .. "'s detector could not be connected. Please fix the config and restart.") +-- Blocking function to obtain a websocket connection to Rail Signal. +local function connectToWebsocket() + local url = config.wsUrl .. "?token=" .. config.linkToken + print("Connecting to Rail Signal websocket...") + while true do + local ws, msg = http.websocket(url) + if ws == false then + print("Error connecting to the Rail Signal API websocket:\n\tError: " .. msg .. "\n\tTrying again in 3 seconds.") + os.sleep(3) else - return det.consist() + print("Successfully connected to Rail Signal API websocket.") + return ws end + end end --- Returns the from, to branches for a signal when a train travels in the given direction. -local function getBranches(dir, signal) - local branches = signal.branches - if string.upper(branches[1].direction) == string.upper(dir) then - return branches[2].id, branches[1].id - elseif string.upper(branches[2].direction) == string.upper(dir) then - return branches[1].id, branches[2].id +local function getComponentById(id) + for _, c in pairs(config.components) do + if c.id == id then + return c end + end + return nil end --- Sends an update to the RailSignal API. -local function sendSignalUpdate(ws, signal, data, msgType) - local from, to = getBranches(string.upper(data.direction), signal) - local msg = textutils.serializeJSON({ - signalId = signal.id, - fromBranchId = from, - toBranchId = to, - type = msgType - }) - print("-> S: " .. signal.id .. ", from: " .. from .. ", to: " .. to .. ", T: " .. msgType) - ws.send(msg) +local function displaySignal(signal) + local color = colors.white + if signal.segment.occupied ~= nil then + if signal.segment.occupied then + color = colors.red + else + color = colors.lime + end + end + signal.segment.monitor.setBackgroundColor(color) + signal.segment.monitor.clear() end -local function updateBranchStatusIndicator(branch, status, config) - if branch.monitor ~= nil then - local mon = peripheral.wrap(branch.monitor) - if mon ~= nil then - local c = config.statusColors[status] - if c == nil then c = config.statusColors.ERROR end - mon.setBackgroundColor(c) - mon.clear() - else - print("Error! Could not connect to monitor " .. branch.monitor .. " for branch " .. branch.id .. ". Check and fix config.") +local function setSwitchActiveConfig(switch, configId) + for _, cfg in pairs(switch.possibleConfigurations) do + if cfg.id == configId then + print("Setting active config id: " .. configId) + rs.setOutput(switch.rsSide, cfg.rsOutput) + ws.send(textutils.serializeJSON({ + cId=switch.id, + type="SWITCH_UPDATE", + activeConfigId=configId + })) + return true + end + end + return false +end + +local function trainOverhead(sb) + return rs.getInput(sb.rsSide) +end + +local function wasTrainOverhead(sb) + return sb.trainOverhead == true +end + +local function getTrainData(sb) + return sb.augment.consist() +end + +-- Sends an update to the API regarding a segment boundary event. The event +-- should be either "ENTERING" if a train has just entered a segment, or +-- "ENTERED" if a train has just completely entered a segment and left its +-- previous segment. +local function sendSegmentBoundaryUpdate(sb, eventType) + local dir = string.upper(sb.trainLastData.direction) + local toSegmentId = nil + for _, segment in pairs(sb.segments) do + if string.upper(segment.direction) == dir then + toSegmentId = segment.id + break + end + end + print("Sending sb update: " .. toSegmentId .. ", " .. eventType) + ws.send(textutils.serializeJSON({ + cId = sb.id, + type = "SEGMENT_BOUNDARY_UPDATE", + toSegmentId = toSegmentId, + eventType = eventType + })) +end + +local function handleRedstoneEvent() + for _, component in pairs(config.components) do + if component.type == "SEGMENT_BOUNDARY" then + local sb = component + if trainOverhead(sb) and not wasTrainOverhead(sb) then + local data = getTrainData(sb) + if data ~= nil then + sb.trainOverhead = true + sb.trainLastData = data + sendSegmentBoundaryUpdate(sb, "ENTERING") + sb.trainScanTimer = os.startTimer(config.trainScanInterval) end + end end + end end -local function updateBranchStatus(signal, branchId, status, config) - for _, branch in pairs(signal.branches) do - if branch.id == branchId then - updateBranchStatusIndicator(branch, status, config) - return - end +-- Handles timer events that happen when we periodically check if a train that +-- was overhead is still there. +local function handleTrainScanTimerEvent(timerId) + for _, component in pairs(config.components) do + if component.trainScanTimer == timerId then + local sb = component + if trainOverhead(sb) then -- train is still overhead. + local data = getTrainData(sb) + if data ~= nil then sb.trainLastData = data end + -- Schedule another timer to check again later. + sb.trainOverhead = true + sb.trainScanTimer = os.startTimer(config.trainScanInterval) + else -- The train has left the signal, so send an update. + sendSegmentBoundaryUpdate(sb, "ENTERED") + sb.trainOverhead = false + sb.trainLastData = nil + sb.trainScanTimer = nil + end end + end end --- Manually set the status indicator for all branch monitors. -local function setAllBranchStatus(config, status) - for _, signal in pairs(config.signals) do - for _, branch in pairs(signal.branches) do - updateBranchStatusIndicator(branch, status, config) - end - end +-- Process status updates for segments that we receive from the API. +local function handleSegmentStatusUpdate(msg) + local signal = getComponentById(msg.cId) + if signal ~= nil and signal.segment.id == msg.segmentId then + signal.segment.occupied = msg.occupied + displaySignal(signal) + end end -local function initMonitorColors(config) - for _, signal in pairs(config.signals) do - for _, branch in pairs(signal.branches) do - if branch.monitor ~= nil then - local mon = peripheral.wrap(branch.monitor) - if mon ~= nil then - for c, v in pairs(config.paletteColors) do - mon.setPaletteColor(c, v) - end - else - print("Error! Could not connect to monitor " .. branch.monitor .. " for branch " .. branch.id .. ". Check and fix config.") - end - end - end - end +local function handleSwitchUpdate(msg) + local switch = getComponentById(msg.cId) + if switch ~= nil then + setSwitchActiveConfig(switch, msg.activeConfigId) + end end --- Checks if all signals managed by this controller are reporting an "online" status. -local function checkSignalOnlineStatus(config) - for _, signal in pairs(config.signals) do - local resp = http.get(config.apiUrl .. "/railSystems/" .. config.rsId .. "/signals/" .. signal.id) - if resp == nil or resp.getResponseCode() ~= 200 then - return false - else - local signalData = textutils.unserializeJSON(resp.readAll()) - if not signalData.online then - return false - end - end - end - return true +local function handleWebsocketMessage(msg) + local data = textutils.unserializeJSON(msg) + if data.type == "SEGMENT_STATUS" then + handleSegmentStatusUpdate(data) + elseif data.type == "SWITCH_UPDATE" then + handleSwitchUpdate(data) + elseif data.type == "ERROR" then + print("Rail Signal sent an error: " .. data.message) + end end -local function handleRedstoneEvent(ws, config) - for _, signal in pairs(config.signals) do - if trainOverhead(signal) and not wasTrainOverheadPreviously(signal) then - local data = getTrainData(signal) - if data == nil then - print("Got redstone event but could not obtain train data on signal " .. signal.id) - else - lastTrainOverheadDataObjs[signal.id] = data - lastTrainOverheadStates[signal.id] = true - sendSignalUpdate(ws, signal, data, "BEGIN") - trainScanTimers[signal.id] = os.startTimer(config.trainScanInterval) - end - end +-- Augments the configuration data with extra information from the API, such +-- as rail system information and component names. +local function initApiData() + local tokenData = fetchJson("/lt/" .. config.linkToken) + if tokenData == nil then + print("Error: Could not fetch device component data from the Rail Signal API.") + return false + end + for _, component in pairs(tokenData.components) do + local c = getComponentById(component.id) + if c == nil then + print("Error: This device's link token is linked to component with id " .. component.id .. " but no component with that id is configured.") + return false + else + c.name = component.name end + end + config.rsName = tokenData.rsName + config.rsId = tokenData.rsId + return true end -local function handleTrainScanTimerEvent(ws, config, timerId) - for k, signal in pairs(config.signals) do - if trainScanTimers[signal.id] == timerId then - if trainOverhead(signal) then -- The train is still overhead. - local data = getTrainData(signal) - if data ~= nil then - lastTrainOverheadDataObjs[signal.id] = data - end - trainScanTimers[signal.id] = os.startTimer(config.trainScanInterval) - else -- The train has left the signal so send an update. - sendSignalUpdate(ws, signal, lastTrainOverheadDataObjs[signal.id], "END") - lastTrainOverheadDataObjs[signal.id] = nil - lastTrainOverheadStates[signal.id] = nil - trainScanTimers[signal.id] = nil - end - return - end - end - print("Warn: Train scan timer was ignored.") +-- MAIN SCRIPT +print("Rail Signal Device Driver " .. VERSION .. " for CC:Tweaked computers") +print(" By Andrew Lalis ") +config = loadConfig() +print("Loaded config.") + +if initApiData() then + print("Data fetched from Rail Signal API successfully.") + print("Initialized for rail system \"" .. config.rsName .. "\", ID: " .. config.rsId) +else + print("Could not fetch data from Rail Signal API.") end -local function handleWebSocketMessage(msg, config) - local data = textutils.unserializeJSON(msg) - local branchId = data["branchId"] - local status = data["status"] - print("<- B: " .. branchId .. ", Status: " .. status) - for _, signal in pairs(config.signals) do - updateBranchStatus(signal, branchId, status, config) +ws = connectToWebsocket() + +-- Initialize all components. +for _, c in pairs(config.components) do + if c.type == "SIGNAL" then + c.segment.monitor = peripheral.wrap(c.segment.monitorId) + local m = c.segment.monitor + m.setPaletteColor(colors.white, 0xFFFFFF) + m.setPaletteColor(colors.red, 0xFF0000) + m.setPaletteColor(colors.lime, 0x00FF00) + m.setPaletteColor(colors.blue, 0x0000FF) + displaySignal(c) + elseif c.type == "SEGMENT_BOUNDARY" then + c.augment = peripheral.wrap(c.augmentId) + elseif c.type == "SWITCH" then + local state = rs.getOutput(c.rsSide) + local activeConfigId = nil + for _, cfg in pairs(c.possibleConfigurations) do + if cfg.rsOutput == state then + activeConfigId = cfg.id + end end + ws.send(textutils.serializeJSON({cid=c.id, type="SWITCH_UPDATE", activeConfigId=activeConfigId})) + end end -local function loadConfig(file) - local f = io.open(file, "r") - if f == nil then return createConfig(file) end - local text = f:read("*a") - f:close() - return textutils.unserialize(text) -end - --- Connects to the RailSignal API websocket. Will block indefinitely until a connection can be obtained. -local function connectToWebSocket(config) - local signalIds = {} - for _, signal in pairs(config.signals) do - table.insert(signalIds, tostring(signal.id)) - end - local signalIdsStr = table.concat(signalIds, ",") - while true do - local ws, err = http.websocket(config.wsUrl, {[config.wsHeader] = signalIdsStr}) - if ws == false then - print("Error connecting to RailSignal websocket:\n\tError: " .. err .. "\n\tTrying again in 3 seconds.") - os.sleep(3) - else - print("Successfully connected to RailSignal websocket at " .. config.wsUrl .. " for managing signals: " .. signalIdsStr) - return ws - end - end -end - --- Main Script -term.clear() -print("RailSignal Signal Controller V" .. VERSION) -print(" By Andrew Lalis. For more info, check here: https://github.com/andrewlalis/RailSignalAPI") -print(" To update, execute \"sig update\" in the command line.") -local w, h = term.getSize() -print(string.rep("-", w)) -if arg[1] ~= nil and string.lower(arg[1]) == "update" then - print("Updating to the latest version of RailSignal signal program.") - fs.delete("sig.lua") - shell.execute("pastebin", "get", "erA3mSfd", "sig.lua") - print("Download complete. Restarting...") - os.sleep(1) - os.reboot() -end -local config = loadConfig("sig_config.tbl") -initMonitorColors(config) -setAllBranchStatus(config, "NONE") -local ws = connectToWebSocket(config) -local refreshWebSocketAlarm = os.setAlarm((os.time() + math.random(1, 23)) % 24) while true do - local eventData = {os.pullEvent()} - local event = eventData[1] - if event == "redstone" then - handleRedstoneEvent(ws, config) - elseif event == "timer" then - handleTrainScanTimerEvent(ws, config, eventData[2]) - elseif event == "websocket_message" then - handleWebSocketMessage(eventData[3], config) - elseif event == "websocket_closed" then - setAllBranchStatus(config, "ERROR") - print("! RailSignal websocket closed. Attempting to reconnect.") - os.sleep(0.5) - ws = connectToWebSocket(config) - elseif event == "alarm" and eventData[2] == refreshWebSocketAlarm then - print("! Checking signal online status.") - if not checkSignalOnlineStatus(config) then - print("Not all signals are reporting an online status. Reconnecting to the websocket.") - ws.close() - ws = connectToWebSocket(config) - end + local eventData = {os.pullEvent()} + local event = eventData[1] + if event == "redstone" then + handleRedstoneEvent() + elseif event == "websocket_message" then + handleWebsocketMessage(eventData[3]) + elseif event == "websocket_closed" then + for _, component in pairs(config.components) do + if component.type == "SIGNAL" then + component.segment.occupied = nil + displaySignal(component) + end end + print("Rail Signal websocket closed. Attempting to reconnect.") + os.sleep(0.5) + ws = connectToWebsocket() + elseif event == "timer" then + handleTrainScanTimerEvent(eventData[2]) + end end -ws.close() \ No newline at end of file