#118 coordinator code cleanup

This commit is contained in:
Mikayla Fischler 2023-02-22 23:09:47 -05:00
parent 79494f0587
commit 4340518ecf
18 changed files with 484 additions and 435 deletions

View File

@ -4,13 +4,17 @@ local apisessions = {}
function apisessions.handle_packet(packet)
end
function apisessions.check_all_watchdogs()
end
function apisessions.close_all()
-- attempt to identify which session's watchdog timer fired
---@param timer_event number
function apisessions.check_all_watchdogs(timer_event)
end
-- delete all closed sessions
function apisessions.free_all_closed()
end
-- close all open connections
function apisessions.close_all()
end
return apisessions

View File

@ -25,6 +25,7 @@ local FAC_COMMAND = comms.FAC_COMMAND
local coordinator = {}
-- request the user to select a monitor
---@nodiscard
---@param names table available monitors
---@return boolean|string|nil
local function ask_monitor(names)
@ -64,9 +65,11 @@ function coordinator.configure_monitors(num_units)
end
-- we need a certain number of monitors (1 per unit + 1 primary display)
if #names < num_units + 1 then
println("not enough monitors connected (need " .. num_units + 1 .. ")")
log.warning("insufficient monitors present (need " .. num_units + 1 .. ")")
local num_displays_needed = num_units + 1
if #names < num_displays_needed then
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
println(message)
log.warning(message)
return false
end
@ -125,7 +128,6 @@ function coordinator.configure_monitors(num_units)
else
-- make sure all displays are connected
for i = 1, num_units do
---@diagnostic disable-next-line: need-check-nil
local display = unit_displays[i]
if not util.table_contains(names, display) then
@ -183,14 +185,19 @@ function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
-- log a message for communications connecting, providing access to progress indication control functions
---@nodiscard
---@param message string
---@return function update, function done
function coordinator.log_comms_connecting(message)
---@diagnostic disable-next-line: return-type-mismatch
return log_dmesg(message, "COMMS", true)
local update, done = log_dmesg(message, "COMMS", true)
---@cast update function
---@cast done function
return update, done
end
-- coordinator communications
---@nodiscard
---@param version string coordinator version
---@param modem table modem device
---@param sv_port integer port of configured supervisor
@ -203,23 +210,19 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
sv_linked = false,
sv_seq_num = 0,
sv_r_seq_num = nil,
modem = modem,
connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW
}
---@class coord_comms
local public = {}
comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS --
-- configure modem channels
local function _conf_channels()
self.modem.closeAll()
self.modem.open(sv_listen)
self.modem.open(api_listen)
modem.closeAll()
modem.open(sv_listen)
modem.open(api_listen)
end
_conf_channels()
@ -242,7 +245,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
pkt.make(msg_type, msg)
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
self.sv_seq_num = self.sv_seq_num + 1
end
@ -259,11 +262,13 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- PUBLIC FUNCTIONS --
---@class coord_comms
local public = {}
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_modem(modem)
self.modem = modem
---@param new_modem table
function public.reconnect_modem(new_modem)
modem = new_modem
_conf_channels()
end
@ -275,6 +280,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end
-- attempt to connect to the subervisor
---@nodiscard
---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg
@ -400,7 +406,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if l_port == api_listen then
if protocol == PROTOCOL.COORD_API then
---@diagnostic disable-next-line: param-type-mismatch
---@cast packet capi_frame
apisessions.handle_packet(packet)
else
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
@ -421,6 +427,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- handle packet
if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
if self.sv_linked then
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
if packet.length == 2 then
@ -432,7 +439,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {})
else
log.error("received invalid INITIAL_BUILDS packet")
log.debug("received invalid INITIAL_BUILDS packet")
end
else
log.debug("INITIAL_BUILDS packet length mismatch")
@ -444,7 +451,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {})
else
log.error("received invalid FAC_BUILDS packet")
log.debug("received invalid FAC_BUILDS packet")
end
else
log.debug("FAC_BUILDS packet length mismatch")
@ -452,7 +459,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then
-- update facility status
if not iocontrol.update_facility_status(packet.data) then
log.error("received invalid FAC_STATUS packet")
log.debug("received invalid FAC_STATUS packet")
end
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
@ -485,7 +492,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
-- acknowledge receipt of builds
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {})
else
log.error("received invalid UNIT_BUILDS packet")
log.debug("received invalid UNIT_BUILDS packet")
end
else
log.debug("UNIT_BUILDS packet length mismatch")
@ -518,7 +525,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
elseif cmd == UNIT_COMMAND.SET_GROUP then
---@todo how is this going to be handled?
-- UI will be updated to display current group if changed successfully
else
log.debug(util.c("received unit command ack with unknown command ", cmd))
end
@ -535,6 +542,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
log.debug("discarding SCADA_CRDN packet before linked")
end
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
if packet.length == 2 then
@ -562,10 +570,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
self.sv_linked = true
else
log.error("invalid supervisor configuration definitions received, establish failed")
log.debug("invalid supervisor configuration definitions received, establish failed")
end
else
log.error("invalid supervisor configuration table received, establish failed")
log.debug("invalid supervisor configuration table received, establish failed")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
@ -577,11 +585,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then
log.debug("supervisor connection denied")
log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then
log.debug("supervisor connection denied due to collision")
log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then
@ -619,9 +627,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
sv_watchdog.cancel()
self.sv_linked = false
println_ts("server connection closed by remote host")
log.warning("server connection closed by remote host")
log.info("server connection closed by remote host")
else
log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end
else
log.debug("discarding non-link SCADA_MGMT packet before linked")
@ -636,6 +644,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end
-- check if the coordinator is still linked to the supervisor
---@nodiscard
function public.is_linked() return self.sv_linked end
return public

View File

@ -1,3 +1,7 @@
--
-- I/O Control for Supervisor/Coordinator Integration
--
local log = require("scada-common.log")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
@ -16,7 +20,6 @@ local io = {}
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
---@param comms coord_comms comms reference
---@diagnostic disable-next-line: redefined-local
function iocontrol.init(conf, comms)
---@class ioctl_facility
io.facility = {
@ -41,11 +44,11 @@ function iocontrol.init(conf, comms)
radiation = types.new_zero_radiation_reading(),
save_cfg_ack = function (success) end, ---@param success boolean
start_ack = function (success) end, ---@param success boolean
stop_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, ---@param success boolean
ack_alarms_ack = function (success) end, ---@param success boolean
save_cfg_ack = function (success) end, ---@param success boolean
start_ack = function (success) end, ---@param success boolean
stop_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, ---@param success boolean
ack_alarms_ack = function (success) end, ---@param success boolean
ps = psil.create(),
@ -56,7 +59,7 @@ function iocontrol.init(conf, comms)
env_d_data = {}
}
-- create induction tables (max 1 per unit, preferably 1 total)
-- create induction tables (currently only 1 is supported)
for _ = 1, conf.num_units do
local data = {} ---@type imatrix_session_db
table.insert(io.facility.induction_ps_tbl, psil.create())
@ -170,6 +173,8 @@ end
---@param build table
---@return boolean valid
function iocontrol.record_facility_builds(build)
local valid = true
if type(build) == "table" then
local fac = io.facility
@ -187,96 +192,103 @@ function iocontrol.record_facility_builds(build)
end
else
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
valid = false
end
end
end
else
log.error("facility builds not a table")
return false
log.debug("facility builds not a table")
valid = false
end
return true
return valid
end
-- populate unit structure builds
---@param builds table
---@return boolean valid
function iocontrol.record_unit_builds(builds)
local valid = true
-- note: if not all units and RTUs are connected, some will be nil
for id, build in pairs(builds) do
local unit = io.units[id] ---@type ioctl_unit
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
if type(build) ~= "table" then
log.error(util.c("corrupted unit builds provided, unit ", id, " not a table"))
return false
log.debug(log_header .. "build not a table")
valid = false
elseif type(unit) ~= "table" then
log.error(util.c("corrupted unit builds provided, invalid unit ", id))
return false
end
log.debug(log_header .. "invalid unit id")
valid = false
else
-- reactor build
if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.unit_ps.publish(key, val)
end
local log_header = util.c("iocontrol.record_unit_builds[unit ", id, "]: ")
-- reactor build
if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.unit_ps.publish(key, val)
end
if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and
(type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then
unit.unit_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width })
end
end
-- boiler builds
if type(build.boilers) == "table" then
for b_id, boiler in pairs(build.boilers) do
if type(unit.boiler_data_tbl[b_id]) == "table" then
unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
unit.boiler_ps_tbl[b_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", b_id))
if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and
(type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then
unit.unit_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width })
end
end
end
-- turbine builds
if type(build.turbines) == "table" then
for t_id, turbine in pairs(build.turbines) do
if type(unit.turbine_data_tbl[t_id]) == "table" then
unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
-- boiler builds
if type(build.boilers) == "table" then
for b_id, boiler in pairs(build.boilers) do
if type(unit.boiler_data_tbl[b_id]) == "table" then
unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
unit.turbine_ps_tbl[t_id].publish(key, val)
for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
unit.boiler_ps_tbl[b_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", b_id))
valid = false
end
end
end
-- turbine builds
if type(build.turbines) == "table" then
for t_id, turbine in pairs(build.turbines) do
if type(unit.turbine_data_tbl[t_id]) == "table" then
unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
unit.turbine_ps_tbl[t_id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid turbine id ", t_id))
valid = false
end
else
log.debug(util.c(log_header, "invalid turbine id ", t_id))
end
end
end
end
return true
return valid
end
-- update facility status
---@param status table
---@return boolean valid
function iocontrol.update_facility_status(status)
local valid = true
local log_header = util.c("iocontrol.update_facility_status: ")
if type(status) ~= "table" then
log.debug(log_header .. "status not a table")
return false
log.debug(util.c(log_header, "status not a table"))
valid = false
else
local fac = io.facility
@ -284,10 +296,17 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1]
if type(ctl_status) == "table" and (#ctl_status == 14) then
if type(ctl_status) == "table" and #ctl_status == 14 then
fac.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2]
fac.auto_active = ctl_status[3] > 0
if type(ctl_status[3]) == "number" then
fac.auto_active = ctl_status[3] > 1
else
fac.auto_active = false
valid = false
end
fac.auto_ramping = ctl_status[4]
fac.auto_saturated = ctl_status[5]
@ -327,6 +346,7 @@ function iocontrol.update_facility_status(status)
end
else
log.debug(log_header .. "control status not a table or length mismatch")
valid = false
end
-- RTU statuses
@ -334,10 +354,10 @@ function iocontrol.update_facility_status(status)
local rtu_statuses = status[2]
fac.rtu_count = 0
if type(rtu_statuses) == "table" then
-- connected RTU count
fac.rtu_count = rtu_statuses.count
fac.ps.publish("rtu_count", fac.rtu_count)
-- power statistics
if type(rtu_statuses.power) == "table" then
@ -346,6 +366,7 @@ function iocontrol.update_facility_status(status)
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
else
log.debug(log_header .. "power statistics list not a table")
valid = false
end
-- induction matricies statuses
@ -371,16 +392,16 @@ function iocontrol.update_facility_status(status)
if data.formed then
if rtu_faulted then
fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.tanks.energy_fill >= 0.99 then
fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
else
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
end
else
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed
fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(fac.induction_data_tbl[id].state) do
@ -396,6 +417,7 @@ function iocontrol.update_facility_status(status)
end
else
log.debug(log_header .. "induction matrix list not a table")
valid = false
end
-- environment detector status
@ -413,313 +435,324 @@ function iocontrol.update_facility_status(status)
end
else
log.debug(log_header .. "radiation monitor list not a table")
return false
valid = false
end
else
log.debug(log_header .. "rtu statuses not a table")
valid = false
end
fac.ps.publish("rtu_count", fac.rtu_count)
end
return true
return valid
end
-- update unit statuses
---@param statuses table
---@return boolean valid
function iocontrol.update_unit_statuses(statuses)
local valid = true
if type(statuses) ~= "table" then
log.debug("iocontrol.update_unit_statuses: unit statuses not a table")
return false
valid = false
elseif #statuses ~= #io.units then
log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units")
return false
valid = false
else
local burn_rate_sum = 0.0
-- get all unit statuses
for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i]
if type(status) ~= "table" or #status ~= 5 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
return false
end
-- reactor PLC status
local reactor_status = status[1]
if type(reactor_status) ~= "table" then
reactor_status = {}
log.debug(log_header .. "reactor status not a table")
end
if #reactor_status == 0 then
unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
local gen_status = reactor_status[3]
if #gen_status == 6 then
unit.reactor_data.last_status_update = gen_status[1]
unit.reactor_data.control_state = gen_status[2]
unit.reactor_data.rps_tripped = gen_status[3]
unit.reactor_data.rps_trip_cause = gen_status[4]
unit.reactor_data.no_reactor = gen_status[5]
unit.reactor_data.formed = gen_status[6]
else
log.debug(log_header .. "reactor general status length mismatch")
end
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
-- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
end
if unit.reactor_data.mek_status.status then
unit.unit_ps.publish("computed_status", 5) -- running
else
if unit.reactor_data.no_reactor then
unit.unit_ps.publish("computed_status", 3) -- faulted
elseif not unit.reactor_data.formed then
unit.unit_ps.publish("computed_status", 2) -- multiblock not formed
elseif unit.reactor_data.rps_status.force_dis then
unit.unit_ps.publish("computed_status", 7) -- reactor force disabled
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.unit_ps.publish("computed_status", 6) -- SCRAM
else
unit.unit_ps.publish("computed_status", 4) -- disabled
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
valid = false
else
log.debug(log_header .. "reactor status length mismatch")
end
-- reactor PLC status
local reactor_status = status[1]
-- RTU statuses
if type(reactor_status) ~= "table" then
reactor_status = {}
log.debug(log_header .. "reactor status not a table")
end
local rtu_statuses = status[2]
if #reactor_status == 0 then
unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
local rps_status = reactor_status[2]
local gen_status = reactor_status[3]
if type(rtu_statuses) == "table" then
-- boiler statuses
if type(rtu_statuses.boilers) == "table" then
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[i] == nil then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
if #gen_status == 6 then
unit.reactor_data.last_status_update = gen_status[1]
unit.reactor_data.control_state = gen_status[2]
unit.reactor_data.rps_tripped = gen_status[3]
unit.reactor_data.rps_trip_cause = gen_status[4]
unit.reactor_data.no_reactor = gen_status[5]
unit.reactor_data.formed = gen_status[6]
else
log.debug(log_header .. "reactor general status length mismatch")
end
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
-- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
end
if unit.reactor_data.mek_status.status then
unit.unit_ps.publish("computed_status", 5) -- running
else
if unit.reactor_data.no_reactor then
unit.unit_ps.publish("computed_status", 3) -- faulted
elseif not unit.reactor_data.formed then
unit.unit_ps.publish("computed_status", 2) -- multiblock not formed
elseif unit.reactor_data.rps_status.force_dis then
unit.unit_ps.publish("computed_status", 7) -- reactor force disabled
elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
unit.unit_ps.publish("computed_status", 6) -- SCRAM
else
unit.unit_ps.publish("computed_status", 4) -- disabled
end
end
for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then
local rtu_faulted = boiler[1] ---@type boolean
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean
unit.boiler_data_tbl[id].state = boiler[3] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
unit.boiler_ps_tbl[id].publish("formed", data.formed)
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
else
log.debug(log_header .. "reactor status length mismatch")
valid = false
end
if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.formed then
if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
-- RTU statuses
local rtu_statuses = status[2]
if type(rtu_statuses) == "table" then
-- boiler statuses
if type(rtu_statuses.boilers) == "table" then
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[i] == nil then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
end
for id, boiler in pairs(rtu_statuses.boilers) do
if type(unit.boiler_data_tbl[id]) == "table" then
local rtu_faulted = boiler[1] ---@type boolean
unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean
unit.boiler_data_tbl[id].state = boiler[3] ---@type table
unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
unit.boiler_ps_tbl[id].publish("formed", data.formed)
unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then
unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.formed then
if data.state.boil_rate > 0 then
unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
end
else
unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end
else
unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
log.debug(util.c(log_header, "invalid boiler id ", id))
valid = false
end
for key, val in pairs(unit.boiler_data_tbl[id].state) do
unit.boiler_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
unit.boiler_ps_tbl[id].publish(key, val)
end
else
log.debug(util.c(log_header, "invalid boiler id ", id))
end
end
else
log.debug(log_header .. "boiler list not a table")
end
-- turbine statuses
if type(rtu_statuses.turbines) == "table" then
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
else
log.debug(log_header .. "boiler list not a table")
valid = false
end
for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then
local rtu_faulted = turbine[1] ---@type boolean
unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
unit.turbine_data_tbl[id].state = turbine[3] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
-- turbine statuses
if type(rtu_statuses.turbines) == "table" then
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[i] == nil then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
end
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
for id, turbine in pairs(rtu_statuses.turbines) do
if type(unit.turbine_data_tbl[id]) == "table" then
local rtu_faulted = turbine[1] ---@type boolean
unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
unit.turbine_data_tbl[id].state = turbine[3] ---@type table
unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
unit.turbine_ps_tbl[id].publish("formed", data.formed)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
if rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.energy_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
unit.turbine_ps_tbl[id].publish("formed", data.formed)
unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
if rtu_faulted then
unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.formed then
if data.tanks.energy_fill >= 0.99 then
unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
elseif data.state.flow_rate < 100 then
unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
end
else
unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end
else
unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
log.debug(util.c(log_header, "invalid turbine id ", id))
valid = false
end
end
else
log.debug(log_header .. "turbine list not a table")
valid = false
end
for key, val in pairs(unit.turbine_data_tbl[id].state) do
unit.turbine_ps_tbl[id].publish(key, val)
end
-- environment detector status
if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then
local rad_mon = rtu_statuses.rad_mon[1]
local rtu_faulted = rad_mon[1] ---@type boolean
unit.radiation = rad_mon[2] ---@type number
for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
unit.turbine_ps_tbl[id].publish(key, val)
end
unit.unit_ps.publish("radiation", unit.radiation)
else
log.debug(util.c(log_header, "invalid turbine id ", id))
unit.radiation = types.new_zero_radiation_reading()
end
else
log.debug(log_header .. "radiation monitor list not a table")
valid = false
end
else
log.debug(log_header .. "rtu list not a table")
valid = false
end
-- annunciator
unit.annunciator = status[3]
if type(unit.annunciator) ~= "table" then
unit.annunciator = {}
log.debug(log_header .. "annunciator state not a table")
valid = false
end
for key, val in pairs(unit.annunciator) do
if key == "TurbineTrip" then
-- split up turbine trip table for all turbines and a general OR combination
local trips = val
local any = false
for id = 1, #trips do
any = any or trips[id]
unit.turbine_ps_tbl[id].publish(key, trips[id])
end
unit.unit_ps.publish("TurbineTrip", any)
elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
for id = 1, #val do
unit.boiler_ps_tbl[id].publish(key, val[id])
end
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
-- split up array for all turbines
for id = 1, #val do
unit.turbine_ps_tbl[id].publish(key, val[id])
end
elseif type(val) == "table" then
-- we missed one of the tables?
log.debug(log_header .. "unrecognized table found in annunciator list, this is a bug")
valid = false
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
-- alarms
local alarm_states = status[4]
if type(alarm_states) == "table" then
for id = 1, #alarm_states do
local state = alarm_states[id]
unit.alarms[id] = state
if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then
unit.unit_ps.publish("Alarm_" .. id, 2)
elseif state == types.ALARM_STATE.RING_BACK then
unit.unit_ps.publish("Alarm_" .. id, 3)
else
unit.unit_ps.publish("Alarm_" .. id, 1)
end
end
else
log.debug(log_header .. "turbine list not a table")
return false
log.debug(log_header .. "alarm states not a table")
valid = false
end
-- environment detector status
if type(rtu_statuses.rad_mon) == "table" then
if #rtu_statuses.rad_mon > 0 then
local rad_mon = rtu_statuses.rad_mon[1]
local rtu_faulted = rad_mon[1] ---@type boolean
unit.radiation = rad_mon[2] ---@type number
-- unit state fields
local unit_state = status[5]
unit.unit_ps.publish("radiation", unit.radiation)
if type(unit_state) == "table" then
if #unit_state == 5 then
unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2])
unit.unit_ps.publish("U_WasteMode", unit_state[3])
unit.unit_ps.publish("U_AutoReady", unit_state[4])
unit.unit_ps.publish("U_AutoDegraded", unit_state[5])
else
unit.radiation = types.new_zero_radiation_reading()
log.debug(log_header .. "unit state length mismatch")
valid = false
end
else
log.debug(log_header .. "radiation monitor list not a table")
return false
log.debug(log_header .. "unit state not a table")
valid = false
end
else
log.debug(log_header .. "rtu list not a table")
end
-- annunciator
unit.annunciator = status[3]
if type(unit.annunciator) ~= "table" then
unit.annunciator = {}
log.debug(log_header .. "annunciator state not a table")
end
for key, val in pairs(unit.annunciator) do
if key == "TurbineTrip" then
-- split up turbine trip table for all turbines and a general OR combination
local trips = val
local any = false
for id = 1, #trips do
any = any or trips[id]
unit.turbine_ps_tbl[id].publish(key, trips[id])
end
unit.unit_ps.publish("TurbineTrip", any)
elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
for id = 1, #val do
unit.boiler_ps_tbl[id].publish(key, val[id])
end
elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
-- split up array for all turbines
for id = 1, #val do
unit.turbine_ps_tbl[id].publish(key, val[id])
end
elseif type(val) == "table" then
-- we missed one of the tables?
log.error(log_header .. "unrecognized table found in annunciator list, this is a bug", true)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
-- alarms
local alarm_states = status[4]
if type(alarm_states) == "table" then
for id = 1, #alarm_states do
local state = alarm_states[id]
unit.alarms[id] = state
if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then
unit.unit_ps.publish("Alarm_" .. id, 2)
elseif state == types.ALARM_STATE.RING_BACK then
unit.unit_ps.publish("Alarm_" .. id, 3)
else
unit.unit_ps.publish("Alarm_" .. id, 1)
end
end
else
log.debug(log_header .. "alarm states not a table")
end
-- unit state fields
local unit_state = status[5]
if type(unit_state) == "table" then
if #unit_state == 5 then
unit.unit_ps.publish("U_StatusLine1", unit_state[1])
unit.unit_ps.publish("U_StatusLine2", unit_state[2])
unit.unit_ps.publish("U_WasteMode", unit_state[3])
unit.unit_ps.publish("U_AutoReady", unit_state[4])
unit.unit_ps.publish("U_AutoDegraded", unit_state[5])
else
log.debug(log_header .. "unit state length mismatch")
end
else
log.debug(log_header .. "unit state not a table")
end
end
@ -729,7 +762,7 @@ function iocontrol.update_unit_statuses(statuses)
sounder.eval(io.units)
end
return true
return valid
end
-- get the IO controller database

View File

@ -1,3 +1,6 @@
--
-- Process Control Management
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
@ -30,11 +33,11 @@ local self = {
--------------------------
-- initialize the process controller
---@param iocontrol ioctl
---@diagnostic disable-next-line: redefined-local
function process.init(iocontrol, comms)
---@param iocontrol ioctl iocontrl system
---@param coord_comms coord_comms coordinator communications
function process.init(iocontrol, coord_comms)
self.io = iocontrol
self.comms = comms
self.comms = coord_comms
for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1
@ -91,13 +94,13 @@ end
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
log.debug("FAC: SCRAM ALL")
log.debug("PROCESS: FAC SCRAM ALL")
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
log.debug("FAC: ACK ALL ALARMS")
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
-- start reactor
@ -105,7 +108,7 @@ end
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMAND.START, id)
log.debug(util.c("UNIT[", id, "]: START"))
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
-- SCRAM reactor
@ -113,14 +116,14 @@ end
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
log.debug(util.c("UNIT[", id, "]: SCRAM"))
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
log.debug(util.c("UNIT[", id, "]: RESET RPS"))
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
-- set burn rate
@ -128,7 +131,7 @@ end
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate))
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- set waste mode
@ -139,13 +142,11 @@ function process.set_waste(id, mode)
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode))
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
if type(waste_mode) ~= "table" then
waste_mode = {}
end
if type(waste_mode) ~= "table" then waste_mode = {} end
waste_mode[id] = mode
@ -160,7 +161,7 @@ end
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS"))
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
@ -168,7 +169,7 @@ end
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm))
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
-- reset an alarm
@ -176,7 +177,7 @@ end
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm))
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
-- assign a unit to a group
@ -184,13 +185,11 @@ end
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id))
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
if type(prio_groups) ~= "table" then
prio_groups = {}
end
if type(prio_groups) ~= "table" then prio_groups = {} end
prio_groups[unit_id] = group_id
@ -208,13 +207,13 @@ end
-- stop automatic process control
function process.stop_auto()
self.comms.send_fac_command(FAC_COMMAND.STOP)
log.debug("FAC: STOP AUTO")
log.debug("PROCESS: STOP AUTO CTL")
end
-- start automatic process control
function process.start_auto()
self.comms.send_auto_start(self.config)
log.debug("FAC: START AUTO")
log.debug("PROCESS: START AUTO CTL")
end
-- save process control settings
@ -246,8 +245,6 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.warning("process.save(): failed to save coordinator settings file")
end
log.debug("saved = " .. util.strval(saved))
self.io.facility.save_cfg_ack(saved)
end
@ -273,18 +270,4 @@ function process.start_ack_handle(response)
self.io.facility.start_ack(ack)
end
--------------------------
-- SUPERVISOR RESPONSES --
--------------------------
-- acknowledgement from the supervisor to assign a unit to a group
function process.sv_assign(unit_id, group_id)
self.io.units[unit_id].group = group_id
end
-- acknowledgement from the supervisor to assign a unit a burn rate limit
function process.sv_limit(unit_id, limit)
self.io.units[unit_id].limit = limit
end
return process

View File

@ -1,3 +1,7 @@
--
-- Graphics Rendering Control
--
local log = require("scada-common.log")
local util = require("scada-common.util")
@ -56,6 +60,7 @@ function renderer.set_displays(monitors)
end
-- check if the renderer is configured to use a given monitor peripheral
---@nodiscard
---@param periph table peripheral
---@return boolean is_used
function renderer.is_monitor_used(periph)
@ -87,6 +92,7 @@ function renderer.reset(recolor)
end
-- check main display width
---@nodiscard
---@return boolean width_okay
function renderer.validate_main_display_width()
local w, _ = engine.monitors.primary.getSize()
@ -94,6 +100,7 @@ function renderer.validate_main_display_width()
end
-- check display sizes
---@nodiscard
---@return boolean valid all unit display dimensions OK
function renderer.validate_unit_display_sizes()
local valid = true
@ -101,7 +108,7 @@ function renderer.validate_unit_display_sizes()
for id, monitor in pairs(engine.monitors.unit_displays) do
local w, h = monitor.getSize()
if w ~= 79 or h ~= 52 then
log.warning(util.c("unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
valid = false
end
end
@ -171,6 +178,7 @@ function renderer.close_ui()
end
-- is the UI ready?
---@nodiscard
---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end

View File

@ -14,7 +14,7 @@ local sounder = {}
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
local _DRATE = 48000 -- 48kHz audio
local _MAX_VAL = 127/2 -- max signed integer in this 8-bit audio
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
local _MAX_SAMPLES = 0x20000 -- 128 * 1024 samples
local _05s_SAMPLES = 24000 -- half a second worth of samples
@ -26,7 +26,8 @@ local alarm_ctl = {
playing = false,
num_active = 0,
next_block = 1,
quad_buffer = { {}, {}, {}, {} } -- split audio up into 0.5s samples so specific components can be ended quicker
-- split audio up into 0.5s samples so specific components can be ended quicker
quad_buffer = { {}, {}, {}, {} }
}
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
@ -52,6 +53,7 @@ local TONES = {
}
-- calculate how many samples are in the given number of milliseconds
---@nodiscard
---@param ms integer milliseconds
---@return integer samples
local function ms_to_samples(ms) return math.floor(ms * 48) end
@ -224,6 +226,7 @@ end
--#endregion
-- hard audio limiter
---@nodiscard
---@param output number output level
---@return number limited -128.0 to 127.0
local function limit(output)
@ -454,7 +457,7 @@ function sounder.test_power_scale()
end
end
log.debug("power rescale test took " .. (util.time_ms() - start) .. "ms")
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
end
--#endregion

View File

@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local COORDINATOR_VERSION = "beta-v0.10.1"
local COORDINATOR_VERSION = "v0.11.0"
local print = util.print
local println = util.println
@ -81,7 +81,7 @@ local function main()
-- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured or monitors == nil then
println("boot> monitor setup failed")
println("startup> monitor setup failed")
log.fatal("monitor configuration failed")
return
end
@ -91,11 +91,11 @@ local function main()
renderer.reset(config.RECOLOR)
if not renderer.validate_main_display_width() then
println("boot> main display must be 8 blocks wide")
println("startup> main display must be 8 blocks wide")
log.fatal("main display not wide enough")
return
elseif not renderer.validate_unit_display_sizes() then
println("boot> one or more unit display dimensions incorrect; they must be 4x4 blocks")
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
log.fatal("unit display dimensions incorrect")
return
end
@ -116,7 +116,7 @@ local function main()
local speaker = ppm.get_device("speaker")
if speaker == nil then
log_boot("annunciator alarm speaker not found")
println("boot> speaker not found")
println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found")
return
else
@ -135,7 +135,7 @@ local function main()
local modem = ppm.get_wireless_modem()
if modem == nil then
log_comms("wireless modem not found")
println("boot> wireless modem not found")
println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
else
@ -145,12 +145,12 @@ local function main()
-- create connection watchdog
local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
conn_watchdog.cancel()
log.debug("boot> conn watchdog created")
log.debug("startup> conn watchdog created")
-- start comms, open all channels
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN,
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
log.debug("boot> comms init")
log.debug("startup> comms init")
log_comms("comms initialized")
-- base loop clock (2Hz, 10 ticks)
@ -176,7 +176,7 @@ local function main()
end
if not init_connect_sv() then
println("boot> failed to connect to supervisor")
println("startup> failed to connect to supervisor")
log_sys("system shutdown")
return
else
@ -199,7 +199,7 @@ local function main()
renderer.close_ui()
log_graphics(util.c("UI crashed: ", message))
println_ts("UI crashed")
log.fatal(util.c("ui crashed with error ", message))
log.fatal(util.c("GUI crashed with error ", message))
else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
@ -223,7 +223,7 @@ local function main()
if ui_ok then
-- start connection watchdog
conn_watchdog.feed()
log.debug("boot> conn watchdog started")
log.debug("startup> conn watchdog started")
log_sys("system started successfully")
end
@ -243,7 +243,6 @@ local function main()
no_modem = true
log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
log.error("comms modem disconnected!")
-- close out UI
renderer.close_ui()
@ -252,20 +251,21 @@ local function main()
log_sys("awaiting comms modem reconnect...")
else
log_sys("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end
elseif type == "monitor" then
if renderer.is_monitor_used(device) then
-- "halt and catch fire" style handling
println_ts("lost a configured monitor, system will now exit")
log_sys("lost a configured monitor, system will now exit")
local msg = "lost a configured monitor, system will now exit"
println_ts(msg)
log_sys(msg)
break
else
log_sys("lost unused monitor, ignoring")
end
elseif type == "speaker" then
println_ts("lost alarm sounder speaker")
log_sys("lost alarm sounder speaker")
local msg = "lost alarm sounder speaker"
println_ts(msg)
log_sys(msg)
end
end
elseif event == "peripheral" then
@ -291,8 +291,9 @@ local function main()
elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors
elseif type == "speaker" then
println_ts("alarm sounder speaker reconnected")
log_sys("alarm sounder speaker reconnected")
local msg = "alarm sounder speaker reconnected"
println_ts(msg)
log_sys(msg)
sounder.reconnect(device)
end
end
@ -301,7 +302,7 @@ local function main()
-- main loop tick
-- free any closed sessions
--apisessions.free_all_closed()
apisessions.free_all_closed()
-- update date and time string for main display
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
@ -326,7 +327,7 @@ local function main()
-- a non-clock/main watchdog timer event
--check API watchdogs
--apisessions.check_all_watchdogs(param1)
apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcallbackdsp.handle(param1)

View File

@ -13,6 +13,7 @@ local cpair = core.graphics.cpair
local border = core.graphics.border
-- new boiler view
---@nodiscard
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y

View File

@ -19,6 +19,7 @@ local border = core.graphics.border
local TEXT_ALIGN = core.graphics.TEXT_ALIGN
-- new induction matrix view
---@nodiscard
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y

View File

@ -16,11 +16,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local VerticalBar = require("graphics.elements.indicators.vbar")
local HazardButton = require("graphics.elements.controls.hazard_button")
local MultiButton = require("graphics.elements.controls.multi_button")
local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
@ -32,6 +29,7 @@ local border = core.graphics.border
local period = core.flasher.PERIOD
-- new process control view
---@nodiscard
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y

View File

@ -1,3 +1,5 @@
local types = require("scada-common.types")
local style = require("coordinator.ui.style")
local core = require("graphics.core")
@ -13,6 +15,7 @@ local cpair = core.graphics.cpair
local border = core.graphics.border
-- create new reactor view
---@nodiscard
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y
@ -47,7 +50,7 @@ local function new_view(root, x, y, data, ps)
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
ps.subscribe("ccool_type", function (type)
if type == "mekanism:sodium" then
if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
ccool.recolor(cpair(colors.blue, colors.gray))
@ -55,7 +58,7 @@ local function new_view(root, x, y, data, ps)
end)
ps.subscribe("hcool_type", function (type)
if type == "mekanism:superheated_sodium" then
if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray))
else
hcool.recolor(cpair(colors.white, colors.gray))

View File

@ -15,6 +15,7 @@ local cpair = core.graphics.cpair
local border = core.graphics.border
-- new turbine view
---@nodiscard
---@param root graphics_element parent
---@param x integer top left x
---@param y integer top left y

View File

@ -57,6 +57,7 @@ local waste_opts = {
}
-- create a unit view
---@nodiscard
---@param parent graphics_element parent
---@param id integer
local function init(parent, id)
@ -237,13 +238,13 @@ local function init(parent, id)
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=2,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=13,x=29,y=9}
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.yellow}
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.yellow}
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
u_ps.subscribe("RCSFault", c_flt.update)
u_ps.subscribe("EmergencyCoolant", c_emg.update)

View File

@ -19,6 +19,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local pipe = core.graphics.pipe
-- make a new unit overview window
---@nodiscard
---@param parent graphics_element parent
---@param x integer top left x
---@param y integer top left y
@ -51,7 +52,7 @@ local function make(parent, x, y, unit)
-- REACTOR --
-------------
reactor_view(root, 1, 3, unit.reactor_data, unit.unit_ps)
local _ = reactor_view(root, 1, 3, unit.reactor_data, unit.unit_ps)
if num_boilers > 0 then
local coolant_pipes = {}
@ -101,16 +102,16 @@ local function make(parent, x, y, unit)
local steam_pipes_b = {}
if no_boilers then
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
if num_turbines >= 2 then
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
end
if num_turbines >= 3 then
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
end
else

View File

@ -1,5 +1,5 @@
--
-- Reactor Unit SCADA Coordinator GUI
-- Reactor Unit Waiting Spinner
--
local style = require("coordinator.ui.style")
@ -16,6 +16,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create a unit waiting view
---@nodiscard
---@param parent graphics_element parent
---@param y integer y offset
local function init(parent, y)

View File

@ -3,13 +3,11 @@ local completion = require("cc.completion")
local util = require("scada-common.util")
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local dialog = {}
-- ask the user yes or no
---@nodiscard
---@param question string
---@param default boolean
---@return boolean|nil
@ -36,6 +34,7 @@ function dialog.ask_y_n(question, default)
end
-- ask the user for an input within a set of options
---@nodiscard
---@param options table
---@param cancel string
---@return boolean|string|nil

View File

@ -30,6 +30,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN
local cpair = core.graphics.cpair
-- create new main view
---@nodiscard
---@param monitor table main viewscreen
local function init(monitor)
local facility = iocontrol.get_db().facility
@ -90,7 +91,7 @@ local function init(monitor)
cnc_bottom_align_start = cnc_bottom_align_start + 2
local process = process_ctl(main, 2, cnc_bottom_align_start)
local _ = process_ctl(main, 2, cnc_bottom_align_start)
-- testing
---@fixme remove test code
@ -123,7 +124,7 @@ local function init(monitor)
SwitchButton{parent=audio,x=1,text="RCS TRANSIENT",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rcs}
SwitchButton{parent=audio,x=1,text="TURBINE TRIP",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_turbinet}
local imatrix_1 = imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
local _ = imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
return main
end

View File

@ -9,12 +9,13 @@ local unit_detail = require("coordinator.ui.components.unit_detail")
local DisplayBox = require("graphics.elements.displaybox")
-- create a unit view
---@nodiscard
---@param monitor table
---@param id integer
local function init(monitor, id)
local main = DisplayBox{window=monitor,fg_bg=style.root}
unit_detail(main, id)
local _ = unit_detail(main, id)
return main
end