Updated switch service and driver.lua

This commit is contained in:
Andrew Lalis 2022-05-29 17:02:33 +02:00
parent 3b2797f259
commit d9a4ec31c4
2 changed files with 245 additions and 207 deletions

View File

@ -3,14 +3,16 @@ package nl.andrewl.railsignalapi.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.dao.ComponentRepository; import nl.andrewl.railsignalapi.dao.ComponentRepository;
import nl.andrewl.railsignalapi.live.ComponentDownlinkService; 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.ErrorMessage;
import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage; import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
import nl.andrewl.railsignalapi.live.websocket.AppUpdateService; import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
import nl.andrewl.railsignalapi.model.component.Switch; import nl.andrewl.railsignalapi.model.component.Switch;
import nl.andrewl.railsignalapi.model.component.SwitchConfiguration;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
/** /**
* Service for managing switches. * Service for managing switches.
*/ */
@ -24,17 +26,20 @@ public class SwitchService {
@Transactional @Transactional
public void onSwitchUpdate(SwitchUpdateMessage msg) { public void onSwitchUpdate(SwitchUpdateMessage msg) {
switchRepository.findById(msg.cId).ifPresent(sw -> { Optional<Switch> optionalSwitch = switchRepository.findById(msg.cId);
sw.getPossibleConfigurations().stream() if (optionalSwitch.isPresent()) {
.filter(c -> c.getId().equals(msg.activeConfigId)) Switch sw = optionalSwitch.get();
.findFirst() for (var config : sw.getPossibleConfigurations()) {
.ifPresentOrElse(config -> { if (config.getId().equals(msg.activeConfigId)) {
sw.setActiveConfiguration(config); sw.setActiveConfiguration(config);
switchRepository.save(sw); switchRepository.save(sw);
appUpdateService.sendComponentUpdate(sw.getRailSystem().getId(), sw.getId()); appUpdateService.sendUpdate(sw.getRailSystem().getId(), new ComponentDataMessage(sw));
}, () -> { return;
downlinkService.sendMessage(new ErrorMessage(sw.getId(), "Invalid active config id.")); }
}); }
}); downlinkService.sendMessage(new ErrorMessage(msg.cId, "Invalid config id."));
} else {
downlinkService.sendMessage(new ErrorMessage(msg.cId, "Unknown switch."));
}
} }
} }

View File

@ -1,235 +1,268 @@
local VERSION = "1.0.2" -- Rail Signal CC:Tweaked Driver
local VERSION = "1.0.0"
-- Global component states -- Global config. Will be loaded at start of script.
local components = {} local config = {}
-- Global websocket reference
local ws = nil
-- Global signal-indexed state maps. local function loadConfig()
local lastTrainOverheadStates = {} local configFile = io.open("rs_config.tbl", "r")
local lastTrainOverheadDataObjs = {} if not configFile then
local trainScanTimers = {} return nil
end
-- Determines if a train is currently traversing the given signal. local txt = configFile:read("*a")
local function trainOverhead(signal) local cfg = textutils.unserialize(txt)
return redstone.getInput(signal.redstoneInputSide) configFile:close()
return cfg
end end
-- Determines if a train was over the given signal the last time we checked. local function fetchJson(endpoint)
local function wasTrainOverheadPreviously(signal) local response, msg, r = http.get({
return lastTrainOverheadStates[signal.id] == true url = config.apiUrl .. endpoint,
method = "GET"
})
if response then
return textutils.unserialiseJSON(response.readAll())
else
print("Error: " .. r.getResponseCode())
return nil
end
end end
local function getTrainData(signal) -- Blocking function to obtain a websocket connection to Rail Signal.
local det = peripheral.wrap(signal.detector) local function connectToWebsocket()
if det == nil then local url = config.wsUrl .. "?token=" .. config.linkToken
print("Error: Signal " .. signal.id .. "'s detector could not be connected. Please fix the config and restart.") 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 else
return det.consist() print("Successfully connected to Rail Signal API websocket.")
return ws
end end
end
end end
-- Returns the from, to branches for a signal when a train travels in the given direction. local function getComponentById(id)
local function getBranches(dir, signal) for _, c in pairs(config.components) do
local branches = signal.branches if c.id == id then
if string.upper(branches[1].direction) == string.upper(dir) then return c
return branches[2].id, branches[1].id
elseif string.upper(branches[2].direction) == string.upper(dir) then
return branches[1].id, branches[2].id
end end
end
return nil
end end
-- Sends an update to the RailSignal API. local function displaySignal(signal)
local function sendSignalUpdate(ws, signal, data, msgType) local color = colors.white
local from, to = getBranches(string.upper(data.direction), signal) if signal.segment.occupied ~= nil then
local msg = textutils.serializeJSON({ if signal.segment.occupied then
signalId = signal.id, color = colors.red
fromBranchId = from, else
toBranchId = to, color = colors.lime
type = msgType end
}) end
print("-> S: " .. signal.id .. ", from: " .. from .. ", to: " .. to .. ", T: " .. msgType) signal.segment.monitor.setBackgroundColor(color)
ws.send(msg) signal.segment.monitor.clear()
end end
local function updateBranchStatusIndicator(branch, status, config) local function setSwitchActiveConfig(switch, configId)
if branch.monitor ~= nil then for _, cfg in pairs(switch.possibleConfigurations) do
local mon = peripheral.wrap(branch.monitor) if cfg.id == configId then
if mon ~= nil then print("Setting active config id: " .. configId)
local c = config.statusColors[status] rs.setOutput(switch.rsSide, cfg.rsOutput)
if c == nil then c = config.statusColors.ERROR end ws.send(textutils.serializeJSON({
mon.setBackgroundColor(c) cId=switch.id,
mon.clear() type="SWITCH_UPDATE",
else activeConfigId=configId
print("Error! Could not connect to monitor " .. branch.monitor .. " for branch " .. branch.id .. ". Check and fix config.") }))
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
end
end end
local function updateBranchStatus(signal, branchId, status, config) -- Handles timer events that happen when we periodically check if a train that
for _, branch in pairs(signal.branches) do -- was overhead is still there.
if branch.id == branchId then local function handleTrainScanTimerEvent(timerId)
updateBranchStatusIndicator(branch, status, config) for _, component in pairs(config.components) do
return if component.trainScanTimer == timerId then
end 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
end end
-- Manually set the status indicator for all branch monitors. -- Process status updates for segments that we receive from the API.
local function setAllBranchStatus(config, status) local function handleSegmentStatusUpdate(msg)
for _, signal in pairs(config.signals) do local signal = getComponentById(msg.cId)
for _, branch in pairs(signal.branches) do if signal ~= nil and signal.segment.id == msg.segmentId then
updateBranchStatusIndicator(branch, status, config) signal.segment.occupied = msg.occupied
end displaySignal(signal)
end end
end end
local function initMonitorColors(config) local function handleSwitchUpdate(msg)
for _, signal in pairs(config.signals) do local switch = getComponentById(msg.cId)
for _, branch in pairs(signal.branches) do if switch ~= nil then
if branch.monitor ~= nil then setSwitchActiveConfig(switch, msg.activeConfigId)
local mon = peripheral.wrap(branch.monitor) end
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
end end
-- Checks if all signals managed by this controller are reporting an "online" status. local function handleWebsocketMessage(msg)
local function checkSignalOnlineStatus(config) local data = textutils.unserializeJSON(msg)
for _, signal in pairs(config.signals) do if data.type == "SEGMENT_STATUS" then
local resp = http.get(config.apiUrl .. "/railSystems/" .. config.rsId .. "/signals/" .. signal.id) handleSegmentStatusUpdate(data)
if resp == nil or resp.getResponseCode() ~= 200 then elseif data.type == "SWITCH_UPDATE" then
return false handleSwitchUpdate(data)
else elseif data.type == "ERROR" then
local signalData = textutils.unserializeJSON(resp.readAll()) print("Rail Signal sent an error: " .. data.message)
if not signalData.online then end
return false
end
end
end
return true
end end
local function handleRedstoneEvent(ws, config) -- Augments the configuration data with extra information from the API, such
for _, signal in pairs(config.signals) do -- as rail system information and component names.
if trainOverhead(signal) and not wasTrainOverheadPreviously(signal) then local function initApiData()
local data = getTrainData(signal) local tokenData = fetchJson("/lt/" .. config.linkToken)
if data == nil then if tokenData == nil then
print("Got redstone event but could not obtain train data on signal " .. signal.id) print("Error: Could not fetch device component data from the Rail Signal API.")
else return false
lastTrainOverheadDataObjs[signal.id] = data end
lastTrainOverheadStates[signal.id] = true for _, component in pairs(tokenData.components) do
sendSignalUpdate(ws, signal, data, "BEGIN") local c = getComponentById(component.id)
trainScanTimers[signal.id] = os.startTimer(config.trainScanInterval) if c == nil then
end print("Error: This device's link token is linked to component with id " .. component.id .. " but no component with that id is configured.")
end return false
else
c.name = component.name
end end
end
config.rsName = tokenData.rsName
config.rsId = tokenData.rsId
return true
end end
local function handleTrainScanTimerEvent(ws, config, timerId) -- MAIN SCRIPT
for k, signal in pairs(config.signals) do print("Rail Signal Device Driver " .. VERSION .. " for CC:Tweaked computers")
if trainScanTimers[signal.id] == timerId then print(" By Andrew Lalis <andrewlalisofficial@gmail.com>")
if trainOverhead(signal) then -- The train is still overhead. config = loadConfig()
local data = getTrainData(signal) print("Loaded config.")
if data ~= nil then
lastTrainOverheadDataObjs[signal.id] = data if initApiData() then
end print("Data fetched from Rail Signal API successfully.")
trainScanTimers[signal.id] = os.startTimer(config.trainScanInterval) print("Initialized for rail system \"" .. config.rsName .. "\", ID: " .. config.rsId)
else -- The train has left the signal so send an update. else
sendSignalUpdate(ws, signal, lastTrainOverheadDataObjs[signal.id], "END") print("Could not fetch data from Rail Signal API.")
lastTrainOverheadDataObjs[signal.id] = nil
lastTrainOverheadStates[signal.id] = nil
trainScanTimers[signal.id] = nil
end
return
end
end
print("Warn: Train scan timer was ignored.")
end end
local function handleWebSocketMessage(msg, config) ws = connectToWebsocket()
local data = textutils.unserializeJSON(msg)
local branchId = data["branchId"] -- Initialize all components.
local status = data["status"] for _, c in pairs(config.components) do
print("<- B: " .. branchId .. ", Status: " .. status) if c.type == "SIGNAL" then
for _, signal in pairs(config.signals) do c.segment.monitor = peripheral.wrap(c.segment.monitorId)
updateBranchStatus(signal, branchId, status, config) 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 end
ws.send(textutils.serializeJSON({cid=c.id, type="SWITCH_UPDATE", activeConfigId=activeConfigId}))
end
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 while true do
local eventData = {os.pullEvent()} local eventData = {os.pullEvent()}
local event = eventData[1] local event = eventData[1]
if event == "redstone" then if event == "redstone" then
handleRedstoneEvent(ws, config) handleRedstoneEvent()
elseif event == "timer" then elseif event == "websocket_message" then
handleTrainScanTimerEvent(ws, config, eventData[2]) handleWebsocketMessage(eventData[3])
elseif event == "websocket_message" then elseif event == "websocket_closed" then
handleWebSocketMessage(eventData[3], config) for _, component in pairs(config.components) do
elseif event == "websocket_closed" then if component.type == "SIGNAL" then
setAllBranchStatus(config, "ERROR") component.segment.occupied = nil
print("! RailSignal websocket closed. Attempting to reconnect.") displaySignal(component)
os.sleep(0.5) end
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
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 end
ws.close()