diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index ae6b21c..d1fda80 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,8 +1,8 @@ -function scada_link(comms) +function scada_link(plc_comms) local linked = false local link_timeout = os.startTimer(5) - comms.send_link_req() + plc_comms.send_link_req() print_ts("sent link request") repeat @@ -12,31 +12,40 @@ function scada_link(comms) if event == "timer" and param1 == link_timeout then -- no response yet print("...no response"); - comms.send_link_req() - print_ts("sent link request") - link_timeout = os.startTimer(5) elseif event == "modem_message" then -- server response? cancel timeout if link_timeout ~= nil then os.cancelTimer(link_timeout) end - local packet = comms.make_packet(p1, p2, p3, p4, p5) - - -- handle response - local response = comms.handle_link(packet) - if response == nil then - print_ts("invalid link response, bad channel?\n") - return - elseif response == true then - print_ts("...linked!\n") - linked = true - else - print_ts("...denied, exiting\n") - return + local s_packet = comms.scada_packet() + s_packet.receive(p1, p2, p3, p4, p5) + local packet = s_packet.as_rplc() + if packet then + -- handle response + local response = plc_comms.handle_link(packet) + if response == nil then + print_ts("invalid link response, bad channel?\n") + break + elseif response == comms.RPLC_LINKING.COLLISION then + print_ts("...reactor PLC ID collision (check config), exiting...\n") + break + elseif response == comms.RPLC_LINKING.ALLOW then + print_ts("...linked!\n") + linked = true + plc_comms.send_rs_io_conns() + plc_comms.send_struct() + plc_comms.send_status() + print_ts("sent initial data\n") + else + print_ts("...denied, exiting...\n") + break + end end end until linked + + return linked end -- Internal Safety System @@ -44,13 +53,15 @@ end -- autonomous from main control function iss_init(reactor) local self = { - _reactor = reactor, - _tripped = false, - _trip_cause = "" + reactor = reactor, + timed_out = false, + tripped = false, + trip_cause = "" } local check = function () local status = "ok" + local was_tripped = self.tripped -- check system states in order of severity if self.damage_critical() then @@ -63,59 +74,100 @@ function iss_init(reactor) status = "full_waste" elseif self.insufficient_fuel() then status = "no_fuel" - elseif self._tripped then - status = self._trip_cause + elseif self.tripped then + status = self.trip_cause else - self._tripped = false + self.tripped = false end if status ~= "ok" then - self._tripped = true - self._trip_cause = status - self._reactor.scram() + self.tripped = true + self.trip_cause = status + self.reactor.scram() end + + local first_trip = ~was_tripped and self.tripped - return self._tripped, status + return self.tripped, status, first_trip + end + + local trip_timeout = function () + self.tripped = false + self.trip_cause = "timeout" + self.timed_out = true + self.reactor.scram() end local reset = function () - self._tripped = false - self._trip_cause = "" + self.timed_out = false + self.tripped = false + self.trip_cause = "" + end + + local status = function (named) + if named then + return { + damage_critical = damage_critical(), + excess_heated_coolant = excess_heated_coolant(), + excess_waste = excess_waste(), + high_temp = high_temp(), + insufficient_fuel = insufficient_fuel(), + no_coolant = no_coolant(), + timed_out = timed_out() + } + else + return { + damage_critical(), + excess_heated_coolant(), + excess_waste(), + high_temp(), + insufficient_fuel(), + no_coolant(), + timed_out() + } + end end local damage_critical = function () - return self._reactor.getDamagePercent() >= 100 + return self.reactor.getDamagePercent() >= 100 end local excess_heated_coolant = function () - return self._reactor.getHeatedCoolantNeeded() == 0 + return self.reactor.getHeatedCoolantNeeded() == 0 end local excess_waste = function () - return self._reactor.getWasteNeeded() == 0 + return self.reactor.getWasteNeeded() == 0 end local high_temp = function () -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 - return self._reactor.getTemperature() >= 1200 + return self.reactor.getTemperature() >= 1200 end local insufficient_fuel = function () - return self._reactor.getFuel() == 0 + return self.reactor.getFuel() == 0 end - local no_coolant = function() - return self._reactor.getCoolantFilledPercentage() < 2 + local no_coolant = function () + return self.reactor.getCoolantFilledPercentage() < 2 + end + + local timed_out = function () + return self.timed_out end return { check = check, + trip_timeout = trip_timeout, reset = reset, + status = status, damage_critical = damage_critical, excess_heated_coolant = excess_heated_coolant, excess_waste = excess_waste, high_temp = high_temp, insufficient_fuel = insufficient_fuel, - no_coolant = no_coolant + no_coolant = no_coolant, + timed_out = timed_out } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e10024a..6d2d9de 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -37,11 +37,13 @@ if not modem.isOpen(config.LISTEN_PORT) then modem.open(config.LISTEN_PORT) end -local comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local plc_comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) -- attempt server connection --- exits application if connection is denied -plc.scada_link(comms) +-- exit application if connection is denied +if ~plc.scada_link(plc_comms) then + return +end -- comms watchdog, 3 second timeout local conn_watchdog = watchdog.new_watchdog(3) @@ -51,28 +53,47 @@ local conn_watchdog = watchdog.new_watchdog(3) local loop_tick = os.startTimer(0.05) local ticks_to_update = 5 +-- runtime variables +local control_state = false + -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEvent() + if event == "peripheral_detach" then + print_ts("[fatal] lost a peripheral, stopping...\n") + -- todo: determine which disconnected and what is left + -- hopefully it wasn't the reactor + reactor.scram() + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_DC) ? + return + end + -- check safety (SCRAM occurs if tripped) - local iss_status, iss_tripped = iss.check() + local iss_status, iss_tripped, iss_first = iss.check() + if iss_first then + plc_comms.send_iss_alarm(iss_status) + end -- handle event if event == "timer" and param1 == loop_tick then - -- basic event tick, send updated data if it is time + -- basic event tick, send updated data if it is time (4Hz) ticks_to_update = ticks_to_update - 1 if ticks_to_update == 0 then + plc_comms.send_status(control_state, iss_tripped) ticks_to_update = 5 end elseif event == "modem_message" then -- got a packet - -- feed the watchdog first so it doesn't eat our packets + -- feed the watchdog first so it doesn't uhh,,,eat our packets conn_watchdog.feed() + local packet = comms.make_packet(p1, p2, p3, p4, p5) + plc_comms.handle_packet(packet) + elseif event == "timer" and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown - reactor.scram() + iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index aa2ab38..23af375 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -5,16 +5,47 @@ PROTOCOLS = { COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller } +RPLC_TYPES = { + KEEP_ALIVE = 0, -- keep alive packets + LINK_REQ = 1, -- linking requests + STATUS = 2, -- reactor/system status + MEK_STRUCT = 3, -- mekanism build structure + RS_IO_CONNS = 4, -- redstone I/O connections + RS_IO_SET = 5, -- set redstone outputs + RS_IO_GET = 6, -- get redstone inputs + MEK_SCRAM = 7, -- SCRAM reactor + MEK_ENABLE = 8, -- enable reactor + MEK_BURN_RATE = 9, -- set burn rate + ISS_ALARM = 10, -- ISS alarm broadcast + ISS_GET = 11, -- get ISS status + ISS_CLEAR = 12 -- clear ISS trip (if in bad state, will trip immideatly) +} + +RPLC_LINKING = { + ALLOW = 0, + DENY = 1, + COLLISION = 2 +} + -- generic SCADA packet object function scada_packet() local self = { modem_msg_in = nil, - raw = nil + valid = false, seq_id = nil, protocol = nil, - length = nil + length = nil, + raw = nil } + local make = function (seq_id, protocol, payload) + self.valid = true + self.seq_id = seq_id + self.protocol = protocol + self.length = #payload + self.raw = { self.seq_id, self.protocol, self.length, payload } + end + local receive = function (side, sender, reply_to, message, distance) self.modem_msg_in = { iface = side, @@ -30,9 +61,10 @@ function scada_packet() -- malformed return false else - self.seq_id = self.raw[0] - self.protocol = self.raw[1] - self.length = self.raw[2] + self.valid = true + self.seq_id = self.raw[1] + self.protocol = self.raw[2] + self.length = self.raw[3] end end @@ -48,6 +80,14 @@ function scada_packet() return self.length end + local data = function (packet) + local subset = nil + if self.valid then + subset = { table.unpack(self.raw, 4, 3 + self.length) } + end + return subset + end + local raw = function (packet) return self.raw end @@ -56,7 +96,24 @@ function scada_packet() return self.modem_msg_in end + local as_rplc = function () + local pkt = nil + if self.valid and self.protocol == PROTOCOLS.RPLC then + local body = data() + if #body > 2 then + pkt = { + id = body[1], + type = body[2], + length = #body - 2, + body = { table.unpack(body, 3, 2 + #body) } + } + end + end + return pkt + end + return { + make = make, receive = receive, seq_id = seq_id, protocol = protocol, @@ -83,65 +140,77 @@ end -- reactor PLC communications function rplc_comms(id, modem, local_port, server_port, reactor) local self = { - _id = id, - _modem = modem, - _server = server_port, - _local = local_port, - _reactor = reactor, - _status_cache = nil + id = id, + seq_id = 0, + modem = modem, + s_port = server_port, + l_port = local_port, + reactor = reactor, + status_cache = nil } + -- PRIVATE FUNCTIONS -- + local _send = function (msg) - self._modem.transmit(self._server, self._local, msg) + local packet = scada_packet() + packet.make(self.seq_id, PROTOCOLS.RPLC, msg) + self.modem.transmit(self.s_port, self.l_port, packet.raw()) + self.seq_id = self.seq_id + 1 end -- variable reactor status information, excluding heating rate local _reactor_status = function () return { - status = self._reactor.getStatus(), - burn_rate = self._reactor.getBurnRate(), - act_burn_r = self._reactor.getActualBurnRate(), - temp = self._reactor.getTemperature(), - damage = self._reactor.getDamagePercent(), - boil_eff = self._reactor.getBoilEfficiency(), - env_loss = self._reactor.getEnvironmentalLoss(), + status = self.reactor.getStatus(), + burn_rate = self.reactor.getBurnRate(), + act_burn_r = self.reactor.getActualBurnRate(), + temp = self.reactor.getTemperature(), + damage = self.reactor.getDamagePercent(), + boil_eff = self.reactor.getBoilEfficiency(), + env_loss = self.reactor.getEnvironmentalLoss(), - fuel = self._reactor.getFuel(), - fuel_need = self._reactor.getFuelNeeded(), - fuel_fill = self._reactor.getFuelFilledPercentage(), - waste = self._reactor.getWaste(), - waste_need = self._reactor.getWasteNeeded(), - waste_fill = self._reactor.getWasteFilledPercentage(), - cool_type = self._reactor.getCoolant()['name'], - cool_amnt = self._reactor.getCoolant()['amount'], - cool_need = self._reactor.getCoolantNeeded(), - cool_fill = self._reactor.getCoolantFilledPercentage(), - hcool_type = self._reactor.getHeatedCoolant()['name'], - hcool_amnt = self._reactor.getHeatedCoolant()['amount'], - hcool_need = self._reactor.getHeatedCoolantNeeded(), - hcool_fill = self._reactor.getHeatedCoolantFilledPercentage() + fuel = self.reactor.getFuel(), + fuel_need = self.reactor.getFuelNeeded(), + fuel_fill = self.reactor.getFuelFilledPercentage(), + waste = self.reactor.getWaste(), + waste_need = self.reactor.getWasteNeeded(), + waste_fill = self.reactor.getWasteFilledPercentage(), + cool_type = self.reactor.getCoolant()['name'], + cool_amnt = self.reactor.getCoolant()['amount'], + cool_need = self.reactor.getCoolantNeeded(), + cool_fill = self.reactor.getCoolantFilledPercentage(), + hcool_type = self.reactor.getHeatedCoolant()['name'], + hcool_amnt = self.reactor.getHeatedCoolant()['amount'], + hcool_need = self.reactor.getHeatedCoolantNeeded(), + hcool_fill = self.reactor.getHeatedCoolantFilledPercentage() } end - local _status_changed = function () - local status = self._reactor_status() + local _update_status_cache = function () + local status = _reactor_status() local changed = false - for key, value in pairs() do - if value ~= _status_cache[key] then + for key, value in pairs(status) do + if value ~= self.status_cache[key] then changed = true break end end + if changed then + self.status_cache = status + end + return changed end - -- attempt to establish link with + -- PUBLIC FUNCTIONS -- + + -- attempt to establish link with supervisor local send_link_req = function () local linking_data = { - id = self._id, - type = "link_req" + id = self.id, + type = RPLC_TYPES.LINK_REQ } _send(linking_data) @@ -151,19 +220,19 @@ function rplc_comms(id, modem, local_port, server_port, reactor) -- server will cache these local send_struct = function () local mek_data = { - heat_cap = self._reactor.getHeatCapacity(), - fuel_asm = self._reactor.getFuelAssemblies(), - fuel_sa = self._reactor.getFuelSurfaceArea(), - fuel_cap = self._reactor.getFuelCapacity(), - waste_cap = self._reactor.getWasteCapacity(), - cool_cap = self._reactor.getCoolantCapacity(), - hcool_cap = self._reactor.getHeatedCoolantCapacity(), - max_burn = self._reactor.getMaxBurnRate() + heat_cap = self.reactor.getHeatCapacity(), + fuel_asm = self.reactor.getFuelAssemblies(), + fuel_sa = self.reactor.getFuelSurfaceArea(), + fuel_cap = self.reactor.getFuelCapacity(), + waste_cap = self.reactor.getWasteCapacity(), + cool_cap = self.reactor.getCoolantCapacity(), + hcool_cap = self.reactor.getHeatedCoolantCapacity(), + max_burn = self.reactor.getMaxBurnRate() } local struct_packet = { - id = self._id, - type = "struct_data", + id = self.id, + type = RPLC_TYPES.MEK_STRUCT, mek_data = mek_data } @@ -171,49 +240,63 @@ function rplc_comms(id, modem, local_port, server_port, reactor) end -- send live status information - local send_status = function () - local mek_data = self._reactor_status() + -- control_state: acknowledged control state from supervisor + -- overridden: if ISS force disabled reactor + local send_status = function (control_state, overridden) + local mek_data = nil - local sys_data = { - timestamp = os.time(), - control_state = false, - overridden = false, - faults = {}, - waste_production = "antimatter" -- "plutonium", "polonium", "antimatter" - } - end - - local send_keep_alive = function () - -- heating rate is volatile, so it is skipped in status - -- send it with keep alive packets - local mek_data = { - heating_rate = self._reactor.getHeatingRate() - } - - -- basic keep alive packet to server - local keep_alive_packet = { - id = self._id, - type = "keep_alive", + if _update_status_cache() then + mek_data = self.status_cache + end + + local sys_status = { + id = self.id, + type = RPLC_TYPES.STATUS, timestamp = os.time(), + control_state = control_state, + overridden = overridden, + heating_rate = self.reactor.getHeatingRate(), mek_data = mek_data } - _send(keep_alive_packet) + _send(sys_status) + end + + local send_rs_io_conns = function () end local handle_link = function (packet) - if packet.type == "link_response" then - return packet.accepted + if packet.type == RPLC_TYPES.LINK_REQ then + return packet.data[1] == RPLC_LINKING.ALLOW else return nil end end + local handle_packet = function (packet) + if packet.type == RPLC_TYPES.KEEP_ALIVE then + -- keep alive request received, nothing to do except feed watchdog + elseif packet.type == RPLC_TYPES.MEK_STRUCT then + -- request for physical structure + send_struct() + elseif packet.type == RPLC_TYPES.RS_IO_CONNS then + -- request for redstone connections + send_rs_io_conns() + elseif packet.type == RPLC_TYPES.RS_IO_GET then + elseif packet.type == RPLC_TYPES.RS_IO_SET then + elseif packet.type == RPLC_TYPES.MEK_SCRAM then + elseif packet.type == RPLC_TYPES.MEK_ENABLE then + elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then + elseif packet.type == RPLC_TYPES.ISS_GET then + elseif packet.type == RPLC_TYPES.ISS_CLEAR then + end + end + return { send_link_req = send_link_req, send_struct = send_struct, send_status = send_status, - send_keep_alive = send_keep_alive, + send_rs_io_conns = send_rs_io_conns, handle_link = handle_link } end