#33 lua module/require architecture changeover

This commit is contained in:
Mikayla Fischler 2022-05-04 13:37:01 -04:00
parent 7bcb260712
commit b575899d46
33 changed files with 679 additions and 518 deletions

View File

@ -1,4 +1,4 @@
-- #REQUIRES comms.lua
local comms = require("scada-common.comms")
-- coordinator communications
function coord_comms()

View File

@ -2,15 +2,14 @@
-- Nuclear Generation Facility SCADA Coordinator
--
os.loadAPI("scada-common/log.lua")
os.loadAPI("scada-common/util.lua")
os.loadAPI("scada-common/ppm.lua")
os.loadAPI("scada-common/comms.lua")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
os.loadAPI("coordinator/config.lua")
os.loadAPI("coordinator/coordinator.lua")
local config = require("config")
local coordinator = require("coordinator")
local COORDINATOR_VERSION = "alpha-v0.1.1"
local COORDINATOR_VERSION = "alpha-v0.1.2"
local print = util.print
local println = util.println
@ -19,9 +18,9 @@ local println_ts = util.println_ts
log.init("/log.txt", log.MODE.APPEND)
log._info("========================================")
log._info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
log._info("========================================")
log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
log.info("========================================")
println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<")
-- mount connected devices

View File

@ -1,14 +1,18 @@
local config = {}
-- set to false to run in offline mode (safety regulation only)
NETWORKED = true
config.NETWORKED = true
-- unique reactor ID
REACTOR_ID = 1
config.REACTOR_ID = 1
-- port to send packets TO server
SERVER_PORT = 16000
config.SERVER_PORT = 16000
-- port to listen to incoming packets FROM server
LISTEN_PORT = 14001
config.LISTEN_PORT = 14001
-- log path
LOG_PATH = "/log.txt"
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
LOG_MODE = 0
config.LOG_MODE = 0
return config

View File

@ -1,7 +1,10 @@
-- #REQUIRES types.lua
-- #REQUIRES comms.lua
-- #REQUIRES ppm.lua
-- #REQUIRES util.lua
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local plc = {}
local iss_status_t = types.iss_status_t
@ -18,7 +21,7 @@ local println_ts = util.println_ts
-- Internal Safety System
-- identifies dangerous states and SCRAMs reactor if warranted
-- autonomous from main SCADA supervisor/coordinator control
function iss_init(reactor)
plc.iss_init = function (reactor)
local self = {
reactor = reactor,
cache = { false, false, false, false, false, false, false },
@ -34,7 +37,7 @@ function iss_init(reactor)
local damage_percent = self.reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
log._error("ISS: failed to check reactor damage")
log.error("ISS: failed to check reactor damage")
return false
else
return damage_percent >= 100
@ -46,7 +49,7 @@ function iss_init(reactor)
local hc_needed = self.reactor.getHeatedCoolantNeeded()
if hc_needed == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
log._error("ISS: failed to check reactor heated coolant level")
log.error("ISS: failed to check reactor heated coolant level")
return false
else
return hc_needed == 0
@ -58,7 +61,7 @@ function iss_init(reactor)
local w_needed = self.reactor.getWasteNeeded()
if w_needed == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
log._error("ISS: failed to check reactor waste level")
log.error("ISS: failed to check reactor waste level")
return false
else
return w_needed == 0
@ -71,7 +74,7 @@ function iss_init(reactor)
local temp = self.reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
log._error("ISS: failed to check reactor temperature")
log.error("ISS: failed to check reactor temperature")
return false
else
return temp >= 1200
@ -83,7 +86,7 @@ function iss_init(reactor)
local fuel = self.reactor.getFuel()
if fuel == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
log._error("ISS: failed to check reactor fuel level")
log.error("ISS: failed to check reactor fuel level")
return false
else
return fuel == 0
@ -95,7 +98,7 @@ function iss_init(reactor)
local coolant_filled = self.reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
log._error("ISS: failed to check reactor coolant level")
log.error("ISS: failed to check reactor coolant level")
return false
else
return coolant_filled < 0.02
@ -134,25 +137,25 @@ function iss_init(reactor)
if self.tripped then
status = self.trip_cause
elseif self.cache[1] then
log._warning("ISS: damage critical!")
log.warning("ISS: damage critical!")
status = iss_status_t.dmg_crit
elseif self.cache[4] then
log._warning("ISS: high temperature!")
log.warning("ISS: high temperature!")
status = iss_status_t.high_temp
elseif self.cache[2] then
log._warning("ISS: heated coolant backup!")
log.warning("ISS: heated coolant backup!")
status = iss_status_t.ex_hcoolant
elseif self.cache[6] then
log._warning("ISS: no coolant!")
log.warning("ISS: no coolant!")
status = iss_status_t.no_coolant
elseif self.cache[3] then
log._warning("ISS: full waste!")
log.warning("ISS: full waste!")
status = iss_status_t.ex_waste
elseif self.cache[5] then
log._warning("ISS: no fuel!")
log.warning("ISS: no fuel!")
status = iss_status_t.no_fuel
elseif self.cache[7] then
log._warning("ISS: supervisor connection timeout!")
log.warning("ISS: supervisor connection timeout!")
status = iss_status_t.timeout
else
self.tripped = false
@ -161,7 +164,7 @@ function iss_init(reactor)
-- if a new trip occured...
local first_trip = false
if not was_tripped and status ~= iss_status_t.ok then
log._warning("ISS: reactor SCRAM")
log.warning("ISS: reactor SCRAM")
first_trip = true
self.tripped = true
@ -169,7 +172,7 @@ function iss_init(reactor)
self.reactor.scram()
if self.reactor.__p_is_faulted() then
log._error("ISS: failed reactor SCRAM")
log.error("ISS: failed reactor SCRAM")
end
end
@ -198,7 +201,7 @@ function iss_init(reactor)
end
-- reactor PLC communications
function comms_init(id, modem, local_port, server_port, reactor, iss)
plc.comms = function (id, modem, local_port, server_port, reactor, iss)
local self = {
id = id,
seq_num = 0,
@ -355,7 +358,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
if not self.reactor.__p_is_faulted() then
_send(RPLC_TYPES.MEK_STRUCT, mek_data)
else
log._error("failed to send structure: PPM fault")
log.error("failed to send structure: PPM fault")
end
end
@ -417,7 +420,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
if not self.reactor.__p_is_faulted() then
_send(RPLC_TYPES.STATUS, sys_status)
else
log._error("failed to send status: PPM fault")
log.error("failed to send status: PPM fault")
end
end
end
@ -463,7 +466,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
pkt = mgmt_pkt.get()
end
else
log._error("illegal packet type " .. s_pkt.protocol(), true)
log.error("illegal packet type " .. s_pkt.protocol(), true)
end
end
@ -477,7 +480,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif self.linked and self.r_seq_num >= packet.scada_frame.seq_num() then
log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
else
self.r_seq_num = packet.scada_frame.seq_num()
@ -496,19 +499,19 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
local trip_time = util.time() - timestamp
if trip_time > 500 then
log._warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")")
log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")")
end
-- log._debug("RPLC RTT = ".. trip_time .. "ms")
-- log.debug("RPLC RTT = ".. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else
log._debug("RPLC keep alive packet length mismatch")
log.debug("RPLC keep alive packet length mismatch")
end
elseif packet.type == RPLC_TYPES.LINK_REQ then
-- link request confirmation
if packet.length == 1 then
log._debug("received unsolicited link request response")
log.debug("received unsolicited link request response")
local link_ack = packet.data[1]
@ -516,31 +519,31 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
self.status_cache = nil
_send_struct()
send_status(plc_state.degraded)
log._debug("re-sent initial status data")
log.debug("re-sent initial status data")
elseif link_ack == RPLC_LINKING.DENY then
println_ts("received unsolicited link denial, unlinking")
log._debug("unsolicited RPLC link request denied")
log.debug("unsolicited RPLC link request denied")
elseif link_ack == RPLC_LINKING.COLLISION then
println_ts("received unsolicited link collision, unlinking")
log._warning("unsolicited RPLC link request collision")
log.warning("unsolicited RPLC link request collision")
else
println_ts("invalid unsolicited link response")
log._error("unsolicited unknown RPLC link request response")
log.error("unsolicited unknown RPLC link request response")
end
self.linked = link_ack == RPLC_LINKING.ALLOW
else
log._debug("RPLC link req packet length mismatch")
log.debug("RPLC link req packet length mismatch")
end
elseif packet.type == RPLC_TYPES.STATUS then
-- request of full status, clear cache first
self.status_cache = nil
send_status(plc_state.degraded)
log._debug("sent out status cache again, did supervisor miss it?")
log.debug("sent out status cache again, did supervisor miss it?")
elseif packet.type == RPLC_TYPES.MEK_STRUCT then
-- request for physical structure
_send_struct()
log._debug("sent out structure again, did supervisor miss it?")
log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPES.MEK_SCRAM then
-- disable the reactor
self.scrammed = true
@ -576,14 +579,14 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
_send_ack(packet.type, success)
else
log._debug("RPLC set burn rate packet length mismatch")
log.debug("RPLC set burn rate packet length mismatch")
end
elseif packet.type == RPLC_TYPES.ISS_CLEAR then
-- clear the ISS status
iss.reset()
_send_ack(packet.type, true)
else
log._warning("received unknown RPLC packet type " .. packet.type)
log.warning("received unknown RPLC packet type " .. packet.type)
end
elseif packet.type == RPLC_TYPES.LINK_REQ then
-- link request confirmation
@ -592,7 +595,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
if link_ack == RPLC_LINKING.ALLOW then
println_ts("linked!")
log._debug("RPLC link request approved")
log.debug("RPLC link request approved")
-- reset remote sequence number and cache
self.r_seq_num = nil
@ -601,24 +604,24 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
_send_struct()
send_status(plc_state.degraded)
log._debug("sent initial status data")
log.debug("sent initial status data")
elseif link_ack == RPLC_LINKING.DENY then
println_ts("link request denied, retrying...")
log._debug("RPLC link request denied")
log.debug("RPLC link request denied")
elseif link_ack == RPLC_LINKING.COLLISION then
println_ts("reactor PLC ID collision (check config), retrying...")
log._warning("RPLC link request collision")
log.warning("RPLC link request collision")
else
println_ts("invalid link response, bad channel? retrying...")
log._error("unknown RPLC link request response")
log.error("unknown RPLC link request response")
end
self.linked = link_ack == RPLC_LINKING.ALLOW
else
log._debug("RPLC link req packet length mismatch")
log.debug("RPLC link req packet length mismatch")
end
else
log._debug("discarding non-link packet before linked")
log.debug("discarding non-link packet before linked")
end
elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
-- handle session close
@ -626,9 +629,9 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
conn_watchdog.cancel()
unlink()
println_ts("server connection closed by remote host")
log._warning("server connection closed by remote host")
log.warning("server connection closed by remote host")
else
log._warning("received unknown SCADA_MGMT packet type " .. packet.type)
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
end
end
end
@ -652,3 +655,5 @@ function comms_init(id, modem, local_port, server_port, reactor, iss)
is_linked = is_linked
}
end
return plc

View File

@ -2,18 +2,16 @@
-- Reactor Programmable Logic Controller
--
os.loadAPI("scada-common/log.lua")
os.loadAPI("scada-common/types.lua")
os.loadAPI("scada-common/util.lua")
os.loadAPI("scada-common/ppm.lua")
os.loadAPI("scada-common/comms.lua")
os.loadAPI("scada-common/mqueue.lua")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
os.loadAPI("config.lua")
os.loadAPI("plc.lua")
os.loadAPI("threads.lua")
local config = require("config")
local plc = require("plc")
local threads = require("threads")
local R_PLC_VERSION = "alpha-v0.5.2"
local R_PLC_VERSION = "alpha-v0.6.0"
local print = util.print
local println = util.println
@ -22,9 +20,9 @@ local println_ts = util.println_ts
log.init(config.LOG_PATH, config.LOG_MODE)
log._info("========================================")
log._info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
log._info("========================================")
log.info("========================================")
log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
log.info("========================================")
println(">> Reactor PLC " .. R_PLC_VERSION .. " <<")
-- mount connected devices
@ -78,7 +76,7 @@ local plc_state = __shared_memory.plc_state
-- we need a reactor and a modem
if smem_dev.reactor == nil then
println("boot> fission reactor not found");
log._warning("no reactor on startup")
log.warning("no reactor on startup")
plc_state.init_ok = false
plc_state.degraded = true
@ -86,7 +84,7 @@ if smem_dev.reactor == nil then
end
if networked and smem_dev.modem == nil then
println("boot> wireless modem not found")
log._warning("no wireless modem on startup")
log.warning("no wireless modem on startup")
if smem_dev.reactor ~= nil then
smem_dev.reactor.scram()
@ -104,19 +102,19 @@ function init()
-- init internal safety system
smem_sys.iss = plc.iss_init(smem_dev.reactor)
log._debug("iss init")
log.debug("iss init")
if __shared_memory.networked then
-- start comms
smem_sys.plc_comms = plc.comms_init(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.iss)
log._debug("comms init")
smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.iss)
log.debug("comms init")
-- comms watchdog, 3 second timeout
smem_sys.conn_watchdog = util.new_watchdog(3)
log._debug("conn watchdog started")
log.debug("conn watchdog started")
else
println("boot> starting in offline mode");
log._debug("running without networking")
log.debug("running without networking")
end
os.queueEvent("clock_start")
@ -124,7 +122,7 @@ function init()
println("boot> completed");
else
println("boot> system in degraded state, awaiting devices...")
log._warning("booted in a degraded state, awaiting peripheral connections...")
log.warning("booted in a degraded state, awaiting peripheral connections...")
end
end
@ -148,11 +146,11 @@ if __shared_memory.networked then
if plc_state.init_ok then
-- send status one last time after ISS shutdown
plc_comms.send_status(plc_state.degraded)
plc_comms.send_iss_status()
smem_sys.plc_comms.send_status(plc_state.degraded)
smem_sys.plc_comms.send_iss_status()
-- close connection
plc_comms.close(conn_watchdog)
smem_sys.plc_comms.close(smem_sys.conn_watchdog)
end
else
-- run threads, excluding comms
@ -160,4 +158,4 @@ else
end
println_ts("exited")
log._info("exited")
log.info("exited")

View File

@ -1,7 +1,9 @@
-- #REQUIRES comms.lua
-- #REQUIRES log.lua
-- #REQUIRES ppm.lua
-- #REQUIRES util.lua
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local threads = {}
local print = util.print
local println = util.println
@ -28,10 +30,10 @@ local MQ__COMM_CMD = {
}
-- main thread
function thread__main(smem, init)
threads.thread__main = function (smem, init)
-- execute thread
local exec = function ()
log._debug("main thread init, clock inactive")
log.debug("main thread init, clock inactive")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick)
-- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks)
@ -89,14 +91,14 @@ function thread__main(smem, init)
if device.type == "fissionReactor" then
println_ts("reactor disconnected!")
log._error("reactor disconnected!")
log.error("reactor disconnected!")
plc_state.no_reactor = true
plc_state.degraded = true
elseif networked and device.type == "modem" then
-- we only care if this is our wireless modem
if device.dev == plc_dev.modem then
println_ts("wireless modem disconnected!")
log._error("comms modem disconnected!")
log.error("comms modem disconnected!")
plc_state.no_modem = true
if plc_state.init_ok then
@ -106,7 +108,7 @@ function thread__main(smem, init)
plc_state.degraded = true
else
log._warning("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end
end
elseif event == "peripheral" then
@ -120,7 +122,7 @@ function thread__main(smem, init)
smem.q.mq_iss.push_command(MQ__ISS_CMD.SCRAM)
println_ts("reactor reconnected.")
log._info("reactor reconnected.")
log.info("reactor reconnected.")
plc_state.no_reactor = false
if plc_state.init_ok then
@ -144,7 +146,7 @@ function thread__main(smem, init)
end
println_ts("wireless modem reconnected.")
log._info("comms modem reconnected.")
log.info("comms modem reconnected.")
plc_state.no_modem = false
-- determine if we are still in a degraded state
@ -152,7 +154,7 @@ function thread__main(smem, init)
plc_state.degraded = false
end
else
log._info("wired modem reconnected.")
log.info("wired modem reconnected.")
end
end
@ -163,12 +165,12 @@ function thread__main(smem, init)
elseif event == "clock_start" then
-- start loop clock
loop_clock = os.startTimer(MAIN_CLOCK)
log._debug("main thread clock started")
log.debug("main thread clock started")
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
log._info("terminate requested, main thread exiting")
log.info("terminate requested, main thread exiting")
-- iss handles reactor shutdown
plc_state.shutdown = true
break
@ -180,10 +182,10 @@ function thread__main(smem, init)
end
-- ISS monitor thread
function thread__iss(smem)
threads.thread__iss = function (smem)
-- execute thread
local exec = function ()
log._debug("iss thread start")
log.debug("iss thread start")
-- load in from shared memory
local networked = smem.networked
@ -257,17 +259,17 @@ function thread__iss(smem)
plc_state.scram = true
if reactor.scram() then
println_ts("successful reactor SCRAM")
log._error("successful reactor SCRAM")
log.error("successful reactor SCRAM")
else
println_ts("failed reactor SCRAM")
log._error("failed reactor SCRAM")
log.error("failed reactor SCRAM")
end
elseif msg.message == MQ__ISS_CMD.TRIP_TIMEOUT then
-- watchdog tripped
plc_state.scram = true
iss.trip_timeout()
println_ts("server timeout")
log._warning("server timeout")
log.warning("server timeout")
end
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
@ -282,19 +284,19 @@ function thread__iss(smem)
-- check for termination request
if plc_state.shutdown then
-- safe exit
log._info("iss thread shutdown initiated")
log.info("iss thread shutdown initiated")
if plc_state.init_ok then
plc_state.scram = true
reactor.scram()
if reactor.__p_is_ok() then
println_ts("reactor disabled")
log._info("iss thread reactor SCRAM OK")
log.info("iss thread reactor SCRAM OK")
else
println_ts("exiting, reactor failed to disable")
log._error("iss thread failed to SCRAM reactor on exit")
log.error("iss thread failed to SCRAM reactor on exit")
end
end
log._info("iss thread exiting")
log.info("iss thread exiting")
break
end
@ -307,10 +309,10 @@ function thread__iss(smem)
end
-- communications sender thread
function thread__comms_tx(smem)
threads.thread__comms_tx = function (smem)
-- execute thread
local exec = function ()
log._debug("comms tx thread start")
log.debug("comms tx thread start")
-- load in from shared memory
local plc_state = smem.plc_state
@ -345,7 +347,7 @@ function thread__comms_tx(smem)
-- check for termination request
if plc_state.shutdown then
log._info("comms tx thread exiting")
log.info("comms tx thread exiting")
break
end
@ -358,10 +360,10 @@ function thread__comms_tx(smem)
end
-- communications handler thread
function thread__comms_rx(smem)
threads.thread__comms_rx = function (smem)
-- execute thread
local exec = function ()
log._debug("comms rx thread start")
log.debug("comms rx thread start")
-- load in from shared memory
local plc_state = smem.plc_state
@ -397,7 +399,7 @@ function thread__comms_rx(smem)
-- check for termination request
if plc_state.shutdown then
log._info("comms rx thread exiting")
log.info("comms rx thread exiting")
break
end
@ -410,10 +412,10 @@ function thread__comms_rx(smem)
end
-- apply setpoints
function thread__setpoint_control(smem)
threads.thread__setpoint_control = function (smem)
-- execute thread
local exec = function ()
log._debug("setpoint control thread start")
log.debug("setpoint control thread start")
-- load in from shared memory
local plc_state = smem.plc_state
@ -434,10 +436,10 @@ function thread__setpoint_control(smem)
if not plc_state.scram then
if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then
-- update without ramp if <= 5 mB/t change
log._debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t")
log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t")
reactor.setBurnRate(setpoints.burn_rate)
else
log._debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t")
log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t")
running = true
end
@ -489,7 +491,7 @@ function thread__setpoint_control(smem)
-- check for termination request
if plc_state.shutdown then
log._info("setpoint control thread exiting")
log.info("setpoint control thread exiting")
break
end
@ -500,3 +502,5 @@ function thread__setpoint_control(smem)
return { exec = exec }
end
return threads

View File

@ -1,47 +1,49 @@
-- #REQUIRES rsio.lua
local rsio = require("scada-common.rsio")
local config = {}
-- port to send packets TO server
SERVER_PORT = 16000
config.SERVER_PORT = 16000
-- port to listen to incoming packets FROM server
LISTEN_PORT = 15001
config.LISTEN_PORT = 15001
-- log path
LOG_PATH = "/log.txt"
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
LOG_MODE = 0
config.LOG_MODE = 0
-- RTU peripheral devices (named: side/network device name)
RTU_DEVICES = {
config.RTU_DEVICES = {
{
name = "boiler_0",
name = "boiler_1",
index = 1,
for_reactor = 1
},
{
name = "turbine_0",
name = "turbine_1",
index = 1,
for_reactor = 1
}
}
-- RTU redstone interface definitions
RTU_REDSTONE = {
config.RTU_REDSTONE = {
{
for_reactor = 1,
io = {
{
channel = rsio.RS_IO.WASTE_PO,
channel = rsio.IO.WASTE_PO,
side = "top",
bundled_color = colors.blue,
for_reactor = 1
},
{
channel = rsio.RS_IO.WASTE_PU,
channel = rsio.IO.WASTE_PU,
side = "top",
bundled_color = colors.cyan,
for_reactor = 1
},
{
channel = rsio.RS_IO.WASTE_AM,
channel = rsio.IO.WASTE_AM,
side = "top",
bundled_color = colors.purple,
for_reactor = 1
@ -49,3 +51,5 @@ RTU_REDSTONE = {
}
}
}
return config

View File

@ -1,8 +1,10 @@
-- #REQUIRES rtu.lua
local rtu = require("rtu")
function new(boiler)
local boiler_rtu = {}
boiler_rtu.new = function (boiler)
local self = {
rtu = rtu.rtu_init(),
rtu = rtu.init_unit(),
boiler = boiler
}
@ -49,3 +51,5 @@ function new(boiler)
rtu_interface = rtu_interface
}
end
return boiler_rtu

View File

@ -1,8 +1,10 @@
-- #REQUIRES rtu.lua
local rtu = require("rtu")
function new(boiler)
local boilerv_rtu = {}
boilerv_rtu.new = function (boiler)
local self = {
rtu = rtu.rtu_init(),
rtu = rtu.init_unit(),
boiler = boiler
}
@ -54,3 +56,5 @@ function new(boiler)
rtu_interface = rtu_interface
}
end
return boilerv_rtu

View File

@ -1,8 +1,10 @@
-- #REQUIRES rtu.lua
local rtu = require("rtu")
function new(machine)
local energymachine_rtu = {}
energymachine_rtu.new = function (machine)
local self = {
rtu = rtu.rtu_init(),
rtu = rtu.init_unit(),
machine = machine
}
@ -31,3 +33,5 @@ function new(machine)
rtu_interface = rtu_interface
}
end
return energymachine_rtu

View File

@ -1,8 +1,10 @@
-- #REQUIRES rtu.lua
local rtu = require("rtu")
function new(imatrix)
local imatrix_rtu = {}
imatrix_rtu.new = function (imatrix)
local self = {
rtu = rtu.rtu_init(),
rtu = rtu.init_unit(),
imatrix = imatrix
}
@ -42,3 +44,5 @@ function new(imatrix)
rtu_interface = rtu_interface
}
end
return imatrix_rtu

View File

@ -1,13 +1,14 @@
-- #REQUIRES rtu.lua
-- #REQUIRES rsio.lua
-- note: this RTU makes extensive use of the programming concept of closures
local rtu = require("rtu")
local rsio = require("scada-common.rsio")
local redstone_rtu = {}
local digital_read = rsio.digital_read
local digital_is_active = rsio.digital_is_active
function new()
redstone_rtu.new = function ()
local self = {
rtu = rtu.rtu_init()
rtu = rtu.init_unit()
}
local rtu_interface = function ()
@ -91,3 +92,5 @@ function new()
link_ao = link_ao
}
end
return redstone_rtu

View File

@ -1,8 +1,10 @@
-- #REQUIRES rtu.lua
local rtu = require("rtu")
function new(turbine)
local turbine_rtu = {}
turbine_rtu.new = function (turbine)
local self = {
rtu = rtu.rtu_init(),
rtu = rtu.init_unit(),
turbine = turbine
}
@ -44,3 +46,5 @@ function new(turbine)
rtu_interface = rtu_interface
}
end
return turbine_rtu

View File

@ -1,8 +1,10 @@
-- #REQUIRES rtu.lua
local rtu = require("rtu")
function new(turbine)
local turbinev_rtu = {}
turbinev_rtu.new = function (turbine)
local self = {
rtu = rtu.rtu_init(),
rtu = rtu.init_unit(),
turbine = turbine
}
@ -14,8 +16,8 @@ function new(turbine)
-- none
-- coils --
self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end), function () end)
self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end), function () end)
self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end, function () end)
self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end, function () end)
-- input registers --
-- multiblock properties
@ -54,3 +56,5 @@ function new(turbine)
rtu_interface = rtu_interface
}
end
return turbinev_rtu

View File

@ -1,10 +1,13 @@
-- #REQUIRES types.lua
local comms = require("scada-common.comms")
local types = require("scada-common.types")
local modbus = {}
local MODBUS_FCODE = types.MODBUS_FCODE
local MODBUS_EXCODE = types.MODBUS_EXCODE
-- new modbus comms handler object
function new(rtu_dev, use_parallel_read)
modbus.new = function (rtu_dev, use_parallel_read)
local self = {
rtu = rtu_dev,
use_parallel = use_parallel_read
@ -401,3 +404,5 @@ function new(rtu_dev, use_parallel_read)
reply__gw_unavailable = reply__gw_unavailable
}
end
return modbus

View File

@ -1,12 +1,15 @@
-- #REQUIRES comms.lua
-- #REQUIRES modbus.lua
-- #REQUIRES ppm.lua
local comms = require("scada-common.comms")
local ppm = require("scada-common.ppm")
local modbus = require("modbus")
local rtu = {}
local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES
function rtu_init()
rtu.init_unit = function ()
local self = {
discrete_inputs = {},
coils = {},
@ -117,7 +120,7 @@ function rtu_init()
}
end
function rtu_comms(modem, local_port, server_port)
rtu.comms = function (modem, local_port, server_port)
local self = {
seq_num = 0,
r_seq_num = nil,
@ -187,7 +190,7 @@ function rtu_comms(modem, local_port, server_port)
pkt = mgmt_pkt.get()
end
else
log._error("illegal packet type " .. s_pkt.protocol(), true)
log.error("illegal packet type " .. s_pkt.protocol(), true)
end
end
@ -203,7 +206,7 @@ function rtu_comms(modem, local_port, server_port)
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif rtu_state.linked and self.r_seq_num >= packet.scada_frame.seq_num() then
log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
else
self.r_seq_num = packet.scada_frame.seq_num()
@ -224,7 +227,7 @@ function rtu_comms(modem, local_port, server_port)
-- immediately execute redstone RTU requests
local return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then
log._warning("requested MODBUS operation failed")
log.warning("requested MODBUS operation failed")
end
else
-- check validity then pass off to unit comms thread
@ -237,13 +240,13 @@ function rtu_comms(modem, local_port, server_port)
unit.pkt_queue.push(packet)
end
else
log._warning("cannot perform requested MODBUS operation")
log.warning("cannot perform requested MODBUS operation")
end
end
else
-- unit ID out of range?
reply = modbus.reply__gw_unavailable(packet)
log._error("MODBUS packet requesting non-existent unit")
log.error("MODBUS packet requesting non-existent unit")
end
send_modbus(reply)
@ -253,7 +256,7 @@ function rtu_comms(modem, local_port, server_port)
-- close connection
conn_watchdog.cancel()
unlink(rtu_state)
if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then
elseif packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then
-- acknowledgement
rtu_state.linked = true
self.r_seq_num = nil
@ -262,11 +265,11 @@ function rtu_comms(modem, local_port, server_port)
send_advertisement(units)
else
-- not supported
log._warning("RTU got unexpected SCADA message type " .. packet.type, true)
log.warning("RTU got unexpected SCADA message type " .. packet.type, true)
end
else
-- should be unreachable assuming packet is from parse_packet()
log._error("illegal packet type " .. protocol, true)
log.error("illegal packet type " .. protocol, true)
end
end
end
@ -337,3 +340,5 @@ function rtu_comms(modem, local_port, server_port)
close = close
}
end
return rtu

View File

@ -2,28 +2,27 @@
-- RTU: Remote Terminal Unit
--
os.loadAPI("scada-common/log.lua")
os.loadAPI("scada-common/types.lua")
os.loadAPI("scada-common/util.lua")
os.loadAPI("scada-common/ppm.lua")
os.loadAPI("scada-common/comms.lua")
os.loadAPI("scada-common/mqueue.lua")
os.loadAPI("scada-common/rsio.lua")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
os.loadAPI("config.lua")
os.loadAPI("modbus.lua")
os.loadAPI("rtu.lua")
os.loadAPI("threads.lua")
local config = require("config")
local modbus = require("modbus")
local rtu = require("rtu")
local threads = require("threads")
os.loadAPI("dev/redstone_rtu.lua")
os.loadAPI("dev/boiler_rtu.lua")
os.loadAPI("dev/boilerv_rtu.lua")
os.loadAPI("dev/energymachine_rtu.lua")
os.loadAPI("dev/imatrix_rtu.lua")
os.loadAPI("dev/turbine_rtu.lua")
os.loadAPI("dev/turbinev_rtu.lua")
local redstone_rtu = require("dev.redstone_rtu")
local boiler_rtu = require("dev.boiler_rtu")
local boilerv_rtu = require("dev.boilerv_rtu")
local energymachine_rtu = require("dev.energymachine_rtu")
local imatrix_rtu = require("dev.imatrix_rtu")
local turbine_rtu = require("dev.turbine_rtu")
local turbinev_rtu = require("dev.turbinev_rtu")
local RTU_VERSION = "alpha-v0.5.0"
local RTU_VERSION = "alpha-v0.6.0"
local rtu_t = types.rtu_t
@ -34,9 +33,9 @@ local println_ts = util.println_ts
log.init(config.LOG_PATH, config.LOG_MODE)
log._info("========================================")
log._info("BOOTING rtu.startup " .. RTU_VERSION)
log._info("========================================")
log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION)
log.info("========================================")
println(">> RTU " .. RTU_VERSION .. " <<")
----------------------------------------
@ -77,11 +76,11 @@ local smem_sys = __shared_memory.rtu_sys
-- get modem
if smem_dev.modem == nil then
println("boot> wireless modem not found")
log._warning("no wireless modem on startup")
log.warning("no wireless modem on startup")
return
end
smem_sys.rtu_comms = rtu.rtu_comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT)
smem_sys.rtu_comms = rtu.comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT)
----------------------------------------
-- interpret config and init units
@ -99,7 +98,7 @@ for reactor_idx = 1, #rtu_redstone do
local capabilities = {}
log._debug("init> starting redstone RTU I/O linking for reactor " .. rtu_redstone[reactor_idx].for_reactor .. "...")
log.debug("init> starting redstone RTU I/O linking for reactor " .. rtu_redstone[reactor_idx].for_reactor .. "...")
for i = 1, #io_table do
local valid = false
@ -118,7 +117,7 @@ for reactor_idx = 1, #rtu_redstone do
local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. reactor_idx ..
" (for reactor " .. rtu_redstone[reactor_idx].for_reactor .. ")"
println_ts(message)
log._warning(message)
log.warning(message)
else
-- link redstone in RTU
local mode = rsio.get_io_mode(conf.channel)
@ -132,13 +131,13 @@ for reactor_idx = 1, #rtu_redstone do
rs_rtu.link_ao(conf.channel, conf.side)
else
-- should be unreachable code, we already validated channels
log._error("init> fell through if chain attempting to identify IO mode", true)
log.error("init> fell through if chain attempting to identify IO mode", true)
break
end
table.insert(capabilities, conf.channel)
log._debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side ..
log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side ..
") for reactor " .. rtu_redstone[reactor_idx].for_reactor)
end
end
@ -156,7 +155,7 @@ for reactor_idx = 1, #rtu_redstone do
thread = nil
})
log._debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor)
log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor)
end
-- mounted peripherals
@ -166,7 +165,7 @@ for i = 1, #rtu_devices do
if device == nil then
local message = "init> '" .. rtu_devices[i].name .. "' not found"
println_ts(message)
log._warning(message)
log.warning(message)
else
local type = ppm.get_type(rtu_devices[i].name)
local rtu_iface = nil
@ -200,7 +199,7 @@ for i = 1, #rtu_devices do
else
local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")"
println_ts(message)
log._warning(message)
log.warning(message)
end
if rtu_iface ~= nil then
@ -221,7 +220,7 @@ for i = 1, #rtu_devices do
table.insert(units, rtu_unit)
log._debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" ..
log.debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" ..
rtu_devices[i].index .. "] for reactor " .. rtu_devices[i].for_reactor)
end
end
@ -237,7 +236,7 @@ local comms_thread = threads.thread__comms(__shared_memory)
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(5)
log._debug("init> conn watchdog started")
log.debug("init> conn watchdog started")
-- assemble thread list
local _threads = { main_thread.exec, comms_thread.exec }
@ -251,4 +250,4 @@ end
parallel.waitForAll(table.unpack(_threads))
println_ts("exited")
log._info("exited")
log.info("exited")

View File

@ -1,7 +1,22 @@
-- #REQUIRES comms.lua
-- #REQUIRES log.lua
-- #REQUIRES ppm.lua
-- #REQUIRES util.lua
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local redstone_rtu = require("dev.redstone_rtu")
local boiler_rtu = require("dev.boiler_rtu")
local boilerv_rtu = require("dev.boilerv_rtu")
local energymachine_rtu = require("dev.energymachine_rtu")
local imatrix_rtu = require("dev.imatrix_rtu")
local turbine_rtu = require("dev.turbine_rtu")
local turbinev_rtu = require("dev.turbinev_rtu")
local modbus = require("modbus")
local threads = {}
local rtu_t = types.rtu_t
local print = util.print
local println = util.println
@ -14,10 +29,10 @@ local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
local COMMS_SLEEP = 150 -- (150ms, 3 ticks)
-- main thread
function thread__main(smem)
threads.thread__main = function (smem)
-- execute thread
local exec = function ()
log._debug("main thread start")
log.debug("main thread start")
-- advertisement/heartbeat clock
local loop_clock = os.startTimer(MAIN_CLOCK)
@ -62,9 +77,9 @@ function thread__main(smem)
-- we only care if this is our wireless modem
if device.dev == rtu_dev.modem then
println_ts("wireless modem disconnected!")
log._warning("comms modem disconnected!")
log.warning("comms modem disconnected!")
else
log._warning("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end
else
for i = 1, #units do
@ -88,9 +103,9 @@ function thread__main(smem)
rtu_comms.reconnect_modem(rtu_dev.modem)
println_ts("wireless modem reconnected.")
log._info("comms modem reconnected.")
log.info("comms modem reconnected.")
else
log._info("wired modem reconnected.")
log.info("wired modem reconnected.")
end
else
-- relink lost peripheral to correct unit entry
@ -102,11 +117,17 @@ function thread__main(smem)
-- found, re-link
unit.device = device
if unit.type == "boiler" then
if unit.type == rtu_t.boiler then
unit.rtu = boiler_rtu.new(device)
elseif unit.type == "turbine" then
elseif unit.type == rtu_t.boiler_valve then
unit.rtu = boilerv_rtu.new(device)
elseif unit.type == rtu_t.turbine then
unit.rtu = turbine_rtu.new(device)
elseif unit.type == "imatrix" then
elseif unit.type == rtu_t.turbine_valve then
unit.rtu = turbinev_rtu.new(device)
elseif unit.type == rtu_t.energy_machine then
unit.rtu = energymachine_rtu.new(device)
elseif unit.type == rtu_t.induction_matrix then
unit.rtu = imatrix_rtu.new(device)
end
@ -121,7 +142,7 @@ function thread__main(smem)
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
rtu_state.shutdown = true
log._info("terminate requested, main thread exiting")
log.info("terminate requested, main thread exiting")
break
end
end
@ -131,10 +152,10 @@ function thread__main(smem)
end
-- communications handler thread
function thread__comms(smem)
threads.thread__comms = function (smem)
-- execute thread
local exec = function ()
log._debug("comms thread start")
log.debug("comms thread start")
-- load in from shared memory
local rtu_state = smem.rtu_state
@ -169,8 +190,8 @@ function thread__comms(smem)
-- check for termination request
if rtu_state.shutdown then
rtu_comms.close()
log._info("comms thread exiting")
rtu_comms.close(rtu_state)
log.info("comms thread exiting")
break
end
@ -183,10 +204,10 @@ function thread__comms(smem)
end
-- per-unit communications handler thread
function thread__unit_comms(smem, unit)
threads.thread__unit_comms = function (smem, unit)
-- execute thread
local exec = function ()
log._debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")")
log.debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")")
-- load in from shared memory
local rtu_state = smem.rtu_state
@ -219,7 +240,7 @@ function thread__unit_comms(smem, unit)
-- check for termination request
if rtu_state.shutdown then
log._info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")")
log.info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")")
break
end
@ -230,3 +251,5 @@ function thread__unit_comms(smem, unit)
return { exec = exec }
end
return threads

View File

@ -1,4 +1,6 @@
-- #REQUIRES util.lua
local util = require("scada-common.util")
local alarm = {}
SEVERITY = {
INFO = 0, -- basic info message
@ -9,7 +11,27 @@ SEVERITY = {
EMERGENCY = 5 -- critical safety alarm
}
function scada_alarm(severity, device, message)
alarm.SEVERITY = SEVERITY
alarm.severity_to_string = function (severity)
if severity == SEVERITY.INFO then
return "INFO"
elseif severity == SEVERITY.WARNING then
return "WARNING"
elseif severity == SEVERITY.ALERT then
return "ALERT"
elseif severity == SEVERITY.FACILITY then
return "FACILITY"
elseif severity == SEVERITY.SAFETY then
return "SAFETY"
elseif severity == SEVERITY.EMERGENCY then
return "EMERGENCY"
else
return "UNKNOWN"
end
end
alarm.scada_alarm = function (severity, device, message)
local self = {
time = util.time(),
ts_string = os.date("[%H:%M:%S]"),
@ -19,7 +41,7 @@ function scada_alarm(severity, device, message)
}
local format = function ()
return self.ts_string .. " [" .. severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message
return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message
end
local properties = function ()
@ -37,20 +59,4 @@ function scada_alarm(severity, device, message)
}
end
function severity_to_string(severity)
if severity == SEVERITY.INFO then
return "INFO"
elseif severity == SEVERITY.WARNING then
return "WARNING"
elseif severity == SEVERITY.ALERT then
return "ALERT"
elseif severity == SEVERITY.FACILITY then
return "FACILITY"
elseif severity == SEVERITY.SAFETY then
return "SAFETY"
elseif severity == SEVERITY.EMERGENCY then
return "EMERGENCY"
else
return "UNKNOWN"
end
end
return alarm

View File

@ -1,4 +1,10 @@
PROTOCOLS = {
--
-- Communications
--
local comms = {}
local PROTOCOLS = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
@ -6,7 +12,7 @@ PROTOCOLS = {
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
RPLC_TYPES = {
local RPLC_TYPES = {
KEEP_ALIVE = 0, -- keep alive packets
LINK_REQ = 1, -- linking requests
STATUS = 2, -- reactor/system status
@ -19,13 +25,13 @@ RPLC_TYPES = {
ISS_CLEAR = 9 -- clear ISS trip (if in bad state, will trip immediately)
}
RPLC_LINKING = {
local RPLC_LINKING = {
ALLOW = 0, -- link approved
DENY = 1, -- link denied
COLLISION = 2 -- link denied due to existing active link
}
SCADA_MGMT_TYPES = {
local SCADA_MGMT_TYPES = {
PING = 0, -- generic ping
CLOSE = 1, -- close a connection
REMOTE_LINKED = 2, -- remote device linked
@ -33,15 +39,21 @@ SCADA_MGMT_TYPES = {
RTU_HEARTBEAT = 4 -- RTU heartbeat
}
RTU_ADVERT_TYPES = {
local RTU_ADVERT_TYPES = {
BOILER = 0, -- boiler
TURBINE = 1, -- turbine
IMATRIX = 2, -- induction matrix
REDSTONE = 3 -- redstone I/O
}
comms.PROTOCOLS = PROTOCOLS
comms.RPLC_TYPES = RPLC_TYPES
comms.RPLC_LINKING = RPLC_LINKING
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
comms.RTU_ADVERT_TYPES = RTU_ADVERT_TYPES
-- generic SCADA packet object
function scada_packet()
comms.scada_packet = function ()
local self = {
modem_msg_in = nil,
valid = false,
@ -124,7 +136,7 @@ end
-- MODBUS packet
-- modeled after MODBUS TCP packet
function modbus_packet()
comms.modbus_packet = function ()
local self = {
frame = nil,
raw = nil,
@ -165,11 +177,11 @@ function modbus_packet()
return size_ok
else
log._debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
log.debug("nil frame encountered", true)
return false
end
end
@ -201,7 +213,7 @@ function modbus_packet()
end
-- reactor PLC packet
function rplc_packet()
comms.rplc_packet = function ()
local self = {
frame = nil,
raw = nil,
@ -256,11 +268,11 @@ function rplc_packet()
return ok
else
log._debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
log.debug("nil frame encountered", true)
return false
end
end
@ -291,7 +303,7 @@ function rplc_packet()
end
-- SCADA management packet
function mgmt_packet()
comms.mgmt_packet = function ()
local self = {
frame = nil,
raw = nil,
@ -339,11 +351,11 @@ function mgmt_packet()
return ok
else
log._debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
log.debug("nil frame encountered", true)
return false
end
end
@ -374,7 +386,7 @@ end
-- SCADA coordinator packet
-- @todo
function coord_packet()
comms.coord_packet = function ()
local self = {
frame = nil,
raw = nil,
@ -418,11 +430,11 @@ function coord_packet()
return ok
else
log._debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
log.debug("nil frame encountered", true)
return false
end
end
@ -453,7 +465,7 @@ end
-- coordinator API (CAPI) packet
-- @todo
function capi_packet()
comms.capi_packet = function ()
local self = {
frame = nil,
raw = nil,
@ -497,11 +509,11 @@ function capi_packet()
return ok
else
log._debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true)
log.debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true)
return false
end
else
log._debug("nil frame encountered", true)
log.debug("nil frame encountered", true)
return false
end
end
@ -529,3 +541,5 @@ function capi_packet()
get = get
}
end
return comms

View File

@ -2,14 +2,17 @@
-- File System Logger
--
-- we use extra short abbreviations since computer craft screens are very small
-- underscores are used since some of these names are used elsewhere (e.g. 'debug' is a lua table)
local log = {}
MODE = {
-- we use extra short abbreviations since computer craft screens are very small
local MODE = {
APPEND = 0,
NEW = 1
}
log.MODE = MODE
local LOG_DEBUG = true
local log_path = "/log.txt"
@ -50,7 +53,7 @@ local _log = function (msg)
end
end
function init(path, write_mode)
log.init = function (path, write_mode)
log_path = path
mode = write_mode
@ -61,7 +64,7 @@ function init(path, write_mode)
end
end
function _debug(msg, trace)
log.debug = function (msg, trace)
if LOG_DEBUG then
local dbg_info = ""
@ -80,15 +83,15 @@ function _debug(msg, trace)
end
end
function _info(msg)
log.info = function (msg)
_log("[INF] " .. msg)
end
function _warning(msg)
log.warning = function (msg)
_log("[WRN] " .. msg)
end
function _error(msg, trace)
log.error = function (msg, trace)
local dbg_info = ""
if trace then
@ -105,6 +108,8 @@ function _error(msg, trace)
_log("[ERR] " .. dbg_info .. msg)
end
function _fatal(msg)
log.fatal = function (msg)
_log("[FTL] " .. msg)
end
return log

View File

@ -2,13 +2,17 @@
-- Message Queue
--
TYPE = {
local mqueue = {}
local TYPE = {
COMMAND = 0,
DATA = 1,
PACKET = 2
}
function new()
mqueue.TYPE = TYPE
mqueue.new = function ()
local queue = {}
local length = function ()
@ -57,3 +61,5 @@ function new()
pop = pop
}
end
return mqueue

View File

@ -1,10 +1,14 @@
-- #REQUIRES log.lua
local log = require("scada-common.log")
--
-- Protected Peripheral Manager
--
ACCESS_FAULT = nil
local ppm = {}
local ACCESS_FAULT = nil
ppm.ACCESS_FAULT = ACCESS_FAULT
----------------------------
-- PRIVATE DATA/FUNCTIONS --
@ -46,7 +50,7 @@ local peri_init = function (iface)
_ppm_sys.faulted = true
if not _ppm_sys.mute then
log._error("PPM: protected " .. key .. "() -> " .. result)
log.error("PPM: protected " .. key .. "() -> " .. result)
end
if result == "Terminated" then
@ -88,48 +92,48 @@ end
-- REPORTING --
-- silence error prints
function disable_reporting()
ppm.disable_reporting = function ()
_ppm_sys.mute = true
end
-- allow error prints
function enable_reporting()
ppm.enable_reporting = function ()
_ppm_sys.mute = false
end
-- FAULT MEMORY --
-- enable automatically clearing fault flag
function enable_afc()
ppm.enable_afc = function ()
_ppm_sys.auto_cf = true
end
-- disable automatically clearing fault flag
function disable_afc()
ppm.disable_afc = function ()
_ppm_sys.auto_cf = false
end
-- check fault flag
function is_faulted()
ppm.is_faulted = function ()
return _ppm_sys.faulted
end
-- clear fault flag
function clear_fault()
ppm.clear_fault = function ()
_ppm_sys.faulted = false
end
-- TERMINATION --
-- if a caught error was a termination request
function should_terminate()
ppm.should_terminate = function ()
return _ppm_sys.terminate
end
-- MOUNTING --
-- mount all available peripherals (clears mounts first)
function mount_all()
ppm.mount_all = function ()
local ifaces = peripheral.getNames()
_ppm_sys.mounts = {}
@ -137,23 +141,23 @@ function mount_all()
for i = 1, #ifaces do
_ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
log._info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")")
log.info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")")
end
if #ifaces == 0 then
log._warning("PPM: mount_all() -> no devices found")
log.warning("PPM: mount_all() -> no devices found")
end
end
-- mount a particular device
function mount(iface)
ppm.mount = function (iface)
local ifaces = peripheral.getNames()
local pm_dev = nil
local pm_type = nil
for i = 1, #ifaces do
if iface == ifaces[i] then
log._info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface))
log.info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface))
_ppm_sys.mounts[iface] = peri_init(iface)
@ -167,15 +171,15 @@ function mount(iface)
end
-- handle peripheral_detach event
function handle_unmount(iface)
ppm.handle_unmount = function (iface)
-- what got disconnected?
local lost_dev = _ppm_sys.mounts[iface]
if lost_dev then
local type = lost_dev.type
log._warning("PPM: lost device " .. type .. " mounted to " .. iface)
log.warning("PPM: lost device " .. type .. " mounted to " .. iface)
else
log._error("PPM: lost device unknown to the PPM mounted to " .. iface)
log.error("PPM: lost device unknown to the PPM mounted to " .. iface)
end
return lost_dev
@ -184,31 +188,31 @@ end
-- GENERAL ACCESSORS --
-- list all available peripherals
function list_avail()
ppm.list_avail = function ()
return peripheral.getNames()
end
-- list mounted peripherals
function list_mounts()
ppm.list_mounts = function ()
return _ppm_sys.mounts
end
-- get a mounted peripheral by side/interface
function get_periph(iface)
ppm.get_periph = function (iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].dev
else return nil end
end
-- get a mounted peripheral type by side/interface
function get_type(iface)
ppm.get_type = function (iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].type
else return nil end
end
-- get all mounted peripherals by type
function get_all_devices(name)
ppm.get_all_devices = function (name)
local devices = {}
for side, data in pairs(_ppm_sys.mounts) do
@ -221,7 +225,7 @@ function get_all_devices(name)
end
-- get a mounted peripheral by type (if multiple, returns the first)
function get_device(name)
ppm.get_device = function (name)
local device = nil
for side, data in pairs(_ppm_sys.mounts) do
@ -237,12 +241,12 @@ end
-- SPECIFIC DEVICE ACCESSORS --
-- get the fission reactor (if multiple, returns the first)
function get_fission_reactor()
return get_device("fissionReactor")
ppm.get_fission_reactor = function ()
return ppm.get_device("fissionReactor")
end
-- get the wireless modem (if multiple, returns the first)
function get_wireless_modem()
ppm.get_wireless_modem = function ()
local w_modem = nil
for side, device in pairs(_ppm_sys.mounts) do
@ -256,6 +260,8 @@ function get_wireless_modem()
end
-- list all connected monitors
function list_monitors()
return get_all_devices("monitor")
ppm.list_monitors = function ()
return ppm.get_all_devices("monitor")
end
return ppm

View File

@ -1,21 +1,27 @@
IO_LVL = {
--
-- Redstone I/O
--
local rsio = {}
local IO_LVL = {
LOW = 0,
HIGH = 1
}
IO_DIR = {
local IO_DIR = {
IN = 0,
OUT = 1
}
IO_MODE = {
local IO_MODE = {
DIGITAL_OUT = 0,
DIGITAL_IN = 1,
ANALOG_OUT = 2,
ANALOG_IN = 3
}
RS_IO = {
local RS_IO = {
-- digital inputs --
-- facility
@ -53,7 +59,12 @@ RS_IO = {
A_T_FLOW_RATE = 21 -- turbine flow rate percentage
}
function to_string(channel)
rsio.IO_LVL = IO_LVL
rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE
rsio.IO = RS_IO
rsio.to_string = function (channel)
local names = {
"F_SCRAM",
"F_AE2_LIVE",
@ -85,11 +96,11 @@ function to_string(channel)
end
end
function is_valid_channel(channel)
rsio.is_valid_channel = function (channel)
return channel ~= nil and channel > 0 and channel <= RS_IO.A_T_FLOW_RATE
end
function is_valid_side(side)
rsio.is_valid_side = function (side)
if side ~= nil then
for _, s in pairs(rs.getSides()) do
if s == side then return true end
@ -98,7 +109,7 @@ function is_valid_side(side)
return false
end
function is_color(color)
rsio.is_color = function (color)
return (color > 0) and (bit.band(color, (color - 1)) == 0);
end
@ -149,7 +160,7 @@ local RS_DIO_MAP = {
{ _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }
}
function get_io_mode(channel)
rsio.get_io_mode = function (channel)
local modes = {
IO_MODE.DIGITAL_IN, -- F_SCRAM
IO_MODE.DIGITAL_IN, -- F_AE2_LIVE
@ -182,7 +193,7 @@ function get_io_mode(channel)
end
-- get digital IO level reading
function digital_read(rs_value)
rsio.digital_read = function (rs_value)
if rs_value then
return IO_LVL.HIGH
else
@ -191,7 +202,7 @@ function digital_read(rs_value)
end
-- returns the level corresponding to active
function digital_write(channel, active)
rsio.digital_write = function (channel, active)
if channel < RS_IO.WASTE_PO or channel > RS_IO.R_PLC_TIMEOUT then
return IO_LVL.LOW
else
@ -200,10 +211,12 @@ function digital_write(channel, active)
end
-- returns true if the level corresponds to active
function digital_is_active(channel, level)
rsio.digital_is_active = function (channel, level)
if channel > RS_IO.R_ENABLE or channel > RS_IO.R_PLC_TIMEOUT then
return false
else
return RS_DIO_MAP[channel]._f(level)
end
end
return rsio

View File

@ -1,6 +1,10 @@
--
-- Global Types
--
rtu_t = {
local types = {}
types.rtu_t = {
redstone = "redstone",
boiler = "boiler",
boiler_valve = "boiler_valve",
@ -10,7 +14,7 @@ rtu_t = {
induction_matrix = "induction_matrix"
}
iss_status_t = {
types.iss_status_t = {
ok = "ok",
dmg_crit = "dmg_crit",
ex_hcoolant = "heated_coolant_backup",
@ -24,7 +28,7 @@ iss_status_t = {
-- MODBUS
-- modbus function codes
local MODBUS_FCODE = {
types.MODBUS_FCODE = {
READ_COILS = 0x01,
READ_DISCRETE_INPUTS = 0x02,
READ_MUL_HOLD_REGS = 0x03,
@ -37,7 +41,7 @@ local MODBUS_FCODE = {
}
-- modbus exception codes
local MODBUS_EXCODE = {
types.MODBUS_EXCODE = {
ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDR = 0x02,
ILLEGAL_DATA_VALUE = 0x03,
@ -49,3 +53,5 @@ local MODBUS_EXCODE = {
GATEWAY_PATH_UNAVAILABLE = 0x0A,
GATEWAY_TARGET_TIMEOUT = 0x0B
}
return types

View File

@ -1,70 +1,69 @@
local util = {}
-- PRINT --
-- we are overwriting 'print' so save it first
local _print = print
-- print
function print(message)
util.print = function (message)
term.write(message)
end
-- print line
function println(message)
_print(message)
util.println = function (message)
print(message)
end
-- timestamped print
function print_ts(message)
util.print_ts = function (message)
term.write(os.date("[%H:%M:%S] ") .. message)
end
-- timestamped print line
function println_ts(message)
_print(os.date("[%H:%M:%S] ") .. message)
util.println_ts = function (message)
print(os.date("[%H:%M:%S] ") .. message)
end
-- TIME --
function time_ms()
util.time_ms = function ()
return os.epoch('local')
end
function time_s()
util.time_s = function ()
return os.epoch('local') / 1000
end
function time()
return time_ms()
util.time = function ()
return util.time_ms()
end
-- PARALLELIZATION --
-- protected sleep call so we still are in charge of catching termination
function psleep(t)
util.psleep = function (t)
pcall(os.sleep, t)
end
-- no-op to provide a brief pause (and a yield)
-- EVENT_CONSUMER: this function consumes events
function nop()
psleep(0.05)
util.nop = function ()
util.psleep(0.05)
end
-- attempt to maintain a minimum loop timing (duration of execution)
function adaptive_delay(target_timing, last_update)
local sleep_for = target_timing - (time() - last_update)
util.adaptive_delay = function (target_timing, last_update)
local sleep_for = target_timing - (util.time() - last_update)
-- only if >50ms since worker loops already yield 0.05s
if sleep_for >= 50 then
psleep(sleep_for / 1000.0)
util.psleep(sleep_for / 1000.0)
end
return time()
return util.time()
end
-- WATCHDOG --
-- ComputerCraft OS Timer based Watchdog
-- triggers a timer event if not fed within 'timeout' seconds
function new_watchdog(timeout)
util.new_watchdog = function (timeout)
local self = {
_timeout = timeout,
_wd_timer = os.startTimer(timeout)
@ -93,3 +92,5 @@ function new_watchdog(timeout)
cancel = cancel
}
end
return util

View File

@ -1,12 +1,16 @@
local config = {}
-- scada network listen for PLC's and RTU's
SCADA_DEV_LISTEN = 16000
config.SCADA_DEV_LISTEN = 16000
-- listen port for SCADA supervisor access by coordinators
SCADA_SV_LISTEN = 16100
config.SCADA_SV_LISTEN = 16100
-- expected number of reactors
NUM_REACTORS = 4
config.NUM_REACTORS = 4
-- log path
LOG_PATH = "/log.txt"
config.LOG_PATH = "/log.txt"
-- log mode
-- 0 = APPEND (adds to existing file on start)
-- 1 = NEW (replaces existing file on start)
LOG_MODE = 0
config.LOG_MODE = 0
return config

View File

@ -0,0 +1,3 @@
local coordinator = {}
return coordinator

View File

@ -1,7 +1,9 @@
-- #REQUIRES mqueue.lua
-- #REQUIRES comms.lua
-- #REQUIRES log.lua
-- #REQUIRES util.lua
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local plc = {}
local PROTOCOLS = comms.PROTOCOLS
local RPLC_TYPES = comms.RPLC_TYPES
@ -16,19 +18,21 @@ local println_ts = util.println_ts
local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
PLC_S_CMDS = {
local PLC_S_CMDS = {
SCRAM = 0,
ENABLE = 1,
BURN_RATE = 2,
ISS_CLEAR = 3
}
plc.PLC_S_CMDS = PLC_S_CMDS
local PERIODICS = {
KEEP_ALIVE = 2.0
}
-- PLC supervisor session
function new_session(id, for_reactor, in_queue, out_queue)
plc.new_session = function (id, for_reactor, in_queue, out_queue)
local log_header = "plc_session(" .. id .. "): "
local self = {
@ -204,7 +208,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
if pkt.length == 1 then
return pkt.data[1]
else
log._warning(log_header .. "RPLC ACK length mismatch")
log.warning(log_header .. "RPLC ACK length mismatch")
return nil
end
end
@ -215,7 +219,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
if self.r_seq_num == nil then
self.r_seq_num = pkt.scada_frame.seq_num()
elseif self.r_seq_num >= pkt.scada_frame.seq_num() then
log._warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num()
@ -225,7 +229,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then
-- check reactor ID
if pkt.id ~= for_reactor then
log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id)
log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id)
return
end
@ -242,13 +246,13 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.last_rtt = srv_now - srv_start
if self.last_rtt > 500 then
log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. ")")
log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. ")")
end
-- log._debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms")
-- log._debug(log_header .. "RPLC TT = ".. (srv_now - plc_send) .. "ms")
-- log.debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms")
-- log.debug(log_header .. "RPLC TT = ".. (srv_now - plc_send) .. "ms")
else
log._debug(log_header .. "RPLC keep alive packet length mismatch")
log.debug(log_header .. "RPLC keep alive packet length mismatch")
end
elseif pkt.type == RPLC_TYPES.STATUS then
-- status packet received, update data
@ -267,11 +271,11 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.received_status_cache = true
else
-- error copying status data
log._error(log_header .. "failed to parse status packet data")
log.error(log_header .. "failed to parse status packet data")
end
end
else
log._debug(log_header .. "RPLC status packet length mismatch")
log.debug(log_header .. "RPLC status packet length mismatch")
end
elseif pkt.type == RPLC_TYPES.MEK_STRUCT then
-- received reactor structure, record it
@ -282,10 +286,10 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.received_struct = true
else
-- error copying structure data
log._error(log_header .. "failed to parse struct packet data")
log.error(log_header .. "failed to parse struct packet data")
end
else
log._debug(log_header .. "RPLC struct packet length mismatch")
log.debug(log_header .. "RPLC struct packet length mismatch")
end
elseif pkt.type == RPLC_TYPES.MEK_SCRAM then
-- SCRAM acknowledgement
@ -294,7 +298,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.acks.scram = true
self.sDB.control_state = false
elseif ack == false then
log._debug(log_header .. "SCRAM failed!")
log.debug(log_header .. "SCRAM failed!")
end
elseif pkt.type == RPLC_TYPES.MEK_ENABLE then
-- enable acknowledgement
@ -303,7 +307,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.acks.enable = true
self.sDB.control_state = true
elseif ack == false then
log._debug(log_header .. "enable failed!")
log.debug(log_header .. "enable failed!")
end
elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then
-- burn rate acknowledgement
@ -311,7 +315,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
if ack then
self.acks.burn_rate = true
elseif ack == false then
log._debug(log_header .. "burn rate update failed!")
log.debug(log_header .. "burn rate update failed!")
end
elseif pkt.type == RPLC_TYPES.ISS_STATUS then
-- ISS status packet received, copy data
@ -321,10 +325,10 @@ function new_session(id, for_reactor, in_queue, out_queue)
-- copied in ISS status data OK
else
-- error copying ISS status data
log._error(log_header .. "failed to parse ISS status packet data")
log.error(log_header .. "failed to parse ISS status packet data")
end
else
log._debug(log_header .. "RPLC ISS status packet length mismatch")
log.debug(log_header .. "RPLC ISS status packet length mismatch")
end
elseif pkt.type == RPLC_TYPES.ISS_ALARM then
-- ISS alarm
@ -337,10 +341,10 @@ function new_session(id, for_reactor, in_queue, out_queue)
-- copied in ISS status data OK
else
-- error copying ISS status data
log._error(log_header .. "failed to parse ISS alarm status data")
log.error(log_header .. "failed to parse ISS alarm status data")
end
else
log._debug(log_header .. "RPLC ISS alarm packet length mismatch")
log.debug(log_header .. "RPLC ISS alarm packet length mismatch")
end
elseif pkt.type == RPLC_TYPES.ISS_CLEAR then
-- ISS clear acknowledgement
@ -350,17 +354,17 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.sDB.iss_tripped = false
self.sDB.iss_trip_cause = "ok"
elseif ack == false then
log._debug(log_header .. "ISS clear failed")
log.debug(log_header .. "ISS clear failed")
end
else
log._debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
if pkt.type == SCADA_MGMT_TYPES.CLOSE then
-- close the session
self.connected = false
else
log._debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
@ -402,7 +406,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
self.connected = false
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
println("connection to reactor " .. self.for_reactor .. " PLC closed by server")
log._info(log_header .. "session closed by server")
log.info(log_header .. "session closed by server")
end
-- iterate the session
@ -454,7 +458,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log._warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_header .. "exceeded 100ms queue process limit")
break
end
end
@ -463,7 +467,7 @@ function new_session(id, for_reactor, in_queue, out_queue)
if not self.connected then
self.plc_conn_watchdog.cancel()
println("connection to reactor " .. self.for_reactor .. " PLC closed by remote host")
log._info(log_header .. "session closed by remote host")
log.info(log_header .. "session closed by remote host")
return self.connected
end
@ -559,3 +563,5 @@ function new_session(id, for_reactor, in_queue, out_queue)
iterate = iterate
}
end
return plc

View File

@ -0,0 +1,3 @@
local rtu = {}
return rtu

View File

@ -1,14 +1,22 @@
-- #REQUIRES mqueue.lua
-- #REQUIRES log.lua
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local coordinator = require("session.coordinator")
local plc = require("session.plc")
local rtu = require("session.rtu")
-- Supervisor Sessions Handler
SESSION_TYPE = {
local svsessions = {}
local SESSION_TYPE = {
RTU_SESSION = 0,
PLC_SESSION = 1,
COORD_SESSION = 2
}
svsessions.SESSION_TYPE = SESSION_TYPE
local self = {
modem = nil,
num_reactors = 0,
@ -20,12 +28,97 @@ local self = {
next_coord_id = 0
}
function link_modem(modem)
-- PRIVATE FUNCTIONS --
-- iterate all the given sessions
local function _iterate(sessions)
for i = 1, #sessions do
local session = sessions[i]
if session.open then
local ok = session.instance.iterate()
if ok then
-- send packets in out queue
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg.qtype == mqueue.TYPE.PACKET then
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
end
end
else
session.open = false
end
end
end
end
-- cleanly close a session
local function _shutdown(session)
session.open = false
session.instance.close()
-- send packets in out queue (namely the close packet)
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg.qtype == mqueue.TYPE.PACKET then
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
end
end
log.debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port)
end
-- close connections
local function _close(sessions)
for i = 1, #sessions do
local session = sessions[i]
if session.open then
_shutdown(session)
end
end
end
-- check if a watchdog timer event matches that of one of the provided sessions
local function _check_watchdogs(sessions, timer_event)
for i = 1, #sessions do
local session = sessions[i]
if session.open then
local triggered = session.instance.check_wd(timer_event)
if triggered then
log.debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port .. "...")
_shutdown(session)
end
end
end
end
-- delete any closed sessions
local function _free_closed(sessions)
local move_to = 1
for i = 1, #sessions do
local session = sessions[i]
if session ~= nil then
if sessions[i].open then
if sessions[move_to] == nil then
sessions[move_to] = session
sessions[i] = nil
end
move_to = move_to + 1
else
log.debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port)
sessions[i] = nil
end
end
end
end
-- PUBLIC FUNCTIONS --
svsessions.link_modem = function (modem)
self.modem = modem
end
-- find a session by the remote port
function find_session(remote_port)
svsessions.find_session = function (remote_port)
-- check RTU sessions
for i = 1, #self.rtu_sessions do
if self.rtu_sessions[i].r_port == remote_port then
@ -51,7 +144,7 @@ function find_session(remote_port)
end
-- get a session by reactor ID
function get_reactor_session(reactor)
svsessions.get_reactor_session = function (reactor)
local session = nil
for i = 1, #self.plc_sessions do
@ -64,8 +157,8 @@ function get_reactor_session(reactor)
end
-- establish a new PLC session
function establish_plc_session(local_port, remote_port, for_reactor)
if get_reactor_session(for_reactor) == nil then
svsessions.establish_plc_session = function (local_port, remote_port, for_reactor)
if svsessions.get_reactor_session(for_reactor) == nil then
local plc_s = {
open = true,
reactor = for_reactor,
@ -79,7 +172,7 @@ function establish_plc_session(local_port, remote_port, for_reactor)
plc_s.instance = plc.new_session(self.next_plc_id, for_reactor, plc_s.in_queue, plc_s.out_queue)
table.insert(self.plc_sessions, plc_s)
log._debug("established new PLC session to " .. remote_port .. " with ID " .. self.next_plc_id)
log.debug("established new PLC session to " .. remote_port .. " with ID " .. self.next_plc_id)
self.next_plc_id = self.next_plc_id + 1
@ -91,38 +184,8 @@ function establish_plc_session(local_port, remote_port, for_reactor)
end
end
-- cleanly close a session
local function _shutdown(session)
session.open = false
session.instance.close()
-- send packets in out queue (namely the close packet)
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg.qtype == mqueue.TYPE.PACKET then
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
end
end
log._debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port)
end
-- check if a watchdog timer event matches that of one of the provided sessions
local function _check_watchdogs(sessions, timer_event)
for i = 1, #sessions do
local session = sessions[i]
if session.open then
local triggered = session.instance.check_wd(timer_event)
if triggered then
log._debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port .. "...")
_shutdown(session)
end
end
end
end
-- attempt to identify which session's watchdog timer fired
function check_all_watchdogs(timer_event)
svsessions.check_all_watchdogs = function (timer_event)
-- check RTU session watchdogs
_check_watchdogs(self.rtu_sessions, timer_event)
@ -133,29 +196,8 @@ function check_all_watchdogs(timer_event)
_check_watchdogs(self.coord_sessions, timer_event)
end
-- iterate all the given sessions
local function _iterate(sessions)
for i = 1, #sessions do
local session = sessions[i]
if session.open then
local ok = session.instance.iterate()
if ok then
-- send packets in out queue
while session.out_queue.ready() do
local msg = session.out_queue.pop()
if msg.qtype == mqueue.TYPE.PACKET then
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
end
end
else
session.open = false
end
end
end
end
-- iterate all sessions
function iterate_all()
svsessions.iterate_all = function ()
-- iterate RTU sessions
_iterate(self.rtu_sessions)
@ -166,28 +208,8 @@ function iterate_all()
_iterate(self.coord_sessions)
end
-- delete any closed sessions
local function _free_closed(sessions)
local move_to = 1
for i = 1, #sessions do
local session = sessions[i]
if session ~= nil then
if sessions[i].open then
if sessions[move_to] == nil then
sessions[move_to] = session
sessions[i] = nil
end
move_to = move_to + 1
else
log._debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port)
sessions[i] = nil
end
end
end
end
-- delete all closed sessions
function free_all_closed()
svsessions.free_all_closed = function ()
-- free closed RTU sessions
_free_closed(self.rtu_sessions)
@ -198,23 +220,15 @@ function free_all_closed()
_free_closed(self.coord_sessions)
end
-- close connections
local function _close(sessions)
for i = 1, #sessions do
local session = sessions[i]
if session.open then
_shutdown(session)
end
end
end
-- close all open connections
function close_all()
svsessions.close_all = function ()
-- close sessions
_close(self.rtu_sessions)
_close(self.plc_sessions)
_close(self.coord_sessions)
-- free sessions
free_all_closed()
svsessions.free_all_closed()
end
return svsessions

View File

@ -2,23 +2,19 @@
-- Nuclear Generation Facility SCADA Supervisor
--
os.loadAPI("scada-common/log.lua")
os.loadAPI("scada-common/types.lua")
os.loadAPI("scada-common/util.lua")
os.loadAPI("scada-common/ppm.lua")
os.loadAPI("scada-common/comms.lua")
os.loadAPI("scada-common/mqueue.lua")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
os.loadAPI("config.lua")
local coordinator = require("session.coordinator")
local plc = require("session.plc")
local rtu = require("session.rtu")
local svsessions = require("session.svsessions")
os.loadAPI("session/rtu.lua")
os.loadAPI("session/plc.lua")
os.loadAPI("session/coordinator.lua")
os.loadAPI("session/svsessions.lua")
local config = require("config")
local supervisor = require("supervisor")
os.loadAPI("supervisor.lua")
local SUPERVISOR_VERSION = "alpha-v0.2.0"
local SUPERVISOR_VERSION = "alpha-v0.3.0"
local print = util.print
local println = util.println
@ -27,9 +23,9 @@ local println_ts = util.println_ts
log.init(config.LOG_PATH, config.LOG_MODE)
log._info("========================================")
log._info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
log._info("========================================")
log.info("========================================")
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
log.info("========================================")
println(">> SCADA Supervisor " .. SUPERVISOR_VERSION .. " <<")
-- mount connected devices
@ -38,12 +34,12 @@ ppm.mount_all()
local modem = ppm.get_wireless_modem()
if modem == nil then
println("boot> wireless modem not found")
log._warning("no wireless modem on startup")
log.warning("no wireless modem on startup")
return
end
-- start comms, open all channels
local superv_comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN)
local superv_comms = supervisor.comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN)
-- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15
@ -61,9 +57,9 @@ while true do
-- we only care if this is our wireless modem
if device.dev == modem then
println_ts("wireless modem disconnected!")
log._error("comms modem disconnected!")
log.error("comms modem disconnected!")
else
log._warning("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end
end
elseif event == "peripheral" then
@ -76,9 +72,9 @@ while true do
superv_comms.reconnect_modem(modem)
println_ts("wireless modem reconnected.")
log._info("comms modem reconnected.")
log.info("comms modem reconnected.")
else
log._info("wired modem reconnected.")
log.info("wired modem reconnected.")
end
end
elseif event == "timer" and param1 == loop_clock then
@ -103,12 +99,12 @@ while true do
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
println_ts("closing sessions...")
log._info("terminate requested, closing sessions...")
log.info("terminate requested, closing sessions...")
svsessions.close_all()
log._info("sessions closed")
log.info("sessions closed")
break
end
end
println_ts("exited")
log._info("exited")
log.info("exited")

View File

@ -1,7 +1,10 @@
-- #REQUIRES comms.lua
-- #REQUIRES mqueue.lua
-- #REQUIRES util.lua
-- #REQUIRES svsessions.lua
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
local svsessions = require("session.svsessions")
local supervisor = {}
local PROTOCOLS = comms.PROTOCOLS
local RPLC_TYPES = comms.RPLC_TYPES
@ -17,7 +20,7 @@ local print_ts = util.print_ts
local println_ts = util.println_ts
-- supervisory controller communications
function superv_comms(num_reactors, modem, dev_listen, coord_listen)
supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen)
local self = {
ln_seq_num = 0,
num_reactors = num_reactors,
@ -101,7 +104,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen)
pkt = coord_pkt.get()
end
else
log._debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
end
end
@ -126,7 +129,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen)
if session then
if packet.type == RPLC_TYPES.LINK_REQ then
-- new device on this port? that's a collision
log._debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision")
log.debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision")
_send_plc_linking(r_port, { RPLC_LINKING.COLLISION })
else
-- pass the packet onto the session handler
@ -140,20 +143,20 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen)
local plc_id = svsessions.establish_plc_session(l_port, r_port, packet.data[1])
if plc_id == false then
-- reactor already has a PLC assigned
log._debug("PLC_LNK: assignment collision with reactor " .. packet.data[1])
log.debug("PLC_LNK: assignment collision with reactor " .. packet.data[1])
_send_plc_linking(r_port, { RPLC_LINKING.COLLISION })
else
-- got an ID; assigned to a reactor successfully
println("connected to reactor " .. packet.data[1] .. " PLC (port " .. r_port .. ")")
log._debug("PLC_LNK: allowed for device at " .. r_port)
log.debug("PLC_LNK: allowed for device at " .. r_port)
_send_plc_linking(r_port, { RPLC_LINKING.ALLOW })
end
else
log._debug("PLC_LNK: new linking packet length mismatch")
log.debug("PLC_LNK: new linking packet length mismatch")
end
else
-- force a re-link
log._debug("PLC_LNK: no session but not a link, force relink")
log.debug("PLC_LNK: no session but not a link, force relink")
_send_plc_linking(r_port, { RPLC_LINKING.DENY })
end
end
@ -164,7 +167,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen)
session.in_queue.push_packet(packet)
end
else
log._debug("illegal packet type " .. protocol .. " on device listening channel")
log.debug("illegal packet type " .. protocol .. " on device listening channel")
end
-- coordinator listening channel
elseif l_port == self.coord_listen then
@ -173,10 +176,10 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen)
elseif protocol == PROTOCOLS.COORD_DATA then
-- coordinator packet
else
log._debug("illegal packet type " .. protocol .. " on coordinator listening channel")
log.debug("illegal packet type " .. protocol .. " on coordinator listening channel")
end
else
log._error("received packet on unused channel " .. l_port, true)
log.error("received packet on unused channel " .. l_port, true)
end
end
end
@ -187,3 +190,5 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen)
handle_packet = handle_packet
}
end
return supervisor