mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
Merge pull request #175 from MikaylaFischler/118-code-cleanup-pass
#118 Code Cleanup
This commit is contained in:
commit
6eee0d0c72
@ -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
|
||||
|
@ -14,17 +14,18 @@ local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local DEVICE_TYPES = comms.DEVICE_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
|
||||
local UNIT_COMMANDS = comms.UNIT_COMMANDS
|
||||
local FAC_COMMANDS = comms.FAC_COMMANDS
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
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,37 +210,33 @@ 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()
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES
|
||||
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local pkt = nil ---@type mgmt_packet|crdn_packet
|
||||
|
||||
if protocol == PROTOCOLS.SCADA_MGMT then
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
pkt = comms.mgmt_packet()
|
||||
elseif protocol == PROTOCOLS.SCADA_CRDN then
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
pkt = comms.crdn_packet()
|
||||
else
|
||||
return
|
||||
@ -242,28 +245,30 @@ 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
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.ESTABLISH, { comms.version, version, DEVICE_TYPES.CRDN })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
@ -271,10 +276,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
self.sv_linked = false
|
||||
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {})
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
|
||||
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
|
||||
@ -300,7 +306,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
elseif event == "modem_message" then
|
||||
-- handle message
|
||||
local packet = public.parse_packet(p1, p2, p3, p4, p5)
|
||||
if packet ~= nil and packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
public.handle_packet(packet)
|
||||
end
|
||||
elseif event == "terminate" then
|
||||
@ -329,25 +335,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMANDS command
|
||||
---@param cmd FAC_COMMAND command
|
||||
function public.send_fac_command(cmd)
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { cmd })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param config coord_auto_config configuration
|
||||
function public.send_auto_start(config)
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, {
|
||||
FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
||||
})
|
||||
end
|
||||
|
||||
-- send a unit command
|
||||
---@param cmd UNIT_COMMANDS command
|
||||
---@param cmd UNIT_COMMAND command
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
@ -366,19 +372,19 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as SCADA management packet
|
||||
if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
-- get as coordinator packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
-- get as coordinator API packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.COORD_API then
|
||||
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
|
||||
local capi_pkt = comms.capi_packet()
|
||||
if capi_pkt.decode(s_pkt) then
|
||||
pkt = capi_pkt.get()
|
||||
@ -399,8 +405,8 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
local l_port = packet.scada_frame.local_port()
|
||||
|
||||
if l_port == api_listen then
|
||||
if protocol == PROTOCOLS.COORD_API then
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
if protocol == PROTOCOL.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
apisessions.handle_packet(packet)
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
|
||||
@ -420,9 +426,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
sv_watchdog.feed()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOLS.SCADA_CRDN then
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then
|
||||
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.length == 2 then
|
||||
-- record builds
|
||||
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
|
||||
@ -430,47 +437,47 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
|
||||
if fac_builds and unit_builds then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.INITIAL_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")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.FAC_BUILDS then
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then
|
||||
if packet.length == 1 then
|
||||
-- record facility builds
|
||||
if iocontrol.record_facility_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_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")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.FAC_STATUS then
|
||||
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_TYPES.FAC_CMD then
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
local ack = packet.data[2] == true
|
||||
|
||||
if cmd == FAC_COMMANDS.SCRAM_ALL then
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
iocontrol.get_db().facility.scram_ack(ack)
|
||||
elseif cmd == FAC_COMMANDS.STOP then
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
iocontrol.get_db().facility.stop_ack(ack)
|
||||
elseif cmd == FAC_COMMANDS.START then
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if packet.length == 7 then
|
||||
process.start_ack_handle({ table.unpack(packet.data, 2) })
|
||||
else
|
||||
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
@ -478,24 +485,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
|
||||
-- record builds
|
||||
if packet.length == 1 then
|
||||
if iocontrol.record_unit_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_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")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_unit_statuses(packet.data) then
|
||||
log.error("received invalid UNIT_STATUSES packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPES.UNIT_CMD then
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
if packet.length == 3 then
|
||||
local cmd = packet.data[1]
|
||||
@ -505,20 +512,20 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit
|
||||
|
||||
if unit ~= nil then
|
||||
if cmd == UNIT_COMMANDS.SCRAM then
|
||||
if cmd == UNIT_COMMAND.SCRAM then
|
||||
unit.scram_ack(ack)
|
||||
elseif cmd == UNIT_COMMANDS.START then
|
||||
elseif cmd == UNIT_COMMAND.START then
|
||||
unit.start_ack(ack)
|
||||
elseif cmd == UNIT_COMMANDS.RESET_RPS then
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
unit.reset_rps_ack(ack)
|
||||
elseif cmd == UNIT_COMMANDS.SET_BURN then
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
unit.set_burn_ack(ack)
|
||||
elseif cmd == UNIT_COMMANDS.SET_WASTE then
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
unit.set_waste_ack(ack)
|
||||
elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_alarms_ack(ack)
|
||||
elseif cmd == UNIT_COMMANDS.SET_GROUP then
|
||||
---@todo how is this going to be handled?
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
-- 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
|
||||
@ -534,8 +541,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
else
|
||||
log.debug("discarding SCADA_CRDN packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
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
|
||||
local est_ack = packet.data[1]
|
||||
@ -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
|
||||
@ -596,7 +604,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
elseif self.sv_linked then
|
||||
if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
@ -614,14 +622,14 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
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
|
||||
|
@ -1,4 +1,7 @@
|
||||
local comms = require("scada-common.comms")
|
||||
--
|
||||
-- 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")
|
||||
@ -7,8 +10,6 @@ local util = require("scada-common.util")
|
||||
local process = require("coordinator.process")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local UNIT_COMMANDS = comms.UNIT_COMMANDS
|
||||
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
local iocontrol = {}
|
||||
@ -19,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 = {
|
||||
@ -44,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(),
|
||||
|
||||
@ -59,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())
|
||||
@ -173,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
|
||||
|
||||
@ -190,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
|
||||
|
||||
@ -287,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]
|
||||
|
||||
@ -330,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
|
||||
@ -337,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
|
||||
@ -349,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
|
||||
@ -374,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
|
||||
@ -399,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
|
||||
@ -416,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
|
||||
|
||||
@ -732,7 +762,7 @@ function iocontrol.update_unit_statuses(statuses)
|
||||
sounder.eval(io.units)
|
||||
end
|
||||
|
||||
return true
|
||||
return valid
|
||||
end
|
||||
|
||||
-- get the IO controller database
|
||||
|
@ -1,11 +1,14 @@
|
||||
--
|
||||
-- Process Control Management
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local FAC_COMMANDS = comms.FAC_COMMANDS
|
||||
local UNIT_COMMANDS = comms.UNIT_COMMANDS
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
local PROCESS = types.PROCESS
|
||||
|
||||
@ -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
|
||||
@ -71,7 +74,7 @@ function process.init(iocontrol, comms)
|
||||
|
||||
if type(waste_mode) == "table" then
|
||||
for id, mode in pairs(waste_mode) do
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded waste mode settings from coord.settings")
|
||||
@ -81,7 +84,7 @@ function process.init(iocontrol, comms)
|
||||
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, id, group)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded priority groups settings from coord.settings")
|
||||
@ -90,45 +93,45 @@ end
|
||||
|
||||
-- facility SCRAM command
|
||||
function process.fac_scram()
|
||||
self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL)
|
||||
log.debug("FAC: SCRAM ALL")
|
||||
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
|
||||
log.debug("PROCESS: FAC SCRAM ALL")
|
||||
end
|
||||
|
||||
-- facility alarm acknowledge command
|
||||
function process.fac_ack_alarms()
|
||||
self.comms.send_fac_command(FAC_COMMANDS.ACK_ALL_ALARMS)
|
||||
log.debug("FAC: ACK ALL ALARMS")
|
||||
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
|
||||
log.debug("PROCESS: FAC ACK ALL ALARMS")
|
||||
end
|
||||
|
||||
-- start reactor
|
||||
---@param id integer unit ID
|
||||
function process.start(id)
|
||||
self.io.units[id].control_state = true
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.START, id)
|
||||
log.debug(util.c("UNIT[", id, "]: START"))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.START, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
|
||||
end
|
||||
|
||||
-- SCRAM reactor
|
||||
---@param id integer unit ID
|
||||
function process.scram(id)
|
||||
self.io.units[id].control_state = false
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id)
|
||||
log.debug(util.c("UNIT[", id, "]: SCRAM"))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
|
||||
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_COMMANDS.RESET_RPS, id)
|
||||
log.debug(util.c("UNIT[", id, "]: RESET RPS"))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
|
||||
end
|
||||
|
||||
-- set burn rate
|
||||
---@param id integer unit ID
|
||||
---@param rate number burn rate
|
||||
function process.set_rate(id, rate)
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate)
|
||||
log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
|
||||
end
|
||||
|
||||
-- set waste mode
|
||||
@ -138,14 +141,12 @@ function process.set_waste(id, mode)
|
||||
-- publish so that if it fails then it gets reset
|
||||
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode)
|
||||
log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, 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
|
||||
|
||||
@ -159,38 +160,36 @@ end
|
||||
-- acknowledge all alarms
|
||||
---@param id integer unit ID
|
||||
function process.ack_all_alarms(id)
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS"))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
|
||||
end
|
||||
|
||||
-- acknowledge an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.ack_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm)
|
||||
log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- reset an alarm
|
||||
---@param id integer unit ID
|
||||
---@param alarm integer alarm ID
|
||||
function process.reset_alarm(id, alarm)
|
||||
self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
|
||||
end
|
||||
|
||||
-- assign a unit to a group
|
||||
---@param unit_id integer unit ID
|
||||
---@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_COMMANDS.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id))
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, 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
|
||||
|
||||
@ -207,14 +206,14 @@ end
|
||||
|
||||
-- stop automatic process control
|
||||
function process.stop_auto()
|
||||
self.comms.send_fac_command(FAC_COMMANDS.STOP)
|
||||
log.debug("FAC: STOP AUTO")
|
||||
self.comms.send_fac_command(FAC_COMMAND.STOP)
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@ -47,7 +49,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 +57,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))
|
||||
|
@ -237,13 +237,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)
|
||||
@ -287,7 +287,7 @@ local function init(parent, id)
|
||||
end
|
||||
|
||||
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
t_ps[1].subscribe("SteamDumpOpen", function (val) t1_sdo.update(val + 1) end)
|
||||
t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
@ -300,7 +300,7 @@ local function init(parent, id)
|
||||
if unit.num_turbines > 1 then
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
t_ps[2].subscribe("SteamDumpOpen", function (val) t2_sdo.update(val + 1) end)
|
||||
t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
@ -314,7 +314,7 @@ local function init(parent, id)
|
||||
if unit.num_turbines > 2 then
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
t_ps[3].subscribe("SteamDumpOpen", function (val) t3_sdo.update(val + 1) end)
|
||||
t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
|
@ -101,16 +101,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
|
||||
|
@ -1,5 +1,5 @@
|
||||
--
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
-- Reactor Unit Waiting Spinner
|
||||
--
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
@ -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
|
||||
|
@ -77,7 +77,7 @@ local function init(monitor)
|
||||
end
|
||||
end
|
||||
|
||||
-- command & control
|
||||
-- command & control
|
||||
|
||||
cnc_y_start = cnc_y_start
|
||||
|
||||
@ -90,7 +90,7 @@ local function init(monitor)
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
local process = process_ctl(main, 2, cnc_bottom_align_start)
|
||||
process_ctl(main, 2, cnc_bottom_align_start)
|
||||
|
||||
-- testing
|
||||
---@fixme remove test code
|
||||
@ -123,7 +123,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])
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||
|
||||
return main
|
||||
end
|
||||
|
@ -16,6 +16,7 @@ local events = {}
|
||||
---@field y integer
|
||||
|
||||
-- create a new touch event definition
|
||||
---@nodiscard
|
||||
---@param monitor string
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
@ -32,7 +33,7 @@ core.events = events
|
||||
|
||||
local graphics = {}
|
||||
|
||||
---@alias TEXT_ALIGN integer
|
||||
---@enum TEXT_ALIGN
|
||||
graphics.TEXT_ALIGN = {
|
||||
LEFT = 1,
|
||||
CENTER = 2,
|
||||
@ -47,6 +48,7 @@ graphics.TEXT_ALIGN = {
|
||||
---@alias element_id string|integer
|
||||
|
||||
-- create a new border definition
|
||||
---@nodiscard
|
||||
---@param width integer border width
|
||||
---@param color color border color
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
@ -66,6 +68,7 @@ end
|
||||
---@field h integer
|
||||
|
||||
-- create a new graphics frame definition
|
||||
---@nodiscard
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
@ -91,6 +94,7 @@ end
|
||||
---@field blit_bkg string
|
||||
|
||||
-- create a new color pair definition
|
||||
---@nodiscard
|
||||
---@param a color
|
||||
---@param b color
|
||||
---@return cpair
|
||||
@ -120,9 +124,9 @@ end
|
||||
---@field thin boolean true for 1 subpixel, false (default) for 2
|
||||
---@field align_tr boolean false to align bottom left (default), true to align top right
|
||||
|
||||
-- create a new pipe
|
||||
--
|
||||
-- create a new pipe<br>
|
||||
-- note: pipe coordinate origin is (0, 0)
|
||||
---@nodiscard
|
||||
---@param x1 integer starting x, origin is 0
|
||||
---@param y1 integer starting y, origin is 0
|
||||
---@param x2 integer ending x, origin is 0
|
||||
|
@ -47,6 +47,7 @@ local element = {}
|
||||
---|tiling_args
|
||||
|
||||
-- a base graphics element, should not be created on its own
|
||||
---@nodiscard
|
||||
---@param args graphics_args arguments
|
||||
function element.new(args)
|
||||
local self = {
|
||||
@ -172,6 +173,7 @@ function element.new(args)
|
||||
end
|
||||
|
||||
-- get value
|
||||
---@nodiscard
|
||||
function protected.get_value()
|
||||
return protected.value
|
||||
end
|
||||
@ -218,6 +220,7 @@ function element.new(args)
|
||||
end
|
||||
|
||||
-- get public interface
|
||||
---@nodiscard
|
||||
---@return graphics_element element, element_id id
|
||||
function protected.get() return public, self.id end
|
||||
|
||||
@ -246,11 +249,13 @@ function element.new(args)
|
||||
----------------------
|
||||
|
||||
-- get the window object
|
||||
---@nodiscard
|
||||
function public.window() return protected.window end
|
||||
|
||||
-- CHILD ELEMENTS --
|
||||
|
||||
-- add a child element
|
||||
---@nodiscard
|
||||
---@param key string|nil id
|
||||
---@param child graphics_template
|
||||
---@return integer|string key
|
||||
@ -271,6 +276,7 @@ function element.new(args)
|
||||
end
|
||||
|
||||
-- get a child element
|
||||
---@nodiscard
|
||||
---@return graphics_element
|
||||
function public.get_child(key) return self.children[key] end
|
||||
|
||||
@ -279,6 +285,7 @@ function element.new(args)
|
||||
function public.remove(key) self.children[key] = nil end
|
||||
|
||||
-- attempt to get a child element by ID (does not include this element itself)
|
||||
---@nodiscard
|
||||
---@param id element_id
|
||||
---@return graphics_element|nil element
|
||||
function public.get_element_by_id(id)
|
||||
@ -297,39 +304,49 @@ function element.new(args)
|
||||
-- AUTO-PLACEMENT --
|
||||
|
||||
-- skip a line for automatically placed elements
|
||||
function public.line_break() self.next_y = self.next_y + 1 end
|
||||
function public.line_break()
|
||||
self.next_y = self.next_y + 1
|
||||
end
|
||||
|
||||
-- PROPERTIES --
|
||||
|
||||
-- get the foreground/background colors
|
||||
---@nodiscard
|
||||
---@return cpair fg_bg
|
||||
function public.get_fg_bg() return protected.fg_bg end
|
||||
function public.get_fg_bg()
|
||||
return protected.fg_bg
|
||||
end
|
||||
|
||||
-- get element x
|
||||
---@nodiscard
|
||||
---@return integer x
|
||||
function public.get_x()
|
||||
return protected.frame.x
|
||||
end
|
||||
|
||||
-- get element y
|
||||
---@nodiscard
|
||||
---@return integer y
|
||||
function public.get_y()
|
||||
return protected.frame.y
|
||||
end
|
||||
|
||||
-- get element width
|
||||
---@nodiscard
|
||||
---@return integer width
|
||||
function public.width()
|
||||
return protected.frame.w
|
||||
end
|
||||
|
||||
-- get element height
|
||||
---@nodiscard
|
||||
---@return integer height
|
||||
function public.height()
|
||||
return protected.frame.h
|
||||
end
|
||||
|
||||
-- get the element value
|
||||
---@nodiscard
|
||||
---@return any value
|
||||
function public.get_value()
|
||||
return protected.get_value()
|
||||
|
@ -12,6 +12,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new root display box
|
||||
---@nodiscard
|
||||
---@param args displaybox_args
|
||||
local function displaybox(args)
|
||||
-- create new graphics element base object
|
||||
|
@ -13,6 +13,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new div element
|
||||
---@nodiscard
|
||||
---@param args div_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function div(args)
|
||||
|
@ -20,6 +20,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new alarm indicator light
|
||||
---@nodiscard
|
||||
---@param args alarm_indicator_light
|
||||
---@return graphics_element element, element_id id
|
||||
local function alarm_indicator_light(args)
|
||||
|
@ -14,6 +14,7 @@ local element = require("graphics.element")
|
||||
---@field y? integer 1 if omitted
|
||||
|
||||
-- new core map box
|
||||
---@nodiscard
|
||||
---@param args core_map_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function core_map(args)
|
||||
|
@ -19,6 +19,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new data indicator
|
||||
---@nodiscard
|
||||
---@param args data_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function data(args)
|
||||
|
@ -17,6 +17,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new horizontal bar
|
||||
---@nodiscard
|
||||
---@param args hbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hbar(args)
|
||||
|
@ -20,6 +20,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new icon indicator
|
||||
---@nodiscard
|
||||
---@param args icon_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function icon(args)
|
||||
|
@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_light(args)
|
||||
|
@ -18,6 +18,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new power indicator
|
||||
---@nodiscard
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
|
@ -19,6 +19,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new radiation indicator
|
||||
---@nodiscard
|
||||
---@param args rad_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rad(args)
|
||||
|
@ -20,6 +20,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new state indicator
|
||||
---@nodiscard
|
||||
---@param args state_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function state_indicator(args)
|
||||
|
@ -20,6 +20,7 @@ local flasher = require("graphics.flasher")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new tri-state indicator light
|
||||
---@nodiscard
|
||||
---@param args tristate_indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tristate_indicator_light(args)
|
||||
|
@ -15,6 +15,7 @@ local element = require("graphics.element")
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
|
||||
-- new vertical bar
|
||||
---@nodiscard
|
||||
---@param args vbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function vbar(args)
|
||||
|
@ -144,7 +144,7 @@ local function rectangle(args)
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
if (args.thin == true) then
|
||||
if args.thin == true then
|
||||
e.window.blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.window.blit(p_s, blit_fg, blit_bg_sides)
|
||||
|
@ -60,7 +60,7 @@ local function tiling(args)
|
||||
-- create pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
for x = 1, inner_width do
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_a .. fill_a)
|
||||
|
@ -21,8 +21,7 @@ local active = false
|
||||
local registry = { {}, {}, {} } -- one registry table per period
|
||||
local callback_counter = 0
|
||||
|
||||
-- blink registered indicators
|
||||
--
|
||||
-- blink registered indicators<br>
|
||||
-- this assumes it is called every 250ms, it does no checking of time on its own
|
||||
local function callback_250ms()
|
||||
if active then
|
||||
@ -55,8 +54,7 @@ function flasher.clear()
|
||||
registry = { {}, {}, {} }
|
||||
end
|
||||
|
||||
-- register a function to be called on the selected blink period
|
||||
--
|
||||
-- register a function to be called on the selected blink period<br>
|
||||
-- times are not strictly enforced, but all with a given period will be set at the same time
|
||||
---@param f function function to call each period
|
||||
---@param period PERIOD time period option (1, 2, or 3)
|
||||
|
@ -2,10 +2,10 @@
|
||||
"versions": {
|
||||
"bootloader": "0.2",
|
||||
"comms": "1.4.0",
|
||||
"reactor-plc": "beta-v0.11.1",
|
||||
"rtu": "beta-v0.11.2",
|
||||
"supervisor": "beta-v0.12.2",
|
||||
"coordinator": "beta-v0.10.1",
|
||||
"reactor-plc": "v0.12.0",
|
||||
"rtu": "v0.12.1",
|
||||
"supervisor": "v0.13.1",
|
||||
"coordinator": "v0.11.0",
|
||||
"pocket": "alpha-v0.0.0"
|
||||
},
|
||||
"files": {
|
||||
@ -177,13 +177,13 @@
|
||||
},
|
||||
"sizes": {
|
||||
"system": 1982,
|
||||
"common": 88163,
|
||||
"graphics": 99360,
|
||||
"common": 88565,
|
||||
"graphics": 99858,
|
||||
"lockbox": 100797,
|
||||
"reactor-plc": 75902,
|
||||
"rtu": 81679,
|
||||
"supervisor": 268416,
|
||||
"coordinator": 181783,
|
||||
"reactor-plc": 75621,
|
||||
"rtu": 85496,
|
||||
"supervisor": 270182,
|
||||
"coordinator": 183279,
|
||||
"pocket": 335
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
@ -6,15 +7,17 @@ local util = require("scada-common.util")
|
||||
|
||||
local plc = {}
|
||||
|
||||
local rps_status_t = types.rps_status_t
|
||||
local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local DEVICE_TYPES = comms.DEVICE_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local RPLC_TYPES = comms.RPLC_TYPES
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local RPLC_TYPE = comms.RPLC_TYPE
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local AUTO_ACK = comms.PLC_AUTO_ACK
|
||||
|
||||
local RPS_LIMITS = const.RPS_LIMITS
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
@ -25,21 +28,10 @@ local println_ts = util.println_ts
|
||||
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
|
||||
local PCALL_START_MSG = "pcall: Reactor is already active."
|
||||
|
||||
-- RPS SAFETY CONSTANTS
|
||||
|
||||
local MAX_DAMAGE_PERCENT = 90
|
||||
local MAX_DAMAGE_TEMPERATURE = 1200
|
||||
local MIN_COOLANT_FILL = 0.10
|
||||
local MAX_WASTE_FILL = 0.8
|
||||
local MAX_HEATED_COLLANT_FILL = 0.95
|
||||
|
||||
-- END RPS SAFETY CONSTANTS
|
||||
|
||||
--- RPS: Reactor Protection System
|
||||
---
|
||||
--- identifies dangerous states and SCRAMs reactor if warranted
|
||||
---
|
||||
--- autonomous from main SCADA supervisor/coordinator control
|
||||
-- RPS: Reactor Protection System<br>
|
||||
-- identifies dangerous states and SCRAMs reactor if warranted<br>
|
||||
-- autonomous from main SCADA supervisor/coordinator control
|
||||
---@nodiscard
|
||||
---@param reactor table
|
||||
---@param is_formed boolean
|
||||
function plc.rps_init(reactor, is_formed)
|
||||
@ -59,24 +51,20 @@ function plc.rps_init(reactor, is_formed)
|
||||
}
|
||||
|
||||
local self = {
|
||||
reactor = reactor,
|
||||
state = { false, false, false, false, false, false, false, false, false, false, false, false },
|
||||
reactor_enabled = false,
|
||||
enabled_at = 0,
|
||||
formed = is_formed,
|
||||
force_disabled = false,
|
||||
tripped = false,
|
||||
trip_cause = "ok" ---@type rps_trip_cause
|
||||
trip_cause = "ok" ---@type rps_trip_cause
|
||||
}
|
||||
|
||||
---@class rps
|
||||
local public = {}
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- set reactor access fault flag
|
||||
local function _set_fault()
|
||||
if self.reactor.__p_last_fault() ~= "Terminated" then
|
||||
if reactor.__p_last_fault() ~= "Terminated" then
|
||||
self.state[state_keys.fault] = true
|
||||
end
|
||||
end
|
||||
@ -88,7 +76,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
|
||||
-- check if the reactor is formed
|
||||
local function _is_formed()
|
||||
local formed = self.reactor.isFormed()
|
||||
local formed = reactor.isFormed()
|
||||
if formed == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
@ -103,7 +91,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
|
||||
-- check if the reactor is force disabled
|
||||
local function _is_force_disabled()
|
||||
local disabled = self.reactor.isForceDisabled()
|
||||
local disabled = reactor.isForceDisabled()
|
||||
if disabled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
@ -118,77 +106,80 @@ function plc.rps_init(reactor, is_formed)
|
||||
|
||||
-- check for critical damage
|
||||
local function _damage_critical()
|
||||
local damage_percent = self.reactor.getDamagePercent()
|
||||
local damage_percent = reactor.getDamagePercent()
|
||||
if damage_percent == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.dmg_crit] then
|
||||
self.state[state_keys.dmg_crit] = damage_percent >= MAX_DAMAGE_PERCENT
|
||||
self.state[state_keys.dmg_crit] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT
|
||||
end
|
||||
end
|
||||
|
||||
-- check if the reactor is at a critically high temperature
|
||||
local function _high_temp()
|
||||
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
|
||||
local temp = self.reactor.getTemperature()
|
||||
local temp = reactor.getTemperature()
|
||||
if temp == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.high_temp] then
|
||||
self.state[state_keys.high_temp] = temp >= MAX_DAMAGE_TEMPERATURE
|
||||
self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE
|
||||
end
|
||||
end
|
||||
|
||||
-- check if there is no coolant (<2% filled)
|
||||
local function _no_coolant()
|
||||
local coolant_filled = self.reactor.getCoolantFilledPercentage()
|
||||
local coolant_filled = reactor.getCoolantFilledPercentage()
|
||||
if coolant_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.no_coolant] then
|
||||
self.state[state_keys.no_coolant] = coolant_filled < MIN_COOLANT_FILL
|
||||
self.state[state_keys.no_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL
|
||||
end
|
||||
end
|
||||
|
||||
-- check for excess waste (>80% filled)
|
||||
local function _excess_waste()
|
||||
local w_filled = self.reactor.getWasteFilledPercentage()
|
||||
local w_filled = reactor.getWasteFilledPercentage()
|
||||
if w_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.ex_waste] then
|
||||
self.state[state_keys.ex_waste] = w_filled > MAX_WASTE_FILL
|
||||
self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL
|
||||
end
|
||||
end
|
||||
|
||||
-- check for heated coolant backup (>95% filled)
|
||||
local function _excess_heated_coolant()
|
||||
local hc_filled = self.reactor.getHeatedCoolantFilledPercentage()
|
||||
local hc_filled = reactor.getHeatedCoolantFilledPercentage()
|
||||
if hc_filled == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.ex_hcoolant] then
|
||||
self.state[state_keys.ex_hcoolant] = hc_filled > MAX_HEATED_COLLANT_FILL
|
||||
self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL
|
||||
end
|
||||
end
|
||||
|
||||
-- check if there is no fuel
|
||||
local function _insufficient_fuel()
|
||||
local fuel = self.reactor.getFuel()
|
||||
local fuel = reactor.getFuelFilledPercentage()
|
||||
if fuel == ppm.ACCESS_FAULT then
|
||||
-- lost the peripheral or terminated, handled later
|
||||
_set_fault()
|
||||
elseif not self.state[state_keys.no_fuel] then
|
||||
self.state[state_keys.no_fuel] = fuel == 0
|
||||
self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL
|
||||
end
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class rps
|
||||
local public = {}
|
||||
|
||||
-- re-link a reactor after a peripheral re-connect
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_reactor(reactor)
|
||||
self.reactor = reactor
|
||||
---@param new_reactor table reconnected reactor
|
||||
function public.reconnect_reactor(new_reactor)
|
||||
reactor = new_reactor
|
||||
end
|
||||
|
||||
-- trip for lost peripheral
|
||||
@ -222,8 +213,8 @@ function plc.rps_init(reactor, is_formed)
|
||||
function public.scram()
|
||||
log.info("RPS: reactor SCRAM")
|
||||
|
||||
self.reactor.scram()
|
||||
if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
|
||||
reactor.scram()
|
||||
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
|
||||
log.error("RPS: failed reactor SCRAM")
|
||||
return false
|
||||
else
|
||||
@ -239,8 +230,8 @@ function plc.rps_init(reactor, is_formed)
|
||||
if not self.tripped then
|
||||
log.info("RPS: reactor start")
|
||||
|
||||
self.reactor.activate()
|
||||
if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_START_MSG) then
|
||||
reactor.activate()
|
||||
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
|
||||
log.error("RPS: failed reactor start")
|
||||
else
|
||||
self.reactor_enabled = true
|
||||
@ -260,7 +251,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
-- clear automatic SCRAM if it was the cause
|
||||
if self.tripped and self.trip_cause == "automatic" then
|
||||
self.state[state_keys.automatic] = true
|
||||
self.trip_cause = rps_status_t.ok
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
self.tripped = false
|
||||
|
||||
log.debug("RPS: cleared automatic SCRAM for re-activation")
|
||||
@ -270,9 +261,10 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
|
||||
-- check all safety conditions
|
||||
---@return boolean tripped, rps_status_t trip_status, boolean first_trip
|
||||
---@nodiscard
|
||||
---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip
|
||||
function public.check()
|
||||
local status = rps_status_t.ok
|
||||
local status = RPS_TRIP_CAUSE.OK
|
||||
local was_tripped = self.tripped
|
||||
local first_trip = false
|
||||
|
||||
@ -298,47 +290,47 @@ function plc.rps_init(reactor, is_formed)
|
||||
status = self.trip_cause
|
||||
elseif self.state[state_keys.sys_fail] then
|
||||
log.warning("RPS: system failure, reactor not formed")
|
||||
status = rps_status_t.sys_fail
|
||||
status = RPS_TRIP_CAUSE.SYS_FAIL
|
||||
elseif self.state[state_keys.force_disabled] then
|
||||
log.warning("RPS: reactor was force disabled")
|
||||
status = rps_status_t.force_disabled
|
||||
status = RPS_TRIP_CAUSE.FORCE_DISABLED
|
||||
elseif self.state[state_keys.dmg_crit] then
|
||||
log.warning("RPS: damage critical")
|
||||
status = rps_status_t.dmg_crit
|
||||
status = RPS_TRIP_CAUSE.DMG_CRIT
|
||||
elseif self.state[state_keys.high_temp] then
|
||||
log.warning("RPS: high temperature")
|
||||
status = rps_status_t.high_temp
|
||||
status = RPS_TRIP_CAUSE.HIGH_TEMP
|
||||
elseif self.state[state_keys.no_coolant] then
|
||||
log.warning("RPS: no coolant")
|
||||
status = rps_status_t.no_coolant
|
||||
status = RPS_TRIP_CAUSE.NO_COOLANT
|
||||
elseif self.state[state_keys.ex_waste] then
|
||||
log.warning("RPS: full waste")
|
||||
status = rps_status_t.ex_waste
|
||||
status = RPS_TRIP_CAUSE.EX_WASTE
|
||||
elseif self.state[state_keys.ex_hcoolant] then
|
||||
log.warning("RPS: heated coolant backup")
|
||||
status = rps_status_t.ex_hcoolant
|
||||
status = RPS_TRIP_CAUSE.EX_HCOOLANT
|
||||
elseif self.state[state_keys.no_fuel] then
|
||||
log.warning("RPS: no fuel")
|
||||
status = rps_status_t.no_fuel
|
||||
status = RPS_TRIP_CAUSE.NO_FUEL
|
||||
elseif self.state[state_keys.fault] then
|
||||
log.warning("RPS: reactor access fault")
|
||||
status = rps_status_t.fault
|
||||
status = RPS_TRIP_CAUSE.FAULT
|
||||
elseif self.state[state_keys.timeout] then
|
||||
log.warning("RPS: supervisor connection timeout")
|
||||
status = rps_status_t.timeout
|
||||
status = RPS_TRIP_CAUSE.TIMEOUT
|
||||
elseif self.state[state_keys.manual] then
|
||||
log.warning("RPS: manual SCRAM requested")
|
||||
status = rps_status_t.manual
|
||||
status = RPS_TRIP_CAUSE.MANUAL
|
||||
elseif self.state[state_keys.automatic] then
|
||||
log.warning("RPS: automatic SCRAM requested")
|
||||
status = rps_status_t.automatic
|
||||
status = RPS_TRIP_CAUSE.AUTOMATIC
|
||||
else
|
||||
self.tripped = false
|
||||
self.trip_cause = rps_status_t.ok
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
end
|
||||
|
||||
-- if a new trip occured...
|
||||
if (not was_tripped) and (status ~= rps_status_t.ok) then
|
||||
if (not was_tripped) and (status ~= RPS_TRIP_CAUSE.OK) then
|
||||
first_trip = true
|
||||
self.tripped = true
|
||||
self.trip_cause = status
|
||||
@ -359,16 +351,23 @@ function plc.rps_init(reactor, is_formed)
|
||||
return self.tripped, status, first_trip
|
||||
end
|
||||
|
||||
---@nodiscard
|
||||
function public.status() return self.state end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_tripped() return self.tripped end
|
||||
---@nodiscard
|
||||
function public.get_trip_cause() return self.trip_cause end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_active() return self.reactor_enabled end
|
||||
---@nodiscard
|
||||
function public.is_formed() return self.formed end
|
||||
---@nodiscard
|
||||
function public.is_force_disabled() return self.force_disabled end
|
||||
|
||||
-- get the runtime of the reactor if active, or the last runtime if disabled
|
||||
---@nodiscard
|
||||
---@return integer runtime time since last enable
|
||||
function public.get_runtime() return util.trinary(self.reactor_enabled, util.time_ms() - self.enabled_at, self.last_runtime) end
|
||||
|
||||
@ -376,7 +375,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
---@param quiet? boolean true to suppress the info log message
|
||||
function public.reset(quiet)
|
||||
self.tripped = false
|
||||
self.trip_cause = rps_status_t.ok
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
|
||||
for i = 1, #self.state do
|
||||
self.state[i] = false
|
||||
@ -390,8 +389,8 @@ function plc.rps_init(reactor, is_formed)
|
||||
self.state[state_keys.automatic] = false
|
||||
self.state[state_keys.timeout] = false
|
||||
|
||||
if self.trip_cause == rps_status_t.automatic or self.trip_cause == rps_status_t.timeout then
|
||||
self.trip_cause = rps_status_t.ok
|
||||
if self.trip_cause == RPS_TRIP_CAUSE.AUTOMATIC or self.trip_cause == RPS_TRIP_CAUSE.TIMEOUT then
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
self.tripped = false
|
||||
|
||||
log.info("RPS: auto reset")
|
||||
@ -402,6 +401,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
|
||||
-- Reactor PLC Communications
|
||||
---@nodiscard
|
||||
---@param id integer reactor ID
|
||||
---@param version string PLC version
|
||||
---@param modem table modem device
|
||||
@ -415,10 +415,6 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
local self = {
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
modem = modem,
|
||||
s_port = server_port,
|
||||
l_port = local_port,
|
||||
reactor = reactor,
|
||||
scrammed = false,
|
||||
linked = false,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
@ -428,46 +424,43 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
max_burn_rate = nil
|
||||
}
|
||||
|
||||
---@class plc_comms
|
||||
local public = {}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
self.modem.closeAll()
|
||||
self.modem.open(self.l_port)
|
||||
modem.closeAll()
|
||||
modem.open(local_port)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
|
||||
-- send an RPLC packet
|
||||
---@param msg_type RPLC_TYPES
|
||||
---@param msg_type RPLC_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local r_pkt = comms.rplc_packet()
|
||||
|
||||
r_pkt.make(id, msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@ -500,21 +493,21 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
}
|
||||
|
||||
local tasks = {
|
||||
function () data_table[1] = self.reactor.getStatus() end,
|
||||
function () data_table[2] = self.reactor.getBurnRate() end,
|
||||
function () data_table[3] = self.reactor.getActualBurnRate() end,
|
||||
function () data_table[4] = self.reactor.getTemperature() end,
|
||||
function () data_table[5] = self.reactor.getDamagePercent() end,
|
||||
function () data_table[6] = self.reactor.getBoilEfficiency() end,
|
||||
function () data_table[7] = self.reactor.getEnvironmentalLoss() end,
|
||||
function () fuel = self.reactor.getFuel() end,
|
||||
function () data_table[9] = self.reactor.getFuelFilledPercentage() end,
|
||||
function () waste = self.reactor.getWaste() end,
|
||||
function () data_table[11] = self.reactor.getWasteFilledPercentage() end,
|
||||
function () coolant = self.reactor.getCoolant() end,
|
||||
function () data_table[14] = self.reactor.getCoolantFilledPercentage() end,
|
||||
function () hcoolant = self.reactor.getHeatedCoolant() end,
|
||||
function () data_table[17] = self.reactor.getHeatedCoolantFilledPercentage() end
|
||||
function () data_table[1] = reactor.getStatus() end,
|
||||
function () data_table[2] = reactor.getBurnRate() end,
|
||||
function () data_table[3] = reactor.getActualBurnRate() end,
|
||||
function () data_table[4] = reactor.getTemperature() end,
|
||||
function () data_table[5] = reactor.getDamagePercent() end,
|
||||
function () data_table[6] = reactor.getBoilEfficiency() end,
|
||||
function () data_table[7] = reactor.getEnvironmentalLoss() end,
|
||||
function () fuel = reactor.getFuel() end,
|
||||
function () data_table[9] = reactor.getFuelFilledPercentage() end,
|
||||
function () waste = reactor.getWaste() end,
|
||||
function () data_table[11] = reactor.getWasteFilledPercentage() end,
|
||||
function () coolant = reactor.getCoolant() end,
|
||||
function () data_table[14] = reactor.getCoolantFilledPercentage() end,
|
||||
function () hcoolant = reactor.getHeatedCoolant() end,
|
||||
function () data_table[17] = reactor.getHeatedCoolantFilledPercentage() end
|
||||
}
|
||||
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
@ -537,7 +530,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
data_table[16] = hcoolant.amount
|
||||
end
|
||||
|
||||
return data_table, self.reactor.__p_is_faulted()
|
||||
return data_table, reactor.__p_is_faulted()
|
||||
end
|
||||
|
||||
-- update the status cache if changed
|
||||
@ -569,11 +562,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- general ack
|
||||
---@param msg_type RPLC_TYPES
|
||||
---@param msg_type RPLC_TYPE
|
||||
---@param status boolean|integer
|
||||
local function _send_ack(msg_type, status)
|
||||
_send(msg_type, { status })
|
||||
@ -587,25 +580,25 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
local mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 }
|
||||
|
||||
local tasks = {
|
||||
function () mek_data[1] = self.reactor.getLength() end,
|
||||
function () mek_data[2] = self.reactor.getWidth() end,
|
||||
function () mek_data[3] = self.reactor.getHeight() end,
|
||||
function () mek_data[4] = self.reactor.getMinPos() end,
|
||||
function () mek_data[5] = self.reactor.getMaxPos() end,
|
||||
function () mek_data[6] = self.reactor.getHeatCapacity() end,
|
||||
function () mek_data[7] = self.reactor.getFuelAssemblies() end,
|
||||
function () mek_data[8] = self.reactor.getFuelSurfaceArea() end,
|
||||
function () mek_data[9] = self.reactor.getFuelCapacity() end,
|
||||
function () mek_data[10] = self.reactor.getWasteCapacity() end,
|
||||
function () mek_data[11] = self.reactor.getCoolantCapacity() end,
|
||||
function () mek_data[12] = self.reactor.getHeatedCoolantCapacity() end,
|
||||
function () mek_data[13] = self.reactor.getMaxBurnRate() end
|
||||
function () mek_data[1] = reactor.getLength() end,
|
||||
function () mek_data[2] = reactor.getWidth() end,
|
||||
function () mek_data[3] = reactor.getHeight() end,
|
||||
function () mek_data[4] = reactor.getMinPos() end,
|
||||
function () mek_data[5] = reactor.getMaxPos() end,
|
||||
function () mek_data[6] = reactor.getHeatCapacity() end,
|
||||
function () mek_data[7] = reactor.getFuelAssemblies() end,
|
||||
function () mek_data[8] = reactor.getFuelSurfaceArea() end,
|
||||
function () mek_data[9] = reactor.getFuelCapacity() end,
|
||||
function () mek_data[10] = reactor.getWasteCapacity() end,
|
||||
function () mek_data[11] = reactor.getCoolantCapacity() end,
|
||||
function () mek_data[12] = reactor.getHeatedCoolantCapacity() end,
|
||||
function () mek_data[13] = reactor.getMaxBurnRate() end
|
||||
}
|
||||
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
|
||||
if not self.reactor.__p_is_faulted() then
|
||||
_send(RPLC_TYPES.MEK_STRUCT, mek_data)
|
||||
if not reactor.__p_is_faulted() then
|
||||
_send(RPLC_TYPE.MEK_STRUCT, mek_data)
|
||||
self.resend_build = false
|
||||
else
|
||||
log.error("failed to send structure: PPM fault")
|
||||
@ -614,19 +607,20 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class plc_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
|
||||
|
||||
-- reconnect a newly connected reactor
|
||||
---@param reactor table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.reconnect_reactor(reactor)
|
||||
self.reactor = reactor
|
||||
---@param new_reactor table
|
||||
function public.reconnect_reactor(new_reactor)
|
||||
reactor = new_reactor
|
||||
self.status_cache = nil
|
||||
self.resend_build = true
|
||||
self.max_burn_rate = nil
|
||||
@ -643,12 +637,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
function public.close()
|
||||
conn_watchdog.cancel()
|
||||
public.unlink()
|
||||
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- attempt to establish link with supervisor
|
||||
function public.send_link_req()
|
||||
_send_mgmt(SCADA_MGMT_TYPES.ESTABLISH, { comms.version, version, DEVICE_TYPES.PLC, id })
|
||||
_send_mgmt(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, id })
|
||||
end
|
||||
|
||||
-- send live status information
|
||||
@ -664,7 +658,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
mek_data = self.status_cache
|
||||
end
|
||||
|
||||
heating_rate = self.reactor.getHeatingRate()
|
||||
heating_rate = reactor.getHeatingRate()
|
||||
end
|
||||
|
||||
local sys_status = {
|
||||
@ -677,35 +671,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
mek_data -- mekanism status data
|
||||
}
|
||||
|
||||
_send(RPLC_TYPES.STATUS, sys_status)
|
||||
_send(RPLC_TYPE.STATUS, sys_status)
|
||||
|
||||
if self.resend_build then
|
||||
_send_struct()
|
||||
end
|
||||
if self.resend_build then _send_struct() end
|
||||
end
|
||||
end
|
||||
|
||||
-- send reactor protection system status
|
||||
function public.send_rps_status()
|
||||
if self.linked then
|
||||
_send(RPLC_TYPES.RPS_STATUS, { rps.is_tripped(), rps.get_trip_cause(), table.unpack(rps.status()) })
|
||||
_send(RPLC_TYPE.RPS_STATUS, { rps.is_tripped(), rps.get_trip_cause(), table.unpack(rps.status()) })
|
||||
end
|
||||
end
|
||||
|
||||
-- send reactor protection system alarm
|
||||
---@param cause rps_status_t reactor protection system status
|
||||
---@param cause rps_trip_cause reactor protection system status
|
||||
function public.send_rps_alarm(cause)
|
||||
if self.linked then
|
||||
local rps_alarm = {
|
||||
cause,
|
||||
table.unpack(rps.status())
|
||||
}
|
||||
|
||||
_send(RPLC_TYPES.RPS_ALARM, rps_alarm)
|
||||
_send(RPLC_TYPE.RPS_ALARM, { cause, table.unpack(rps.status()) })
|
||||
end
|
||||
end
|
||||
|
||||
-- parse an RPLC packet
|
||||
---@nodiscard
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
@ -721,13 +709,13 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as RPLC packet
|
||||
if s_pkt.protocol() == PROTOCOLS.RPLC then
|
||||
if s_pkt.protocol() == PROTOCOL.RPLC then
|
||||
local rplc_pkt = comms.rplc_packet()
|
||||
if rplc_pkt.decode(s_pkt) then
|
||||
pkt = rplc_pkt.get()
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
@ -745,7 +733,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
---@param plc_state plc_state PLC state
|
||||
---@param setpoints setpoints setpoint control table
|
||||
function public.handle_packet(packet, plc_state, setpoints)
|
||||
if packet ~= nil and packet.scada_frame.local_port() == self.l_port then
|
||||
if packet.scada_frame.local_port() == local_port then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
@ -762,18 +750,19 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
-- handle packet
|
||||
if protocol == PROTOCOLS.RPLC then
|
||||
if protocol == PROTOCOL.RPLC then
|
||||
---@cast packet rplc_frame
|
||||
if self.linked then
|
||||
if packet.type == RPLC_TYPES.STATUS then
|
||||
if packet.type == RPLC_TYPE.STATUS then
|
||||
-- request of full status, clear cache first
|
||||
self.status_cache = nil
|
||||
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
log.debug("sent out status cache again, did supervisor miss it?")
|
||||
elseif packet.type == RPLC_TYPES.MEK_STRUCT then
|
||||
elseif packet.type == RPLC_TYPE.MEK_STRUCT then
|
||||
-- request for physical structure
|
||||
_send_struct()
|
||||
log.debug("sent out structure again, did supervisor miss it?")
|
||||
elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then
|
||||
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
|
||||
-- set the burn rate
|
||||
if (packet.length == 2) and (type(packet.data[1]) == "number") then
|
||||
local success = false
|
||||
@ -782,7 +771,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = self.reactor.getMaxBurnRate()
|
||||
self.max_burn_rate = reactor.getMaxBurnRate()
|
||||
end
|
||||
|
||||
-- if we know our max burn rate, update current burn rate setpoint if in range
|
||||
@ -793,8 +782,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
setpoints.burn_rate = burn_rate
|
||||
success = true
|
||||
else
|
||||
self.reactor.setBurnRate(burn_rate)
|
||||
success = not self.reactor.__p_is_faulted()
|
||||
reactor.setBurnRate(burn_rate)
|
||||
success = not reactor.__p_is_faulted()
|
||||
end
|
||||
else
|
||||
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
|
||||
@ -805,29 +794,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
else
|
||||
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
|
||||
end
|
||||
elseif packet.type == RPLC_TYPES.RPS_ENABLE then
|
||||
elseif packet.type == RPLC_TYPE.RPS_ENABLE then
|
||||
-- enable the reactor
|
||||
self.scrammed = false
|
||||
_send_ack(packet.type, rps.activate())
|
||||
elseif packet.type == RPLC_TYPES.RPS_SCRAM then
|
||||
elseif packet.type == RPLC_TYPE.RPS_SCRAM then
|
||||
-- disable the reactor per manual request
|
||||
self.scrammed = true
|
||||
rps.trip_manual()
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPES.RPS_ASCRAM then
|
||||
elseif packet.type == RPLC_TYPE.RPS_ASCRAM then
|
||||
-- disable the reactor per automatic request
|
||||
self.scrammed = true
|
||||
rps.trip_auto()
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPES.RPS_RESET then
|
||||
elseif packet.type == RPLC_TYPE.RPS_RESET then
|
||||
-- reset the RPS status
|
||||
rps.reset()
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPES.RPS_AUTO_RESET then
|
||||
elseif packet.type == RPLC_TYPE.RPS_AUTO_RESET then
|
||||
-- reset automatic SCRAM and timeout trips
|
||||
rps.auto_reset()
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPES.AUTO_BURN_RATE then
|
||||
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
|
||||
-- automatic control requested a new burn rate
|
||||
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
|
||||
local ack = AUTO_ACK.FAIL
|
||||
@ -837,7 +826,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
|
||||
-- if no known max burn rate, check again
|
||||
if self.max_burn_rate == nil then
|
||||
self.max_burn_rate = self.reactor.getMaxBurnRate()
|
||||
self.max_burn_rate = reactor.getMaxBurnRate()
|
||||
end
|
||||
|
||||
-- if we know our max burn rate, update current burn rate setpoint if in range
|
||||
@ -848,9 +837,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
|
||||
if rps.scram() then
|
||||
ack = AUTO_ACK.ZERO_DIS_OK
|
||||
self.auto_last_disable = util.time_ms()
|
||||
else
|
||||
log.debug("AUTO: automatic reactor stop failed")
|
||||
log.warning("AUTO: automatic reactor stop failed")
|
||||
end
|
||||
else
|
||||
ack = AUTO_ACK.ZERO_DIS_OK
|
||||
@ -860,12 +848,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
-- activate the reactor
|
||||
log.debug("AUTO: activating the reactor")
|
||||
|
||||
self.reactor.setBurnRate(0.01)
|
||||
if self.reactor.__p_is_faulted() then
|
||||
log.debug("AUTO: failed to reset burn rate for auto activation")
|
||||
reactor.setBurnRate(0.01)
|
||||
if reactor.__p_is_faulted() then
|
||||
log.warning("AUTO: failed to reset burn rate for auto activation")
|
||||
else
|
||||
if not rps.auto_activate() then
|
||||
log.debug("AUTO: automatic reactor activation failed")
|
||||
log.warning("AUTO: automatic reactor activation failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -879,8 +867,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
ack = AUTO_ACK.RAMP_SET_OK
|
||||
else
|
||||
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
|
||||
self.reactor.setBurnRate(burn_rate)
|
||||
ack = util.trinary(self.reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
|
||||
reactor.setBurnRate(burn_rate)
|
||||
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -898,9 +886,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
else
|
||||
log.debug("discarding RPLC packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if self.linked then
|
||||
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- link request confirmation
|
||||
if packet.length == 1 then
|
||||
log.debug("received unsolicited establish response")
|
||||
@ -933,7 +922,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 and type(packet.data[1]) == "number" then
|
||||
local timestamp = packet.data[1]
|
||||
@ -949,7 +938,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
else
|
||||
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
conn_watchdog.cancel()
|
||||
public.unlink()
|
||||
@ -958,14 +947,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
else
|
||||
log.warning("received unsupported SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- link request confirmation
|
||||
if packet.length == 1 then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
println_ts("linked!")
|
||||
log.debug("supervisor establish request approved")
|
||||
log.info("supervisor establish request approved, PLC is linked")
|
||||
|
||||
-- reset remote sequence number and cache
|
||||
self.r_seq_num = nil
|
||||
@ -978,16 +967,16 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
elseif self.last_est_ack ~= est_ack then
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
println_ts("link request denied, retrying...")
|
||||
log.debug("establish request denied")
|
||||
log.info("supervisor establish request denied, retrying")
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
println_ts("reactor PLC ID collision (check config), retrying...")
|
||||
log.warning("establish request collision")
|
||||
log.warning("establish request collision, retrying")
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
println_ts("supervisor version mismatch (try updating), retrying...")
|
||||
log.warning("establish request version mismatch")
|
||||
log.warning("establish request version mismatch, retrying")
|
||||
else
|
||||
println_ts("invalid link response, bad channel? retrying...")
|
||||
log.error("unknown establish request response")
|
||||
log.error("unknown establish request response, retrying")
|
||||
end
|
||||
end
|
||||
|
||||
@ -1006,7 +995,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
|
||||
end
|
||||
end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_scrammed() return self.scrammed end
|
||||
---@nodiscard
|
||||
function public.is_linked() return self.linked end
|
||||
|
||||
return public
|
||||
|
@ -14,7 +14,7 @@ local config = require("reactor-plc.config")
|
||||
local plc = require("reactor-plc.plc")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "beta-v0.11.1"
|
||||
local R_PLC_VERSION = "v0.12.1"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -116,15 +116,15 @@ local function main()
|
||||
|
||||
-- we need a reactor, can at least do some things even if it isn't formed though
|
||||
if smem_dev.reactor == nil then
|
||||
println("boot> fission reactor not found");
|
||||
log.warning("no reactor on startup")
|
||||
println("init> fission reactor not found");
|
||||
log.warning("init> no reactor on startup")
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
plc_state.no_reactor = true
|
||||
elseif not smem_dev.reactor.isFormed() then
|
||||
println("boot> fission reactor not formed");
|
||||
log.warning("reactor logic adapter present, but reactor is not formed")
|
||||
println("init> fission reactor not formed");
|
||||
log.warning("init> reactor logic adapter present, but reactor is not formed")
|
||||
|
||||
plc_state.degraded = true
|
||||
plc_state.reactor_formed = false
|
||||
@ -132,8 +132,8 @@ local function main()
|
||||
|
||||
-- modem is required if networked
|
||||
if __shared_memory.networked and smem_dev.modem == nil then
|
||||
println("boot> wireless modem not found")
|
||||
log.warning("no wireless modem on startup")
|
||||
println("init> wireless modem not found")
|
||||
log.warning("init> no wireless modem on startup")
|
||||
|
||||
-- scram reactor if present and enabled
|
||||
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
@ -145,8 +145,7 @@ local function main()
|
||||
plc_state.no_modem = true
|
||||
end
|
||||
|
||||
-- PLC init
|
||||
---
|
||||
-- PLC init<br>
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
local function init()
|
||||
if plc_state.init_ok then
|
||||
@ -169,18 +168,17 @@ local function main()
|
||||
config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
|
||||
log.debug("init> comms init")
|
||||
else
|
||||
println("boot> starting in offline mode")
|
||||
log.debug("init> running without networking")
|
||||
println("init> starting in offline mode")
|
||||
log.info("init> running without networking")
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
util.push_event("clock_start")
|
||||
|
||||
println("boot> completed")
|
||||
log.debug("init> boot completed")
|
||||
println("init> completed")
|
||||
log.info("init> startup completed")
|
||||
else
|
||||
println("boot> system in degraded state, awaiting devices...")
|
||||
log.warning("init> booted in a degraded state, awaiting peripheral connections...")
|
||||
println("init> system in degraded state, awaiting devices...")
|
||||
log.warning("init> started in a degraded state, awaiting peripheral connections...")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -28,10 +28,12 @@ local MQ__COMM_CMD = {
|
||||
}
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
---@param init function
|
||||
function threads.thread__main(smem, init)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
@ -44,9 +46,9 @@ function threads.thread__main(smem, init)
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- load in from shared memory
|
||||
local networked = smem.networked
|
||||
local plc_state = smem.plc_state
|
||||
local plc_dev = smem.plc_dev
|
||||
local networked = smem.networked
|
||||
local plc_state = smem.plc_state
|
||||
local plc_dev = smem.plc_dev
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
@ -266,7 +268,6 @@ function threads.thread__main(smem, init)
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
|
||||
if not plc_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
util.push_event("clock_start")
|
||||
end
|
||||
end
|
||||
@ -276,9 +277,11 @@ function threads.thread__main(smem, init)
|
||||
end
|
||||
|
||||
-- RPS operation thread
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__rps(smem)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
@ -297,10 +300,10 @@ function threads.thread__rps(smem)
|
||||
-- thread loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
-- get reactor, may have changed do to disconnect/reconnect
|
||||
local reactor = plc_dev.reactor
|
||||
local reactor = plc_dev.reactor
|
||||
|
||||
-- RPS checks
|
||||
if plc_state.init_ok then
|
||||
@ -415,9 +418,11 @@ function threads.thread__rps(smem)
|
||||
end
|
||||
|
||||
-- communications sender thread
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__comms_tx(smem)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
@ -489,9 +494,11 @@ function threads.thread__comms_tx(smem)
|
||||
end
|
||||
|
||||
-- communications handler thread
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__comms_rx(smem)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
@ -562,10 +569,12 @@ function threads.thread__comms_rx(smem)
|
||||
return public
|
||||
end
|
||||
|
||||
-- apply setpoints
|
||||
-- ramp control outputs to desired setpoints
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
function threads.thread__setpoint_control(smem)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
|
@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
|
||||
local boilerv_rtu = {}
|
||||
|
||||
-- create new boiler (mek 10.1+) device
|
||||
---@nodiscard
|
||||
---@param boiler table
|
||||
function boilerv_rtu.new(boiler)
|
||||
local unit = rtu.init_unit()
|
||||
|
@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
|
||||
local envd_rtu = {}
|
||||
|
||||
-- create new environment detector device
|
||||
---@nodiscard
|
||||
---@param envd table
|
||||
function envd_rtu.new(envd)
|
||||
local unit = rtu.init_unit()
|
||||
|
@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
|
||||
local imatrix_rtu = {}
|
||||
|
||||
-- create new induction matrix (mek 10.1+) device
|
||||
---@nodiscard
|
||||
---@param imatrix table
|
||||
function imatrix_rtu.new(imatrix)
|
||||
local unit = rtu.init_unit()
|
||||
|
@ -1,7 +1,7 @@
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local rsio = require("scada-common.rsio")
|
||||
|
||||
local rtu = require("rtu.rtu")
|
||||
|
||||
local redstone_rtu = {}
|
||||
|
||||
local IO_LVL = rsio.IO_LVL
|
||||
@ -10,14 +10,15 @@ local digital_read = rsio.digital_read
|
||||
local digital_write = rsio.digital_write
|
||||
|
||||
-- create new redstone device
|
||||
---@nodiscard
|
||||
function redstone_rtu.new()
|
||||
local unit = rtu.init_unit()
|
||||
|
||||
-- get RTU interface
|
||||
local interface = unit.interface()
|
||||
|
||||
-- extends rtu_device; fields added manually to please Lua diagnostics
|
||||
---@class rtu_rs_device
|
||||
--- extends rtu_device; fields added manually to please Lua diagnostics
|
||||
local public = {
|
||||
io_count = interface.io_count,
|
||||
read_coil = interface.read_coil,
|
||||
|
@ -2,7 +2,8 @@ local rtu = require("rtu.rtu")
|
||||
|
||||
local sna_rtu = {}
|
||||
|
||||
-- create new solar neutron activator (sna) device
|
||||
-- create new solar neutron activator (SNA) device
|
||||
---@nodiscard
|
||||
---@param sna table
|
||||
function sna_rtu.new(sna)
|
||||
local unit = rtu.init_unit()
|
||||
|
@ -2,7 +2,8 @@ local rtu = require("rtu.rtu")
|
||||
|
||||
local sps_rtu = {}
|
||||
|
||||
-- create new super-critical phase shifter (sps) device
|
||||
-- create new super-critical phase shifter (SPS) device
|
||||
---@nodiscard
|
||||
---@param sps table
|
||||
function sps_rtu.new(sps)
|
||||
local unit = rtu.init_unit()
|
||||
|
@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
|
||||
local turbinev_rtu = {}
|
||||
|
||||
-- create new turbine (mek 10.1+) device
|
||||
---@nodiscard
|
||||
---@param turbine table
|
||||
function turbinev_rtu.new(turbine)
|
||||
local unit = rtu.init_unit()
|
||||
|
@ -7,22 +7,15 @@ local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
local MODBUS_EXCODE = types.MODBUS_EXCODE
|
||||
|
||||
-- new modbus comms handler object
|
||||
---@nodiscard
|
||||
---@param rtu_dev rtu_device|rtu_rs_device RTU device
|
||||
---@param use_parallel_read boolean whether or not to use parallel calls when reading
|
||||
function modbus.new(rtu_dev, use_parallel_read)
|
||||
local self = {
|
||||
rtu = rtu_dev,
|
||||
use_parallel = use_parallel_read
|
||||
}
|
||||
|
||||
---@class modbus
|
||||
local public = {}
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
-- read a span of coils (digital outputs)
|
||||
--
|
||||
-- read a span of coils (digital outputs)<br>
|
||||
-- returns a table of readings or a MODBUS_EXCODE error code
|
||||
---@nodiscard
|
||||
---@param c_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table|MODBUS_EXCODE readings
|
||||
@ -30,20 +23,20 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
local tasks = {}
|
||||
local readings = {} ---@type table|MODBUS_EXCODE
|
||||
local access_fault = false
|
||||
local _, coils, _, _ = self.rtu.io_count()
|
||||
local _, coils, _, _ = rtu_dev.io_count()
|
||||
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = c_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_coil(addr)
|
||||
local reading, fault = rtu_dev.read_coil(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_coil(addr)
|
||||
readings[i], access_fault = rtu_dev.read_coil(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -54,7 +47,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
end
|
||||
|
||||
@ -69,9 +62,9 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
-- read a span of discrete inputs (digital inputs)
|
||||
--
|
||||
-- read a span of discrete inputs (digital inputs)<br>
|
||||
-- returns a table of readings or a MODBUS_EXCODE error code
|
||||
---@nodiscard
|
||||
---@param di_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table|MODBUS_EXCODE readings
|
||||
@ -79,20 +72,20 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
local tasks = {}
|
||||
local readings = {} ---@type table|MODBUS_EXCODE
|
||||
local access_fault = false
|
||||
local discrete_inputs, _, _, _ = self.rtu.io_count()
|
||||
local discrete_inputs, _, _, _ = rtu_dev.io_count()
|
||||
local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = di_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_di(addr)
|
||||
local reading, fault = rtu_dev.read_di(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_di(addr)
|
||||
readings[i], access_fault = rtu_dev.read_di(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -103,7 +96,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
end
|
||||
|
||||
@ -118,9 +111,9 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
-- read a span of holding registers (analog outputs)
|
||||
--
|
||||
-- read a span of holding registers (analog outputs)<br>
|
||||
-- returns a table of readings or a MODBUS_EXCODE error code
|
||||
---@nodiscard
|
||||
---@param hr_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table|MODBUS_EXCODE readings
|
||||
@ -128,20 +121,20 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
local tasks = {}
|
||||
local readings = {} ---@type table|MODBUS_EXCODE
|
||||
local access_fault = false
|
||||
local _, _, _, hold_regs = self.rtu.io_count()
|
||||
local _, _, _, hold_regs = rtu_dev.io_count()
|
||||
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = hr_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_holding_reg(addr)
|
||||
local reading, fault = rtu_dev.read_holding_reg(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_holding_reg(addr)
|
||||
readings[i], access_fault = rtu_dev.read_holding_reg(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -152,7 +145,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
end
|
||||
|
||||
@ -167,9 +160,9 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
return return_ok, readings
|
||||
end
|
||||
|
||||
-- read a span of input registers (analog inputs)
|
||||
--
|
||||
-- read a span of input registers (analog inputs)<br>
|
||||
-- returns a table of readings or a MODBUS_EXCODE error code
|
||||
---@nodiscard
|
||||
---@param ir_addr_start integer
|
||||
---@param count integer
|
||||
---@return boolean ok, table|MODBUS_EXCODE readings
|
||||
@ -177,20 +170,20 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
local tasks = {}
|
||||
local readings = {} ---@type table|MODBUS_EXCODE
|
||||
local access_fault = false
|
||||
local _, _, input_regs, _ = self.rtu.io_count()
|
||||
local _, _, input_regs, _ = rtu_dev.io_count()
|
||||
local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = ir_addr_start + i - 1
|
||||
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
insert(tasks, function ()
|
||||
local reading, fault = self.rtu.read_input_reg(addr)
|
||||
local reading, fault = rtu_dev.read_input_reg(addr)
|
||||
if fault then access_fault = true else readings[i] = reading end
|
||||
end)
|
||||
else
|
||||
readings[i], access_fault = self.rtu.read_input_reg(addr)
|
||||
readings[i], access_fault = rtu_dev.read_input_reg(addr)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -201,7 +194,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- run parallel tasks if configured
|
||||
if self.use_parallel then
|
||||
if use_parallel_read then
|
||||
parallel.waitForAll(table.unpack(tasks))
|
||||
end
|
||||
|
||||
@ -217,16 +210,17 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- write a single coil (digital output)
|
||||
---@nodiscard
|
||||
---@param c_addr integer
|
||||
---@param value any
|
||||
---@return boolean ok, MODBUS_EXCODE
|
||||
local function _5_write_single_coil(c_addr, value)
|
||||
local response = nil
|
||||
local _, coils, _, _ = self.rtu.io_count()
|
||||
local _, coils, _, _ = rtu_dev.io_count()
|
||||
local return_ok = c_addr <= coils
|
||||
|
||||
if return_ok then
|
||||
local access_fault = self.rtu.write_coil(c_addr, value)
|
||||
local access_fault = rtu_dev.write_coil(c_addr, value)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -240,16 +234,17 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- write a single holding register (analog output)
|
||||
---@nodiscard
|
||||
---@param hr_addr integer
|
||||
---@param value any
|
||||
---@return boolean ok, MODBUS_EXCODE
|
||||
local function _6_write_single_holding_register(hr_addr, value)
|
||||
local response = nil
|
||||
local _, _, _, hold_regs = self.rtu.io_count()
|
||||
local _, _, _, hold_regs = rtu_dev.io_count()
|
||||
local return_ok = hr_addr <= hold_regs
|
||||
|
||||
if return_ok then
|
||||
local access_fault = self.rtu.write_holding_reg(hr_addr, value)
|
||||
local access_fault = rtu_dev.write_holding_reg(hr_addr, value)
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -263,19 +258,20 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- write multiple coils (digital outputs)
|
||||
---@nodiscard
|
||||
---@param c_addr_start integer
|
||||
---@param values any
|
||||
---@return boolean ok, MODBUS_EXCODE
|
||||
local function _15_write_multiple_coils(c_addr_start, values)
|
||||
local response = nil
|
||||
local _, coils, _, _ = self.rtu.io_count()
|
||||
local _, coils, _, _ = rtu_dev.io_count()
|
||||
local count = #values
|
||||
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = c_addr_start + i - 1
|
||||
local access_fault = self.rtu.write_coil(addr, values[i])
|
||||
local access_fault = rtu_dev.write_coil(addr, values[i])
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -291,19 +287,20 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- write multiple holding registers (analog outputs)
|
||||
---@nodiscard
|
||||
---@param hr_addr_start integer
|
||||
---@param values any
|
||||
---@return boolean ok, MODBUS_EXCODE
|
||||
local function _16_write_multiple_holding_registers(hr_addr_start, values)
|
||||
local response = nil
|
||||
local _, _, _, hold_regs = self.rtu.io_count()
|
||||
local _, _, _, hold_regs = rtu_dev.io_count()
|
||||
local count = #values
|
||||
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
|
||||
|
||||
if return_ok then
|
||||
for i = 1, count do
|
||||
local addr = hr_addr_start + i - 1
|
||||
local access_fault = self.rtu.write_holding_reg(addr, values[i])
|
||||
local access_fault = rtu_dev.write_holding_reg(addr, values[i])
|
||||
|
||||
if access_fault then
|
||||
return_ok = false
|
||||
@ -318,7 +315,11 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
return return_ok, response
|
||||
end
|
||||
|
||||
---@class modbus
|
||||
local public = {}
|
||||
|
||||
-- validate a request without actually executing it
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.check_request(packet)
|
||||
@ -360,6 +361,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- handle a MODBUS TCP packet and generate a reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame
|
||||
---@return boolean return_code, modbus_packet reply
|
||||
function public.handle_packet(packet)
|
||||
@ -420,6 +422,7 @@ function modbus.new(rtu_dev, use_parallel_read)
|
||||
end
|
||||
|
||||
-- return a SERVER_DEVICE_BUSY error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__srv_device_busy(packet)
|
||||
@ -432,6 +435,7 @@ function modbus.reply__srv_device_busy(packet)
|
||||
end
|
||||
|
||||
-- return a NEG_ACKNOWLEDGE error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__neg_ack(packet)
|
||||
@ -444,6 +448,7 @@ function modbus.reply__neg_ack(packet)
|
||||
end
|
||||
|
||||
-- return a GATEWAY_PATH_UNAVAILABLE error reply
|
||||
---@nodiscard
|
||||
---@param packet modbus_frame MODBUS packet frame
|
||||
---@return modbus_packet reply
|
||||
function modbus.reply__gw_unavailable(packet)
|
||||
|
113
rtu/rtu.lua
113
rtu/rtu.lua
@ -1,24 +1,26 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local modbus = require("rtu.modbus")
|
||||
|
||||
local rtu = {}
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local DEVICE_TYPES = comms.DEVICE_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
-- create a new RTU
|
||||
-- create a new RTU unit
|
||||
---@nodiscard
|
||||
function rtu.init_unit()
|
||||
local self = {
|
||||
discrete_inputs = {},
|
||||
@ -152,14 +154,13 @@ function rtu.init_unit()
|
||||
-- public RTU device access
|
||||
|
||||
-- get the public interface to this RTU
|
||||
function protected.interface()
|
||||
return public
|
||||
end
|
||||
function protected.interface() return public end
|
||||
|
||||
return protected
|
||||
end
|
||||
|
||||
-- RTU Communications
|
||||
---@nodiscard
|
||||
---@param version string RTU version
|
||||
---@param modem table modem device
|
||||
---@param local_port integer local listening port
|
||||
@ -168,20 +169,12 @@ end
|
||||
---@param conn_watchdog watchdog watchdog reference
|
||||
function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog)
|
||||
local self = {
|
||||
version = version,
|
||||
seq_num = 0,
|
||||
r_seq_num = nil,
|
||||
txn_id = 0,
|
||||
modem = modem,
|
||||
s_port = server_port,
|
||||
l_port = local_port,
|
||||
conn_watchdog = conn_watchdog,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW
|
||||
}
|
||||
|
||||
---@class rtu_comms
|
||||
local public = {}
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
@ -190,50 +183,46 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
self.modem.closeAll()
|
||||
self.modem.open(self.l_port)
|
||||
modem.closeAll()
|
||||
modem.open(local_port)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
|
||||
-- send a scada management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- generate device advertisement table
|
||||
---@nodiscard
|
||||
---@param units table
|
||||
---@return table advertisement
|
||||
local function _generate_advertisement(units)
|
||||
local advertisement = {}
|
||||
|
||||
for i = 1, #units do
|
||||
local unit = units[i] --@type rtu_unit_registry_entry
|
||||
local type = comms.rtu_t_to_unit_type(unit.type)
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
|
||||
if type ~= nil then
|
||||
local advert = {
|
||||
type,
|
||||
unit.index,
|
||||
unit.reactor
|
||||
}
|
||||
if unit.type ~= nil then
|
||||
local advert = { unit.type, unit.index, unit.reactor }
|
||||
|
||||
if type == RTU_UNIT_TYPES.REDSTONE then
|
||||
if unit.type == RTU_UNIT_TYPE.REDSTONE then
|
||||
insert(advert, unit.device)
|
||||
end
|
||||
|
||||
@ -246,20 +235,22 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class rtu_comms
|
||||
local public = {}
|
||||
|
||||
-- send a MODBUS TCP packet
|
||||
---@param m_pkt modbus_packet
|
||||
function public.send_modbus(m_pkt)
|
||||
local s_pkt = comms.scada_packet()
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
modem.transmit(server_port, local_port, s_pkt.raw_sendable())
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
@ -273,30 +264,31 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
-- close the connection to the server
|
||||
---@param rtu_state rtu_state
|
||||
function public.close(rtu_state)
|
||||
self.conn_watchdog.cancel()
|
||||
conn_watchdog.cancel()
|
||||
public.unlink(rtu_state)
|
||||
_send(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
_send(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- send establish request (includes advertisement)
|
||||
---@param units table
|
||||
function public.send_establish(units)
|
||||
_send(SCADA_MGMT_TYPES.ESTABLISH, { comms.version, self.version, DEVICE_TYPES.RTU, _generate_advertisement(units) })
|
||||
_send(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) })
|
||||
end
|
||||
|
||||
-- send capability advertisement
|
||||
---@param units table
|
||||
function public.send_advertisement(units)
|
||||
_send(SCADA_MGMT_TYPES.RTU_ADVERT, _generate_advertisement(units))
|
||||
_send(SCADA_MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units))
|
||||
end
|
||||
|
||||
-- notify that a peripheral was remounted
|
||||
---@param unit_index integer RTU unit ID
|
||||
function public.send_remounted(unit_index)
|
||||
_send(SCADA_MGMT_TYPES.RTU_DEV_REMOUNT, { unit_index })
|
||||
_send(SCADA_MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index })
|
||||
end
|
||||
|
||||
-- parse a MODBUS/SCADA packet
|
||||
---@nodiscard
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
@ -312,13 +304,13 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as MODBUS TCP packet
|
||||
if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
local m_pkt = comms.modbus_packet()
|
||||
if m_pkt.decode(s_pkt) then
|
||||
pkt = m_pkt.get()
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
@ -333,10 +325,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
|
||||
-- handle a MODBUS/SCADA packet
|
||||
---@param packet modbus_frame|mgmt_frame
|
||||
---@param units table
|
||||
---@param units table RTU units
|
||||
---@param rtu_state rtu_state
|
||||
function public.handle_packet(packet, units, rtu_state)
|
||||
if packet ~= nil and packet.scada_frame.local_port() == self.l_port then
|
||||
if packet.scada_frame.local_port() == local_port then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
self.r_seq_num = packet.scada_frame.seq_num()
|
||||
@ -348,14 +340,14 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
end
|
||||
|
||||
-- feed watchdog on valid sequence number
|
||||
self.conn_watchdog.feed()
|
||||
conn_watchdog.feed()
|
||||
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if protocol == PROTOCOLS.MODBUS_TCP then
|
||||
if protocol == PROTOCOL.MODBUS_TCP then
|
||||
---@cast packet modbus_frame
|
||||
if rtu_state.linked then
|
||||
local return_code = false
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
local reply = modbus.reply__neg_ack(packet)
|
||||
|
||||
-- handle MODBUS instruction
|
||||
@ -365,20 +357,17 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
|
||||
if unit.name == "redstone_io" then
|
||||
-- immediately execute redstone RTU requests
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
return_code, reply = unit.modbus_io.handle_packet(packet)
|
||||
if not return_code then
|
||||
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
|
||||
end
|
||||
else
|
||||
-- check validity then pass off to unit comms thread
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
return_code, reply = unit.modbus_io.check_request(packet)
|
||||
if return_code then
|
||||
-- check if there are more than 3 active transactions
|
||||
-- still queue the packet, but this may indicate a problem
|
||||
if unit.pkt_queue.length() > 3 then
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
reply = modbus.reply__srv_device_busy(packet)
|
||||
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
|
||||
" transactions already in the queue" .. unit_dbg_tag)
|
||||
@ -392,7 +381,6 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
end
|
||||
else
|
||||
-- unit ID out of range?
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
reply = modbus.reply__gw_unavailable(packet)
|
||||
log.error("received MODBUS packet for non-existent unit")
|
||||
end
|
||||
@ -401,9 +389,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
else
|
||||
log.debug("discarding MODBUS packet before linked")
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
-- SCADA management packet
|
||||
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
if packet.length == 1 then
|
||||
local est_ack = packet.data[1]
|
||||
|
||||
@ -419,10 +408,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
if est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
-- version mismatch
|
||||
println_ts("supervisor comms version mismatch (try updating), retrying...")
|
||||
log.warning("supervisor connection denied due to comms version mismatch")
|
||||
log.warning("supervisor connection denied due to comms version mismatch, retrying")
|
||||
else
|
||||
println_ts("supervisor connection denied, retrying...")
|
||||
log.warning("supervisor connection denied")
|
||||
log.warning("supervisor connection denied, retrying")
|
||||
end
|
||||
end
|
||||
|
||||
@ -434,7 +423,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
elseif rtu_state.linked then
|
||||
if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 and type(packet.data[1]) == "number" then
|
||||
local timestamp = packet.data[1]
|
||||
@ -450,15 +439,15 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
|
||||
else
|
||||
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- close connection
|
||||
self.conn_watchdog.cancel()
|
||||
conn_watchdog.cancel()
|
||||
public.unlink(rtu_state)
|
||||
println_ts("server connection closed by remote host")
|
||||
log.warning("server connection closed by remote host")
|
||||
elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then
|
||||
-- request for capabilities again
|
||||
public.send_advertisement(units)
|
||||
public.send_advertisement(units)
|
||||
else
|
||||
-- not supported
|
||||
log.warning("received unsupported SCADA_MGMT message type " .. packet.type)
|
||||
|
103
rtu/startup.lua
103
rtu/startup.lua
@ -25,9 +25,9 @@ local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "beta-v0.11.2"
|
||||
local RTU_VERSION = "v0.12.1"
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -132,13 +132,17 @@ local function main()
|
||||
|
||||
-- CHECK: reactor ID must be >= to 1
|
||||
if (not util.is_int(io_reactor)) or (io_reactor < 0) then
|
||||
println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0"))
|
||||
local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: io table exists
|
||||
if type(io_table) ~= "table" then
|
||||
println(util.c("configure> redstone entry #", entry_idx, " no IO table found"))
|
||||
local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
@ -148,10 +152,10 @@ local function main()
|
||||
|
||||
local continue = true
|
||||
|
||||
-- check for duplicate entries
|
||||
-- CHECK: no duplicate entries
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type rtu_unit_registry_entry
|
||||
if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
|
||||
if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
|
||||
-- duplicate entry
|
||||
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
|
||||
" with already defined redstone I/O")
|
||||
@ -181,7 +185,7 @@ local function main()
|
||||
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
|
||||
" (for reactor ", io_reactor, ")")
|
||||
println(message)
|
||||
log.error(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
else
|
||||
-- link redstone in RTU
|
||||
@ -224,23 +228,28 @@ local function main()
|
||||
|
||||
---@class rtu_unit_registry_entry
|
||||
local unit = {
|
||||
uid = 0,
|
||||
name = "redstone_io",
|
||||
type = rtu_t.redstone,
|
||||
index = entry_idx,
|
||||
reactor = io_reactor,
|
||||
device = capabilities, -- use device field for redstone ports
|
||||
is_multiblock = false,
|
||||
formed = nil, ---@type boolean|nil
|
||||
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
|
||||
uid = 0, ---@type integer
|
||||
name = "redstone_io", ---@type string
|
||||
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
|
||||
index = entry_idx, ---@type integer
|
||||
reactor = io_reactor, ---@type integer
|
||||
device = capabilities, ---@type table use device field for redstone ports
|
||||
is_multiblock = false, ---@type boolean
|
||||
formed = nil, ---@type boolean|nil
|
||||
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
|
||||
modbus_io = modbus.new(rs_rtu, false),
|
||||
pkt_queue = nil, ---@type mqueue|nil
|
||||
thread = nil
|
||||
pkt_queue = nil, ---@type mqueue|nil
|
||||
thread = nil ---@type parallel_thread|nil
|
||||
}
|
||||
|
||||
table.insert(units, unit)
|
||||
|
||||
log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor))
|
||||
local for_message = "facility"
|
||||
if io_reactor > 0 then
|
||||
for_message = util.c("reactor ", io_reactor)
|
||||
end
|
||||
|
||||
log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
|
||||
|
||||
unit.uid = #units
|
||||
end
|
||||
@ -254,27 +263,33 @@ local function main()
|
||||
|
||||
-- CHECK: name is a string
|
||||
if type(name) ~= "string" then
|
||||
println(util.c("configure> device entry #", i, ": device ", name, " isn't a string"))
|
||||
local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: index is an integer >= 1
|
||||
if (not util.is_int(index)) or (index <= 0) then
|
||||
println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1"))
|
||||
local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
-- CHECK: reactor is an integer >= 0
|
||||
if (not util.is_int(for_reactor)) or (for_reactor < 0) then
|
||||
println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0"))
|
||||
local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0")
|
||||
println(message)
|
||||
log.fatal(message)
|
||||
return false
|
||||
end
|
||||
|
||||
local device = ppm.get_periph(name)
|
||||
|
||||
local type = nil
|
||||
local type = nil ---@type string|nil
|
||||
local rtu_iface = nil ---@type rtu_device
|
||||
local rtu_type = ""
|
||||
local rtu_type = nil ---@type RTU_UNIT_TYPE
|
||||
local is_multiblock = false
|
||||
local formed = nil ---@type boolean|nil
|
||||
|
||||
@ -291,7 +306,7 @@ local function main()
|
||||
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
rtu_type = rtu_t.boiler_valve
|
||||
rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
rtu_iface = boilerv_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
@ -303,7 +318,7 @@ local function main()
|
||||
end
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
rtu_type = rtu_t.turbine_valve
|
||||
rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
rtu_iface = turbinev_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
@ -315,7 +330,7 @@ local function main()
|
||||
end
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock
|
||||
rtu_type = rtu_t.induction_matrix
|
||||
rtu_type = RTU_UNIT_TYPE.IMATRIX
|
||||
rtu_iface = imatrix_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
@ -327,7 +342,7 @@ local function main()
|
||||
end
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
rtu_type = rtu_t.sps
|
||||
rtu_type = RTU_UNIT_TYPE.SPS
|
||||
rtu_iface = sps_rtu.new(device)
|
||||
is_multiblock = true
|
||||
formed = device.isFormed()
|
||||
@ -339,15 +354,15 @@ local function main()
|
||||
end
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
rtu_type = rtu_t.sna
|
||||
rtu_type = RTU_UNIT_TYPE.SNA
|
||||
rtu_iface = sna_rtu.new(device)
|
||||
elseif type == "environmentDetector" then
|
||||
-- advanced peripherals environment detector
|
||||
rtu_type = rtu_t.env_detector
|
||||
rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
rtu_iface = envd_rtu.new(device)
|
||||
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
|
||||
-- placeholder device
|
||||
rtu_type = "virtual"
|
||||
rtu_type = RTU_UNIT_TYPE.VIRTUAL
|
||||
rtu_iface = rtu.init_unit().interface()
|
||||
else
|
||||
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
|
||||
@ -358,18 +373,18 @@ local function main()
|
||||
|
||||
---@class rtu_unit_registry_entry
|
||||
local rtu_unit = {
|
||||
uid = 0,
|
||||
name = name,
|
||||
type = rtu_type,
|
||||
index = index,
|
||||
reactor = for_reactor,
|
||||
device = device,
|
||||
is_multiblock = is_multiblock,
|
||||
uid = 0, ---@type integer
|
||||
name = name, ---@type string
|
||||
type = rtu_type, ---@type RTU_UNIT_TYPE
|
||||
index = index, ---@type integer
|
||||
reactor = for_reactor, ---@type integer
|
||||
device = device, ---@type table
|
||||
is_multiblock = is_multiblock, ---@type boolean
|
||||
formed = formed, ---@type boolean|nil
|
||||
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
|
||||
modbus_io = modbus.new(rtu_iface, true),
|
||||
pkt_queue = mqueue.new(), ---@type mqueue|nil
|
||||
thread = nil
|
||||
thread = nil ---@type parallel_thread|nil
|
||||
}
|
||||
|
||||
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
|
||||
@ -377,7 +392,7 @@ local function main()
|
||||
table.insert(units, rtu_unit)
|
||||
|
||||
if is_multiblock and not formed then
|
||||
log.debug(util.c("configure> device '", name, "' is not formed"))
|
||||
log.info(util.c("configure> device '", name, "' is not formed"))
|
||||
end
|
||||
|
||||
local for_message = "facility"
|
||||
@ -385,7 +400,7 @@ local function main()
|
||||
for_message = util.c("reactor ", for_reactor)
|
||||
end
|
||||
|
||||
log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for ", for_message))
|
||||
log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
|
||||
|
||||
rtu_unit.uid = #units
|
||||
end
|
||||
@ -403,12 +418,12 @@ local function main()
|
||||
if configure() then
|
||||
-- start connection watchdog
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
|
||||
log.debug("boot> conn watchdog started")
|
||||
log.debug("startup> conn watchdog started")
|
||||
|
||||
-- setup comms
|
||||
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT,
|
||||
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
|
||||
log.debug("boot> comms init")
|
||||
log.debug("startup> comms init")
|
||||
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
@ -422,6 +437,8 @@ local function main()
|
||||
end
|
||||
end
|
||||
|
||||
log.info("startup> completed")
|
||||
|
||||
-- run threads
|
||||
parallel.waitForAll(table.unpack(_threads))
|
||||
else
|
||||
|
105
rtu/threads.lua
105
rtu/threads.lua
@ -1,21 +1,21 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
||||
local envd_rtu = require("rtu.dev.envd_rtu")
|
||||
local imatrix_rtu = require("rtu.dev.imatrix_rtu")
|
||||
local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local modbus = require("rtu.modbus")
|
||||
local modbus = require("rtu.modbus")
|
||||
|
||||
local threads = {}
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -26,9 +26,11 @@ local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
|
||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem rtu_shared_memory
|
||||
function threads.thread__main(smem)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
@ -93,8 +95,9 @@ function threads.thread__main(smem)
|
||||
-- we are going to let the PPM prevent crashes
|
||||
-- return fault flags/codes to MODBUS queries
|
||||
local unit = units[i]
|
||||
println_ts(util.c("lost the ", unit.type, " on interface ", unit.name))
|
||||
log.warning(util.c("lost the ", unit.type, " unit peripheral on interface ", unit.name))
|
||||
local type_name = types.rtu_type_to_string(unit.type)
|
||||
println_ts(util.c("lost the ", type_name, " on interface ", unit.name))
|
||||
log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name))
|
||||
break
|
||||
end
|
||||
end
|
||||
@ -112,9 +115,9 @@ function threads.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
|
||||
@ -129,51 +132,51 @@ function threads.thread__main(smem)
|
||||
-- found, re-link
|
||||
unit.device = device
|
||||
|
||||
if unit.type == "virtual" then
|
||||
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
||||
resend_advert = true
|
||||
if type == "boilerValve" then
|
||||
-- boiler multiblock
|
||||
unit.type = rtu_t.boiler_valve
|
||||
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
|
||||
elseif type == "turbineValve" then
|
||||
-- turbine multiblock
|
||||
unit.type = rtu_t.turbine_valve
|
||||
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
|
||||
elseif type == "inductionPort" then
|
||||
-- induction matrix multiblock
|
||||
unit.type = rtu_t.induction_matrix
|
||||
unit.type = RTU_UNIT_TYPE.IMATRIX
|
||||
elseif type == "spsPort" then
|
||||
-- SPS multiblock
|
||||
unit.type = rtu_t.sps
|
||||
unit.type = RTU_UNIT_TYPE.SPS
|
||||
elseif type == "solarNeutronActivator" then
|
||||
-- SNA
|
||||
unit.type = rtu_t.sna
|
||||
unit.type = RTU_UNIT_TYPE.SNA
|
||||
elseif type == "environmentDetector" then
|
||||
-- advanced peripherals environment detector
|
||||
unit.type = rtu_t.env_detector
|
||||
unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
|
||||
else
|
||||
resend_advert = false
|
||||
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
|
||||
end
|
||||
end
|
||||
|
||||
if unit.type == rtu_t.boiler_valve then
|
||||
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
unit.rtu = boilerv_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||
elseif unit.type == rtu_t.turbine_valve then
|
||||
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
unit.rtu = turbinev_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||
elseif unit.type == rtu_t.induction_matrix then
|
||||
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
unit.rtu = imatrix_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||
elseif unit.type == rtu_t.sps then
|
||||
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
||||
unit.rtu = sps_rtu.new(device)
|
||||
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
|
||||
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
|
||||
elseif unit.type == rtu_t.sna then
|
||||
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
||||
unit.rtu = sna_rtu.new(device)
|
||||
elseif unit.type == rtu_t.env_detector then
|
||||
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
unit.rtu = envd_rtu.new(device)
|
||||
else
|
||||
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
||||
@ -185,8 +188,10 @@ function threads.thread__main(smem)
|
||||
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
|
||||
println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name)
|
||||
log.info("reconnected the " .. unit.type .. " on interface " .. unit.name)
|
||||
local type_name = types.rtu_type_to_string(unit.type)
|
||||
local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
|
||||
println_ts(message)
|
||||
log.info(message)
|
||||
|
||||
if resend_advert then
|
||||
rtu_comms.send_advertisement(units)
|
||||
@ -229,22 +234,24 @@ function threads.thread__main(smem)
|
||||
end
|
||||
|
||||
-- communications handler thread
|
||||
---@nodiscard
|
||||
---@param smem rtu_shared_memory
|
||||
function threads.thread__comms(smem)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("comms thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local units = smem.rtu_sys.units
|
||||
local rtu_state = smem.rtu_state
|
||||
local rtu_comms = smem.rtu_sys.rtu_comms
|
||||
local units = smem.rtu_sys.units
|
||||
|
||||
local comms_queue = smem.q.mq_comms
|
||||
local comms_queue = smem.q.mq_comms
|
||||
|
||||
local last_update = util.time()
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
@ -301,14 +308,16 @@ function threads.thread__comms(smem)
|
||||
end
|
||||
|
||||
-- per-unit communications handler thread
|
||||
---@nodiscard
|
||||
---@param smem rtu_shared_memory
|
||||
---@param unit rtu_unit_registry_entry
|
||||
function threads.thread__unit_comms(smem, unit)
|
||||
local public = {} ---@class thread
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
log.debug("rtu unit thread start -> " .. unit.type .. "(" .. unit.name .. ")")
|
||||
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), "(", unit.name, ")"))
|
||||
|
||||
-- load in from shared memory
|
||||
local rtu_state = smem.rtu_state
|
||||
@ -319,8 +328,8 @@ function threads.thread__unit_comms(smem, unit)
|
||||
|
||||
local last_f_check = 0
|
||||
|
||||
local detail_name = util.c(unit.type, " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor)
|
||||
local short_name = util.c(unit.type, " (", unit.name, ")")
|
||||
local detail_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor)
|
||||
local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
|
||||
|
||||
if packet_queue == nil then
|
||||
log.error("rtu unit thread created without a message queue, exiting...", true)
|
||||
@ -368,25 +377,25 @@ function threads.thread__unit_comms(smem, unit)
|
||||
local type, device = ppm.mount(iface)
|
||||
|
||||
if device ~= nil then
|
||||
if type == "boilerValve" and unit.type == rtu_t.boiler_valve then
|
||||
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
-- boiler multiblock
|
||||
unit.device = device
|
||||
unit.rtu = boilerv_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "turbineValve" and unit.type == rtu_t.turbine_valve then
|
||||
elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
-- turbine multiblock
|
||||
unit.device = device
|
||||
unit.rtu = turbinev_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "inductionPort" and unit.type == rtu_t.induction_matrix then
|
||||
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
|
||||
-- induction matrix multiblock
|
||||
unit.device = device
|
||||
unit.rtu = imatrix_rtu.new(device)
|
||||
unit.formed = device.isFormed()
|
||||
unit.modbus_io = modbus.new(unit.rtu, true)
|
||||
elseif type == "spsPort" and unit.type == rtu_t.sps then
|
||||
elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
|
||||
-- SPS multiblock
|
||||
unit.device = device
|
||||
unit.rtu = sps_rtu.new(device)
|
||||
@ -433,7 +442,7 @@ function threads.thread__unit_comms(smem, unit)
|
||||
end
|
||||
|
||||
if not rtu_state.shutdown then
|
||||
log.info(util.c("rtu unit thread ", unit.type, "(", unit.name, " restarting in 5 seconds..."))
|
||||
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), "(", unit.name, " restarting in 5 seconds..."))
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
|
@ -3,21 +3,18 @@
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
---@class comms
|
||||
local comms = {}
|
||||
|
||||
local rtu_t = types.rtu_t
|
||||
|
||||
local insert = table.insert
|
||||
|
||||
local max_distance = nil
|
||||
|
||||
comms.version = "1.4.0"
|
||||
|
||||
---@alias PROTOCOLS integer
|
||||
local PROTOCOLS = {
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
|
||||
RPLC = 1, -- reactor PLC protocol
|
||||
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
|
||||
@ -25,8 +22,8 @@ local PROTOCOLS = {
|
||||
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
|
||||
}
|
||||
|
||||
---@alias RPLC_TYPES integer
|
||||
local RPLC_TYPES = {
|
||||
---@enum RPLC_TYPE
|
||||
local RPLC_TYPE = {
|
||||
STATUS = 0, -- reactor/system status
|
||||
MEK_STRUCT = 1, -- mekanism build structure
|
||||
MEK_BURN_RATE = 2, -- set burn rate
|
||||
@ -40,8 +37,8 @@ local RPLC_TYPES = {
|
||||
AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited
|
||||
}
|
||||
|
||||
---@alias SCADA_MGMT_TYPES integer
|
||||
local SCADA_MGMT_TYPES = {
|
||||
---@enum SCADA_MGMT_TYPE
|
||||
local SCADA_MGMT_TYPE = {
|
||||
ESTABLISH = 0, -- establish new connection
|
||||
KEEP_ALIVE = 1, -- keep alive packet w/ RTT
|
||||
CLOSE = 2, -- close a connection
|
||||
@ -49,8 +46,8 @@ local SCADA_MGMT_TYPES = {
|
||||
RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
|
||||
}
|
||||
|
||||
---@alias SCADA_CRDN_TYPES integer
|
||||
local SCADA_CRDN_TYPES = {
|
||||
---@enum SCADA_CRDN_TYPE
|
||||
local SCADA_CRDN_TYPE = {
|
||||
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
|
||||
FAC_BUILDS = 1, -- facility RTU builds
|
||||
FAC_STATUS = 2, -- state of facility and facility devices
|
||||
@ -60,12 +57,11 @@ local SCADA_CRDN_TYPES = {
|
||||
UNIT_CMD = 6 -- command a reactor unit
|
||||
}
|
||||
|
||||
---@alias CAPI_TYPES integer
|
||||
local CAPI_TYPES = {
|
||||
ESTABLISH = 0 -- initial greeting
|
||||
---@enum CAPI_TYPE
|
||||
local CAPI_TYPE = {
|
||||
}
|
||||
|
||||
---@alias ESTABLISH_ACK integer
|
||||
---@enum ESTABLISH_ACK
|
||||
local ESTABLISH_ACK = {
|
||||
ALLOW = 0, -- link approved
|
||||
DENY = 1, -- link denied
|
||||
@ -73,26 +69,15 @@ local ESTABLISH_ACK = {
|
||||
BAD_VERSION = 3 -- link denied due to comms version mismatch
|
||||
}
|
||||
|
||||
---@alias DEVICE_TYPES integer
|
||||
local DEVICE_TYPES = {
|
||||
---@enum DEVICE_TYPE
|
||||
local DEVICE_TYPE = {
|
||||
PLC = 0, -- PLC device type for establish
|
||||
RTU = 1, -- RTU device type for establish
|
||||
SV = 2, -- supervisor device type for establish
|
||||
CRDN = 3 -- coordinator device type for establish
|
||||
}
|
||||
|
||||
---@alias RTU_UNIT_TYPES integer
|
||||
local RTU_UNIT_TYPES = {
|
||||
REDSTONE = 0, -- redstone I/O
|
||||
BOILER_VALVE = 1, -- boiler mekanism 10.1+
|
||||
TURBINE_VALVE = 2, -- turbine, mekanism 10.1+
|
||||
IMATRIX = 3, -- induction matrix
|
||||
SPS = 4, -- SPS
|
||||
SNA = 5, -- SNA
|
||||
ENV_DETECTOR = 6 -- environment detector
|
||||
}
|
||||
|
||||
---@alias PLC_AUTO_ACK integer
|
||||
---@enum PLC_AUTO_ACK
|
||||
local PLC_AUTO_ACK = {
|
||||
FAIL = 0, -- failed to set burn rate/burn rate invalid
|
||||
DIRECT_SET_OK = 1, -- successfully set burn rate
|
||||
@ -100,16 +85,16 @@ local PLC_AUTO_ACK = {
|
||||
ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate
|
||||
}
|
||||
|
||||
---@alias FAC_COMMANDS integer
|
||||
local FAC_COMMANDS = {
|
||||
---@enum FAC_COMMAND
|
||||
local FAC_COMMAND = {
|
||||
SCRAM_ALL = 0, -- SCRAM all reactors
|
||||
STOP = 1, -- stop automatic control
|
||||
START = 2, -- start automatic control
|
||||
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units
|
||||
}
|
||||
|
||||
---@alias UNIT_COMMANDS integer
|
||||
local UNIT_COMMANDS = {
|
||||
---@enum UNIT_COMMAND
|
||||
local UNIT_COMMAND = {
|
||||
SCRAM = 0, -- SCRAM the reactor
|
||||
START = 1, -- start the reactor
|
||||
RESET_RPS = 2, -- reset the RPS
|
||||
@ -121,26 +106,25 @@ local UNIT_COMMANDS = {
|
||||
SET_GROUP = 8 -- assign this unit to a group
|
||||
}
|
||||
|
||||
comms.PROTOCOLS = PROTOCOLS
|
||||
comms.PROTOCOL = PROTOCOL
|
||||
|
||||
comms.RPLC_TYPES = RPLC_TYPES
|
||||
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
|
||||
comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES
|
||||
comms.CAPI_TYPES = CAPI_TYPES
|
||||
comms.RPLC_TYPE = RPLC_TYPE
|
||||
comms.SCADA_MGMT_TYPE = SCADA_MGMT_TYPE
|
||||
comms.SCADA_CRDN_TYPE = SCADA_CRDN_TYPE
|
||||
comms.CAPI_TYPE = CAPI_TYPE
|
||||
|
||||
comms.ESTABLISH_ACK = ESTABLISH_ACK
|
||||
comms.DEVICE_TYPES = DEVICE_TYPES
|
||||
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
|
||||
comms.DEVICE_TYPE = DEVICE_TYPE
|
||||
|
||||
comms.PLC_AUTO_ACK = PLC_AUTO_ACK
|
||||
|
||||
comms.UNIT_COMMANDS = UNIT_COMMANDS
|
||||
comms.FAC_COMMANDS = FAC_COMMANDS
|
||||
comms.UNIT_COMMAND = UNIT_COMMAND
|
||||
comms.FAC_COMMAND = FAC_COMMAND
|
||||
|
||||
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet
|
||||
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame
|
||||
|
||||
-- configure the maximum allowable message receive distance <br/>
|
||||
-- configure the maximum allowable message receive distance<br>
|
||||
-- packets received with distances greater than this will be silently discarded
|
||||
---@param distance integer max modem message distance (less than 1 disables the limit)
|
||||
function comms.set_trusted_range(distance)
|
||||
@ -152,6 +136,7 @@ function comms.set_trusted_range(distance)
|
||||
end
|
||||
|
||||
-- generic SCADA packet object
|
||||
---@nodiscard
|
||||
function comms.scada_packet()
|
||||
local self = {
|
||||
modem_msg_in = nil,
|
||||
@ -168,7 +153,7 @@ function comms.scada_packet()
|
||||
|
||||
-- make a SCADA packet
|
||||
---@param seq_num integer
|
||||
---@param protocol PROTOCOLS
|
||||
---@param protocol PROTOCOL
|
||||
---@param payload table
|
||||
function public.make(seq_num, protocol, payload)
|
||||
self.valid = true
|
||||
@ -180,11 +165,12 @@ function comms.scada_packet()
|
||||
end
|
||||
|
||||
-- parse in a modem message as a SCADA packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@param side string modem side
|
||||
---@param sender integer sender port
|
||||
---@param reply_to integer reply port
|
||||
---@param message any message body
|
||||
---@param distance integer transmission distance
|
||||
---@return boolean valid valid message received
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
self.modem_msg_in = {
|
||||
iface = side,
|
||||
@ -223,24 +209,34 @@ function comms.scada_packet()
|
||||
|
||||
-- public accessors --
|
||||
|
||||
---@nodiscard
|
||||
function public.modem_event() return self.modem_msg_in end
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
---@nodiscard
|
||||
function public.local_port() return self.modem_msg_in.s_port end
|
||||
---@nodiscard
|
||||
function public.remote_port() return self.modem_msg_in.r_port end
|
||||
|
||||
---@nodiscard
|
||||
function public.is_valid() return self.valid end
|
||||
|
||||
---@nodiscard
|
||||
function public.seq_num() return self.seq_num end
|
||||
---@nodiscard
|
||||
function public.protocol() return self.protocol end
|
||||
---@nodiscard
|
||||
function public.length() return self.length end
|
||||
---@nodiscard
|
||||
function public.data() return self.payload end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- MODBUS packet
|
||||
-- MODBUS packet<br>
|
||||
-- modeled after MODBUS TCP packet
|
||||
---@nodiscard
|
||||
function comms.modbus_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
@ -248,7 +244,7 @@ function comms.modbus_packet()
|
||||
txn_id = -1,
|
||||
length = 0,
|
||||
unit_id = -1,
|
||||
func_code = 0,
|
||||
func_code = 0x80,
|
||||
data = {}
|
||||
}
|
||||
|
||||
@ -285,7 +281,7 @@ function comms.modbus_packet()
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
if frame.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
local size_ok = frame.length() >= 3
|
||||
|
||||
if size_ok then
|
||||
@ -309,9 +305,11 @@ function comms.modbus_packet()
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class modbus_frame
|
||||
local frame = {
|
||||
@ -330,12 +328,13 @@ function comms.modbus_packet()
|
||||
end
|
||||
|
||||
-- reactor PLC packet
|
||||
---@nodiscard
|
||||
function comms.rplc_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = {},
|
||||
id = 0,
|
||||
type = -1,
|
||||
type = 0, ---@type RPLC_TYPE
|
||||
length = 0,
|
||||
data = {}
|
||||
}
|
||||
@ -345,22 +344,22 @@ function comms.rplc_packet()
|
||||
|
||||
-- check that type is known
|
||||
local function _rplc_type_valid()
|
||||
return self.type == RPLC_TYPES.STATUS or
|
||||
self.type == RPLC_TYPES.MEK_STRUCT or
|
||||
self.type == RPLC_TYPES.MEK_BURN_RATE or
|
||||
self.type == RPLC_TYPES.RPS_ENABLE or
|
||||
self.type == RPLC_TYPES.RPS_SCRAM or
|
||||
self.type == RPLC_TYPES.RPS_ASCRAM or
|
||||
self.type == RPLC_TYPES.RPS_STATUS or
|
||||
self.type == RPLC_TYPES.RPS_ALARM or
|
||||
self.type == RPLC_TYPES.RPS_RESET or
|
||||
self.type == RPLC_TYPES.RPS_AUTO_RESET or
|
||||
self.type == RPLC_TYPES.AUTO_BURN_RATE
|
||||
return self.type == RPLC_TYPE.STATUS or
|
||||
self.type == RPLC_TYPE.MEK_STRUCT or
|
||||
self.type == RPLC_TYPE.MEK_BURN_RATE or
|
||||
self.type == RPLC_TYPE.RPS_ENABLE or
|
||||
self.type == RPLC_TYPE.RPS_SCRAM or
|
||||
self.type == RPLC_TYPE.RPS_ASCRAM or
|
||||
self.type == RPLC_TYPE.RPS_STATUS or
|
||||
self.type == RPLC_TYPE.RPS_ALARM or
|
||||
self.type == RPLC_TYPE.RPS_RESET or
|
||||
self.type == RPLC_TYPE.RPS_AUTO_RESET or
|
||||
self.type == RPLC_TYPE.AUTO_BURN_RATE
|
||||
end
|
||||
|
||||
-- make an RPLC packet
|
||||
---@param id integer
|
||||
---@param packet_type RPLC_TYPES
|
||||
---@param packet_type RPLC_TYPE
|
||||
---@param data table
|
||||
function public.make(id, packet_type, data)
|
||||
if type(data) == "table" then
|
||||
@ -387,7 +386,7 @@ function comms.rplc_packet()
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.RPLC then
|
||||
if frame.protocol() == PROTOCOL.RPLC then
|
||||
local ok = frame.length() >= 2
|
||||
|
||||
if ok then
|
||||
@ -410,9 +409,11 @@ function comms.rplc_packet()
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class rplc_frame
|
||||
local frame = {
|
||||
@ -430,11 +431,12 @@ function comms.rplc_packet()
|
||||
end
|
||||
|
||||
-- SCADA management packet
|
||||
---@nodiscard
|
||||
function comms.mgmt_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = {},
|
||||
type = -1,
|
||||
type = 0, ---@type SCADA_MGMT_TYPE
|
||||
length = 0,
|
||||
data = {}
|
||||
}
|
||||
@ -444,16 +446,16 @@ function comms.mgmt_packet()
|
||||
|
||||
-- check that type is known
|
||||
local function _scada_type_valid()
|
||||
return self.type == SCADA_MGMT_TYPES.ESTABLISH or
|
||||
self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or
|
||||
self.type == SCADA_MGMT_TYPES.CLOSE or
|
||||
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
|
||||
self.type == SCADA_MGMT_TYPES.RTU_ADVERT or
|
||||
self.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT
|
||||
return self.type == SCADA_MGMT_TYPE.ESTABLISH or
|
||||
self.type == SCADA_MGMT_TYPE.KEEP_ALIVE or
|
||||
self.type == SCADA_MGMT_TYPE.CLOSE or
|
||||
self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or
|
||||
self.type == SCADA_MGMT_TYPE.RTU_ADVERT or
|
||||
self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT
|
||||
end
|
||||
|
||||
-- make a SCADA management packet
|
||||
---@param packet_type SCADA_MGMT_TYPES
|
||||
---@param packet_type SCADA_MGMT_TYPE
|
||||
---@param data table
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
@ -479,7 +481,7 @@ function comms.mgmt_packet()
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
if frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local ok = frame.length() >= 1
|
||||
|
||||
if ok then
|
||||
@ -500,9 +502,11 @@ function comms.mgmt_packet()
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class mgmt_frame
|
||||
local frame = {
|
||||
@ -519,11 +523,12 @@ function comms.mgmt_packet()
|
||||
end
|
||||
|
||||
-- SCADA coordinator packet
|
||||
---@nodiscard
|
||||
function comms.crdn_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = {},
|
||||
type = -1,
|
||||
type = 0, ---@type SCADA_CRDN_TYPE
|
||||
length = 0,
|
||||
data = {}
|
||||
}
|
||||
@ -532,18 +537,19 @@ function comms.crdn_packet()
|
||||
local public = {}
|
||||
|
||||
-- check that type is known
|
||||
---@nodiscard
|
||||
local function _crdn_type_valid()
|
||||
return self.type == SCADA_CRDN_TYPES.INITIAL_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPES.FAC_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPES.FAC_STATUS or
|
||||
self.type == SCADA_CRDN_TYPES.FAC_CMD or
|
||||
self.type == SCADA_CRDN_TYPES.UNIT_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or
|
||||
self.type == SCADA_CRDN_TYPES.UNIT_CMD
|
||||
return self.type == SCADA_CRDN_TYPE.INITIAL_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPE.FAC_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPE.FAC_STATUS or
|
||||
self.type == SCADA_CRDN_TYPE.FAC_CMD or
|
||||
self.type == SCADA_CRDN_TYPE.UNIT_BUILDS or
|
||||
self.type == SCADA_CRDN_TYPE.UNIT_STATUSES or
|
||||
self.type == SCADA_CRDN_TYPE.UNIT_CMD
|
||||
end
|
||||
|
||||
-- make a coordinator packet
|
||||
---@param packet_type SCADA_CRDN_TYPES
|
||||
---@param packet_type SCADA_CRDN_TYPE
|
||||
---@param data table
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
@ -569,7 +575,7 @@ function comms.crdn_packet()
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.SCADA_CRDN then
|
||||
if frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local ok = frame.length() >= 1
|
||||
|
||||
if ok then
|
||||
@ -590,9 +596,11 @@ function comms.crdn_packet()
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class crdn_frame
|
||||
local frame = {
|
||||
@ -609,12 +617,13 @@ function comms.crdn_packet()
|
||||
end
|
||||
|
||||
-- coordinator API (CAPI) packet
|
||||
-- @todo
|
||||
---@todo implement for pocket access, set enum type for self.type
|
||||
---@nodiscard
|
||||
function comms.capi_packet()
|
||||
local self = {
|
||||
frame = nil,
|
||||
raw = {},
|
||||
type = -1,
|
||||
type = 0,
|
||||
length = 0,
|
||||
data = {}
|
||||
}
|
||||
@ -623,12 +632,12 @@ function comms.capi_packet()
|
||||
local public = {}
|
||||
|
||||
local function _capi_type_valid()
|
||||
-- @todo
|
||||
---@todo
|
||||
return false
|
||||
end
|
||||
|
||||
-- make a coordinator API packet
|
||||
---@param packet_type CAPI_TYPES
|
||||
---@param packet_type CAPI_TYPE
|
||||
---@param data table
|
||||
function public.make(packet_type, data)
|
||||
if type(data) == "table" then
|
||||
@ -654,7 +663,7 @@ function comms.capi_packet()
|
||||
if frame then
|
||||
self.frame = frame
|
||||
|
||||
if frame.protocol() == PROTOCOLS.COORD_API then
|
||||
if frame.protocol() == PROTOCOL.COORD_API then
|
||||
local ok = frame.length() >= 1
|
||||
|
||||
if ok then
|
||||
@ -675,9 +684,11 @@ function comms.capi_packet()
|
||||
end
|
||||
|
||||
-- get raw to send
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
|
||||
-- get this packet as a frame with an immutable relation to this object
|
||||
---@nodiscard
|
||||
function public.get()
|
||||
---@class capi_frame
|
||||
local frame = {
|
||||
@ -693,50 +704,4 @@ function comms.capi_packet()
|
||||
return public
|
||||
end
|
||||
|
||||
-- convert rtu_t to RTU unit type
|
||||
---@param type rtu_t
|
||||
---@return RTU_UNIT_TYPES|nil
|
||||
function comms.rtu_t_to_unit_type(type)
|
||||
if type == rtu_t.redstone then
|
||||
return RTU_UNIT_TYPES.REDSTONE
|
||||
elseif type == rtu_t.boiler_valve then
|
||||
return RTU_UNIT_TYPES.BOILER_VALVE
|
||||
elseif type == rtu_t.turbine_valve then
|
||||
return RTU_UNIT_TYPES.TURBINE_VALVE
|
||||
elseif type == rtu_t.induction_matrix then
|
||||
return RTU_UNIT_TYPES.IMATRIX
|
||||
elseif type == rtu_t.sps then
|
||||
return RTU_UNIT_TYPES.SPS
|
||||
elseif type == rtu_t.sna then
|
||||
return RTU_UNIT_TYPES.SNA
|
||||
elseif type == rtu_t.env_detector then
|
||||
return RTU_UNIT_TYPES.ENV_DETECTOR
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- convert RTU unit type to rtu_t
|
||||
---@param utype RTU_UNIT_TYPES
|
||||
---@return rtu_t|nil
|
||||
function comms.advert_type_to_rtu_t(utype)
|
||||
if utype == RTU_UNIT_TYPES.REDSTONE then
|
||||
return rtu_t.redstone
|
||||
elseif utype == RTU_UNIT_TYPES.BOILER_VALVE then
|
||||
return rtu_t.boiler_valve
|
||||
elseif utype == RTU_UNIT_TYPES.TURBINE_VALVE then
|
||||
return rtu_t.turbine_valve
|
||||
elseif utype == RTU_UNIT_TYPES.IMATRIX then
|
||||
return rtu_t.induction_matrix
|
||||
elseif utype == RTU_UNIT_TYPES.SPS then
|
||||
return rtu_t.sps
|
||||
elseif utype == RTU_UNIT_TYPES.SNA then
|
||||
return rtu_t.sna
|
||||
elseif utype == RTU_UNIT_TYPES.ENV_DETECTOR then
|
||||
return rtu_t.env_detector
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return comms
|
||||
|
71
scada-common/constants.lua
Normal file
71
scada-common/constants.lua
Normal file
@ -0,0 +1,71 @@
|
||||
--
|
||||
-- System and Safety Constants
|
||||
--
|
||||
|
||||
-- Notes on Radiation
|
||||
-- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
|
||||
-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
|
||||
-- - damaging radiation 0.00006 Sv/h (60 uSv/h)
|
||||
|
||||
local constants = {}
|
||||
|
||||
--#region Reactor Protection System (on the PLC) Limits
|
||||
|
||||
local rps = {}
|
||||
|
||||
rps.MAX_DAMAGE_PERCENT = 90 -- damage >= 90%
|
||||
rps.MAX_DAMAGE_TEMPERATURE = 1200 -- temp >= 1200K
|
||||
rps.MIN_COOLANT_FILL = 0.10 -- fill < 10%
|
||||
rps.MAX_WASTE_FILL = 0.8 -- fill > 80%
|
||||
rps.MAX_HEATED_COLLANT_FILL = 0.95 -- fill > 95%
|
||||
rps.NO_FUEL_FILL = 0.0 -- fill <= 0%
|
||||
|
||||
constants.RPS_LIMITS = rps
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Annunciator Limits
|
||||
|
||||
local annunc = {}
|
||||
|
||||
annunc.RCSFlowLow = -2.0 -- flow < -2.0 mB/s
|
||||
annunc.CoolantLevelLow = 0.4 -- fill < 40%
|
||||
annunc.ReactorTempHigh = 1000 -- temp > 1000K
|
||||
annunc.ReactorHighDeltaT = 50 -- rate > 50K/s
|
||||
annunc.FuelLevelLow = 0.05 -- fill <= 5%
|
||||
annunc.WasteLevelHigh = 0.85 -- fill >= 85%
|
||||
annunc.SteamFeedMismatch = 10 -- ±10mB difference between total coolant flow and total steam input rate
|
||||
annunc.RadiationWarning = 0.00001 -- 10 uSv/h
|
||||
|
||||
constants.ANNUNCIATOR_LIMITS = annunc
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Supervisor Alarm Limits
|
||||
|
||||
local alarms = {}
|
||||
|
||||
-- unit alarms
|
||||
|
||||
alarms.HIGH_TEMP = 1150 -- temp >= 1150K
|
||||
alarms.HIGH_WASTE = 0.5 -- fill > 50%
|
||||
alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good
|
||||
|
||||
-- facility alarms
|
||||
|
||||
alarms.CHARGE_HIGH = 1.0 -- once at or above 100% charge
|
||||
alarms.CHARGE_RE_ENABLE = 0.95 -- once below 95% charge
|
||||
alarms.FAC_HIGH_RAD = 0.00001 -- 10 uSv/h
|
||||
|
||||
constants.ALARM_LIMITS = alarms
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Supervisor Constants
|
||||
|
||||
-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks
|
||||
constants.FLOW_STABILITY_DELAY_MS = 15000
|
||||
|
||||
--#endregion
|
||||
|
||||
return constants
|
@ -70,6 +70,7 @@ function crypto.init(password, server_port)
|
||||
end
|
||||
|
||||
-- encrypt plaintext
|
||||
---@nodiscard
|
||||
---@param plaintext string
|
||||
---@return table initial_value, string ciphertext
|
||||
function crypto.encrypt(plaintext)
|
||||
@ -113,6 +114,7 @@ function crypto.encrypt(plaintext)
|
||||
end
|
||||
|
||||
-- decrypt ciphertext
|
||||
---@nodiscard
|
||||
---@param iv string CTR initial value
|
||||
---@param ciphertext string ciphertext hex
|
||||
---@return string plaintext
|
||||
@ -135,6 +137,7 @@ function crypto.decrypt(iv, ciphertext)
|
||||
end
|
||||
|
||||
-- generate HMAC of message
|
||||
---@nodiscard
|
||||
---@param message_hex string initial value concatenated with ciphertext
|
||||
function crypto.hmac(message_hex)
|
||||
local start = util.time()
|
||||
@ -201,11 +204,12 @@ function crypto.secure_modem(modem)
|
||||
end
|
||||
|
||||
-- parse in a modem message as a network packet
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
---@nodiscard
|
||||
---@param side string modem side
|
||||
---@param sender integer sender port
|
||||
---@param reply_to integer reply port
|
||||
---@param message any encrypted packet sent with secure_modem.transmit
|
||||
---@param distance integer
|
||||
---@param distance integer transmission distance
|
||||
---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
local body = ""
|
||||
|
@ -18,7 +18,7 @@ log.MODE = MODE
|
||||
-- whether to log debug messages or not
|
||||
local LOG_DEBUG = true
|
||||
|
||||
local _log_sys = {
|
||||
local log_sys = {
|
||||
path = "/log.txt",
|
||||
mode = MODE.APPEND,
|
||||
file = nil,
|
||||
@ -33,27 +33,25 @@ local free_space = fs.getFreeSpace
|
||||
---@param write_mode MODE
|
||||
---@param dmesg_redirect? table terminal/window to direct dmesg to
|
||||
function log.init(path, write_mode, dmesg_redirect)
|
||||
_log_sys.path = path
|
||||
_log_sys.mode = write_mode
|
||||
log_sys.path = path
|
||||
log_sys.mode = write_mode
|
||||
|
||||
if _log_sys.mode == MODE.APPEND then
|
||||
_log_sys.file = fs.open(path, "a")
|
||||
if log_sys.mode == MODE.APPEND then
|
||||
log_sys.file = fs.open(path, "a")
|
||||
else
|
||||
_log_sys.file = fs.open(path, "w")
|
||||
log_sys.file = fs.open(path, "w")
|
||||
end
|
||||
|
||||
if dmesg_redirect then
|
||||
_log_sys.dmesg_out = dmesg_redirect
|
||||
log_sys.dmesg_out = dmesg_redirect
|
||||
else
|
||||
_log_sys.dmesg_out = term.current()
|
||||
log_sys.dmesg_out = term.current()
|
||||
end
|
||||
end
|
||||
|
||||
-- direct dmesg output to a monitor/window
|
||||
---@param window table window or terminal reference
|
||||
function log.direct_dmesg(window)
|
||||
_log_sys.dmesg_out = window
|
||||
end
|
||||
function log.direct_dmesg(window) log_sys.dmesg_out = window end
|
||||
|
||||
-- private log write function
|
||||
---@param msg string
|
||||
@ -64,8 +62,8 @@ local function _log(msg)
|
||||
|
||||
-- attempt to write log
|
||||
local status, result = pcall(function ()
|
||||
_log_sys.file.writeLine(stamped)
|
||||
_log_sys.file.flush()
|
||||
log_sys.file.writeLine(stamped)
|
||||
log_sys.file.flush()
|
||||
end)
|
||||
|
||||
-- if we don't have space, we need to create a new log file
|
||||
@ -80,18 +78,18 @@ local function _log(msg)
|
||||
end
|
||||
end
|
||||
|
||||
if out_of_space or (free_space(_log_sys.path) < 100) then
|
||||
if out_of_space or (free_space(log_sys.path) < 100) then
|
||||
-- delete the old log file before opening a new one
|
||||
_log_sys.file.close()
|
||||
fs.delete(_log_sys.path)
|
||||
log_sys.file.close()
|
||||
fs.delete(log_sys.path)
|
||||
|
||||
-- re-init logger and pass dmesg_out so that it doesn't change
|
||||
log.init(_log_sys.path, _log_sys.mode, _log_sys.dmesg_out)
|
||||
log.init(log_sys.path, log_sys.mode, log_sys.dmesg_out)
|
||||
|
||||
-- leave a message
|
||||
_log_sys.file.writeLine(time_stamp .. "recycled log file")
|
||||
_log_sys.file.writeLine(stamped)
|
||||
_log_sys.file.flush()
|
||||
log_sys.file.writeLine(time_stamp .. "recycled log file")
|
||||
log_sys.file.writeLine(stamped)
|
||||
log_sys.file.flush()
|
||||
end
|
||||
end
|
||||
|
||||
@ -109,7 +107,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
tag = util.strval(tag)
|
||||
|
||||
local t_stamp = string.format("%12.2f", os.clock())
|
||||
local out = _log_sys.dmesg_out
|
||||
local out = log_sys.dmesg_out
|
||||
|
||||
if out ~= nil then
|
||||
local out_w, out_h = out.getSize()
|
||||
@ -197,6 +195,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
end
|
||||
|
||||
-- print a dmesg message, but then show remaining seconds instead of timestamp
|
||||
---@nodiscard
|
||||
---@param msg string message
|
||||
---@param tag? string log tag
|
||||
---@param tag_color? integer log tag color
|
||||
@ -204,7 +203,7 @@ end
|
||||
function log.dmesg_working(msg, tag, tag_color)
|
||||
local ts_coord = log.dmesg(msg, tag, tag_color)
|
||||
|
||||
local out = _log_sys.dmesg_out
|
||||
local out = log_sys.dmesg_out
|
||||
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
||||
|
||||
if out ~= nil then
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
local mqueue = {}
|
||||
|
||||
---@alias MQ_TYPE integer
|
||||
---@enum MQ_TYPE
|
||||
local TYPE = {
|
||||
COMMAND = 0,
|
||||
DATA = 1,
|
||||
@ -14,6 +14,7 @@ local TYPE = {
|
||||
mqueue.TYPE = TYPE
|
||||
|
||||
-- create a new message queue
|
||||
---@nodiscard
|
||||
function mqueue.new()
|
||||
local queue = {}
|
||||
|
||||
@ -35,10 +36,13 @@ function mqueue.new()
|
||||
function public.length() return #queue end
|
||||
|
||||
-- check if queue is empty
|
||||
---@nodiscard
|
||||
---@return boolean is_empty
|
||||
function public.empty() return #queue == 0 end
|
||||
|
||||
-- check if queue has contents
|
||||
---@nodiscard
|
||||
---@return boolean has_contents
|
||||
function public.ready() return #queue ~= 0 end
|
||||
|
||||
-- push a new item onto the queue
|
||||
@ -68,6 +72,7 @@ function mqueue.new()
|
||||
end
|
||||
|
||||
-- get an item off the queue
|
||||
---@nodiscard
|
||||
---@return queue_item|nil
|
||||
function public.pop()
|
||||
if #queue > 0 then
|
||||
|
@ -24,7 +24,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
|
||||
|
||||
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
|
||||
|
||||
local _ppm_sys = {
|
||||
local ppm_sys = {
|
||||
mounts = {},
|
||||
next_vid = 0,
|
||||
auto_cf = false,
|
||||
@ -34,11 +34,9 @@ local _ppm_sys = {
|
||||
mute = false
|
||||
}
|
||||
|
||||
-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program
|
||||
---
|
||||
---also provides peripheral-specific fault checks (auto-clear fault defaults to true)
|
||||
---
|
||||
---assumes iface is a valid peripheral
|
||||
-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program<br>
|
||||
-- also provides peripheral-specific fault checks (auto-clear fault defaults to true)<br>
|
||||
-- assumes iface is a valid peripheral
|
||||
---@param iface string CC peripheral interface
|
||||
local function peri_init(iface)
|
||||
local self = {
|
||||
@ -68,7 +66,7 @@ local function peri_init(iface)
|
||||
if status then
|
||||
-- auto fault clear
|
||||
if self.auto_cf then self.faulted = false end
|
||||
if _ppm_sys.auto_cf then _ppm_sys.faulted = false end
|
||||
if ppm_sys.auto_cf then ppm_sys.faulted = false end
|
||||
|
||||
self.fault_counts[key] = 0
|
||||
|
||||
@ -80,10 +78,10 @@ local function peri_init(iface)
|
||||
self.faulted = true
|
||||
self.last_fault = result
|
||||
|
||||
_ppm_sys.faulted = true
|
||||
_ppm_sys.last_fault = result
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = result
|
||||
|
||||
if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
count_str = " [" .. self.fault_counts[key] .. " total faults]"
|
||||
@ -95,7 +93,7 @@ local function peri_init(iface)
|
||||
self.fault_counts[key] = self.fault_counts[key] + 1
|
||||
|
||||
if result == "Terminated" then
|
||||
_ppm_sys.terminate = true
|
||||
ppm_sys.terminate = true
|
||||
end
|
||||
|
||||
return ACCESS_FAULT
|
||||
@ -136,10 +134,10 @@ local function peri_init(iface)
|
||||
self.faulted = true
|
||||
self.last_fault = UNDEFINED_FIELD
|
||||
|
||||
_ppm_sys.faulted = true
|
||||
_ppm_sys.last_fault = UNDEFINED_FIELD
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = UNDEFINED_FIELD
|
||||
|
||||
if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
count_str = " [" .. self.fault_counts[key] .. " total calls]"
|
||||
@ -169,48 +167,35 @@ end
|
||||
-- REPORTING --
|
||||
|
||||
-- silence error prints
|
||||
function ppm.disable_reporting()
|
||||
_ppm_sys.mute = true
|
||||
end
|
||||
function ppm.disable_reporting() ppm_sys.mute = true end
|
||||
|
||||
-- allow error prints
|
||||
function ppm.enable_reporting()
|
||||
_ppm_sys.mute = false
|
||||
end
|
||||
function ppm.enable_reporting() ppm_sys.mute = false end
|
||||
|
||||
-- FAULT MEMORY --
|
||||
|
||||
-- enable automatically clearing fault flag
|
||||
function ppm.enable_afc()
|
||||
_ppm_sys.auto_cf = true
|
||||
end
|
||||
function ppm.enable_afc() ppm_sys.auto_cf = true end
|
||||
|
||||
-- disable automatically clearing fault flag
|
||||
function ppm.disable_afc()
|
||||
_ppm_sys.auto_cf = false
|
||||
end
|
||||
function ppm.disable_afc() ppm_sys.auto_cf = false end
|
||||
|
||||
-- clear fault flag
|
||||
function ppm.clear_fault()
|
||||
_ppm_sys.faulted = false
|
||||
end
|
||||
function ppm.clear_fault() ppm_sys.faulted = false end
|
||||
|
||||
-- check fault flag
|
||||
function ppm.is_faulted()
|
||||
return _ppm_sys.faulted
|
||||
end
|
||||
---@nodiscard
|
||||
function ppm.is_faulted() return ppm_sys.faulted end
|
||||
|
||||
-- get the last fault message
|
||||
function ppm.get_last_fault()
|
||||
return _ppm_sys.last_fault
|
||||
end
|
||||
---@nodiscard
|
||||
function ppm.get_last_fault() return ppm_sys.last_fault end
|
||||
|
||||
-- TERMINATION --
|
||||
|
||||
-- if a caught error was a termination request
|
||||
function ppm.should_terminate()
|
||||
return _ppm_sys.terminate
|
||||
end
|
||||
---@nodiscard
|
||||
function ppm.should_terminate() return ppm_sys.terminate end
|
||||
|
||||
-- MOUNTING --
|
||||
|
||||
@ -218,12 +203,12 @@ end
|
||||
function ppm.mount_all()
|
||||
local ifaces = peripheral.getNames()
|
||||
|
||||
_ppm_sys.mounts = {}
|
||||
ppm_sys.mounts = {}
|
||||
|
||||
for i = 1, #ifaces do
|
||||
_ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
|
||||
log.info(util.c("PPM: found a ", _ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
end
|
||||
|
||||
if #ifaces == 0 then
|
||||
@ -232,6 +217,7 @@ function ppm.mount_all()
|
||||
end
|
||||
|
||||
-- mount a particular device
|
||||
---@nodiscard
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type, table|nil device
|
||||
function ppm.mount(iface)
|
||||
@ -241,10 +227,10 @@ function ppm.mount(iface)
|
||||
|
||||
for i = 1, #ifaces do
|
||||
if iface == ifaces[i] then
|
||||
_ppm_sys.mounts[iface] = peri_init(iface)
|
||||
ppm_sys.mounts[iface] = peri_init(iface)
|
||||
|
||||
pm_type = _ppm_sys.mounts[iface].type
|
||||
pm_dev = _ppm_sys.mounts[iface].dev
|
||||
pm_type = ppm_sys.mounts[iface].type
|
||||
pm_dev = ppm_sys.mounts[iface].dev
|
||||
|
||||
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
|
||||
break
|
||||
@ -255,26 +241,27 @@ function ppm.mount(iface)
|
||||
end
|
||||
|
||||
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
|
||||
---@nodiscard
|
||||
---@return string type, table device
|
||||
function ppm.mount_virtual()
|
||||
local iface = "ppm_vdev_" .. _ppm_sys.next_vid
|
||||
local iface = "ppm_vdev_" .. ppm_sys.next_vid
|
||||
|
||||
_ppm_sys.mounts[iface] = peri_init("__virtual__")
|
||||
_ppm_sys.next_vid = _ppm_sys.next_vid + 1
|
||||
ppm_sys.mounts[iface] = peri_init("__virtual__")
|
||||
ppm_sys.next_vid = ppm_sys.next_vid + 1
|
||||
|
||||
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
|
||||
|
||||
return _ppm_sys.mounts[iface].type, _ppm_sys.mounts[iface].dev
|
||||
return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev
|
||||
end
|
||||
|
||||
-- manually unmount a peripheral from the PPM
|
||||
---@param device table device table
|
||||
function ppm.unmount(device)
|
||||
if device then
|
||||
for side, data in pairs(_ppm_sys.mounts) do
|
||||
for side, data in pairs(ppm_sys.mounts) do
|
||||
if data.dev == device then
|
||||
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", side))
|
||||
_ppm_sys.mounts[side] = nil
|
||||
ppm_sys.mounts[side] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
@ -282,6 +269,7 @@ function ppm.unmount(device)
|
||||
end
|
||||
|
||||
-- handle peripheral_detach event
|
||||
---@nodiscard
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type, table|nil device
|
||||
function ppm.handle_unmount(iface)
|
||||
@ -289,7 +277,7 @@ function ppm.handle_unmount(iface)
|
||||
local pm_type = nil
|
||||
|
||||
-- what got disconnected?
|
||||
local lost_dev = _ppm_sys.mounts[iface]
|
||||
local lost_dev = ppm_sys.mounts[iface]
|
||||
|
||||
if lost_dev then
|
||||
pm_type = lost_dev.type
|
||||
@ -300,7 +288,7 @@ function ppm.handle_unmount(iface)
|
||||
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
|
||||
end
|
||||
|
||||
_ppm_sys.mounts[iface] = nil
|
||||
ppm_sys.mounts[iface] = nil
|
||||
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
@ -308,23 +296,26 @@ end
|
||||
-- GENERAL ACCESSORS --
|
||||
|
||||
-- list all available peripherals
|
||||
---@nodiscard
|
||||
---@return table names
|
||||
function ppm.list_avail()
|
||||
return peripheral.getNames()
|
||||
end
|
||||
|
||||
-- list mounted peripherals
|
||||
---@nodiscard
|
||||
---@return table mounts
|
||||
function ppm.list_mounts()
|
||||
return _ppm_sys.mounts
|
||||
return ppm_sys.mounts
|
||||
end
|
||||
|
||||
-- get a mounted peripheral side/interface by device table
|
||||
---@nodiscard
|
||||
---@param device table device table
|
||||
---@return string|nil iface CC peripheral interface
|
||||
function ppm.get_iface(device)
|
||||
if device then
|
||||
for side, data in pairs(_ppm_sys.mounts) do
|
||||
for side, data in pairs(ppm_sys.mounts) do
|
||||
if data.dev == device then return side end
|
||||
end
|
||||
end
|
||||
@ -333,30 +324,33 @@ function ppm.get_iface(device)
|
||||
end
|
||||
|
||||
-- get a mounted peripheral by side/interface
|
||||
---@nodiscard
|
||||
---@param iface string CC peripheral interface
|
||||
---@return table|nil device function table
|
||||
function ppm.get_periph(iface)
|
||||
if _ppm_sys.mounts[iface] then
|
||||
return _ppm_sys.mounts[iface].dev
|
||||
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
|
||||
---@nodiscard
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type
|
||||
function ppm.get_type(iface)
|
||||
if _ppm_sys.mounts[iface] then
|
||||
return _ppm_sys.mounts[iface].type
|
||||
if ppm_sys.mounts[iface] then
|
||||
return ppm_sys.mounts[iface].type
|
||||
else return nil end
|
||||
end
|
||||
|
||||
-- get all mounted peripherals by type
|
||||
---@nodiscard
|
||||
---@param name string type name
|
||||
---@return table devices device function tables
|
||||
function ppm.get_all_devices(name)
|
||||
local devices = {}
|
||||
|
||||
for _, data in pairs(_ppm_sys.mounts) do
|
||||
for _, data in pairs(ppm_sys.mounts) do
|
||||
if data.type == name then
|
||||
table.insert(devices, data.dev)
|
||||
end
|
||||
@ -366,12 +360,13 @@ function ppm.get_all_devices(name)
|
||||
end
|
||||
|
||||
-- get a mounted peripheral by type (if multiple, returns the first)
|
||||
---@nodiscard
|
||||
---@param name string type name
|
||||
---@return table|nil device function table
|
||||
function ppm.get_device(name)
|
||||
local device = nil
|
||||
|
||||
for side, data in pairs(_ppm_sys.mounts) do
|
||||
for _, data in pairs(ppm_sys.mounts) do
|
||||
if data.type == name then
|
||||
device = data.dev
|
||||
break
|
||||
@ -384,20 +379,21 @@ end
|
||||
-- SPECIFIC DEVICE ACCESSORS --
|
||||
|
||||
-- get the fission reactor (if multiple, returns the first)
|
||||
---@nodiscard
|
||||
---@return table|nil reactor function table
|
||||
function ppm.get_fission_reactor()
|
||||
return ppm.get_device("fissionReactorLogicAdapter")
|
||||
end
|
||||
|
||||
-- get the wireless modem (if multiple, returns the first)
|
||||
--
|
||||
-- get the wireless modem (if multiple, returns the first)<br>
|
||||
-- if this is in a CraftOS emulated environment, wired modems will be used instead
|
||||
---@nodiscard
|
||||
---@return table|nil modem function table
|
||||
function ppm.get_wireless_modem()
|
||||
local w_modem = nil
|
||||
local emulated_env = periphemu ~= nil
|
||||
|
||||
for _, device in pairs(_ppm_sys.mounts) do
|
||||
for _, device in pairs(ppm_sys.mounts) do
|
||||
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
|
||||
w_modem = device.dev
|
||||
break
|
||||
@ -408,11 +404,12 @@ function ppm.get_wireless_modem()
|
||||
end
|
||||
|
||||
-- list all connected monitors
|
||||
---@nodiscard
|
||||
---@return table monitors
|
||||
function ppm.get_monitor_list()
|
||||
local list = {}
|
||||
|
||||
for iface, device in pairs(_ppm_sys.mounts) do
|
||||
for iface, device in pairs(ppm_sys.mounts) do
|
||||
if device.type == "monitor" then
|
||||
list[iface] = device
|
||||
end
|
||||
|
@ -5,6 +5,7 @@
|
||||
local psil = {}
|
||||
|
||||
-- instantiate a new PSI layer
|
||||
---@nodiscard
|
||||
function psil.create()
|
||||
local self = {
|
||||
ic = {}
|
||||
@ -19,8 +20,7 @@ function psil.create()
|
||||
---@class psil
|
||||
local public = {}
|
||||
|
||||
-- subscribe to a data object in the interconnect
|
||||
--
|
||||
-- subscribe to a data object in the interconnect<br>
|
||||
-- will call func() right away if a value is already avaliable
|
||||
---@param key string data key
|
||||
---@param func function function to call on change
|
||||
|
@ -89,6 +89,7 @@ rsio.IO = IO_PORT
|
||||
-----------------------
|
||||
|
||||
-- port to string
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
function rsio.to_string(port)
|
||||
local names = {
|
||||
@ -194,6 +195,7 @@ local RS_DIO_MAP = {
|
||||
}
|
||||
|
||||
-- get the mode of a port
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
---@return IO_MODE
|
||||
function rsio.get_io_mode(port)
|
||||
@ -239,6 +241,7 @@ end
|
||||
local RS_SIDES = rs.getSides()
|
||||
|
||||
-- check if a port is valid
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
---@return boolean valid
|
||||
function rsio.is_valid_port(port)
|
||||
@ -246,6 +249,7 @@ function rsio.is_valid_port(port)
|
||||
end
|
||||
|
||||
-- check if a side is valid
|
||||
---@nodiscard
|
||||
---@param side string
|
||||
---@return boolean valid
|
||||
function rsio.is_valid_side(side)
|
||||
@ -258,6 +262,7 @@ function rsio.is_valid_side(side)
|
||||
end
|
||||
|
||||
-- check if a color is a valid single color
|
||||
---@nodiscard
|
||||
---@param color integer
|
||||
---@return boolean valid
|
||||
function rsio.is_color(color)
|
||||
@ -269,22 +274,25 @@ end
|
||||
-----------------
|
||||
|
||||
-- get digital I/O level reading from a redstone boolean input value
|
||||
---@param rs_value boolean
|
||||
---@nodiscard
|
||||
---@param rs_value boolean raw value from redstone
|
||||
---@return IO_LVL
|
||||
function rsio.digital_read(rs_value)
|
||||
if rs_value then return IO_LVL.HIGH else return IO_LVL.LOW end
|
||||
end
|
||||
|
||||
-- get redstone boolean output value corresponding to a digital I/O level
|
||||
---@param level IO_LVL
|
||||
---@nodiscard
|
||||
---@param level IO_LVL logic level
|
||||
---@return boolean
|
||||
function rsio.digital_write(level)
|
||||
return level == IO_LVL.HIGH
|
||||
end
|
||||
|
||||
-- returns the level corresponding to active
|
||||
---@param port IO_PORT
|
||||
---@param active boolean
|
||||
---@nodiscard
|
||||
---@param port IO_PORT port (to determine active high/low)
|
||||
---@param active boolean state to convert to logic level
|
||||
---@return IO_LVL|false
|
||||
function rsio.digital_write_active(port, active)
|
||||
if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then
|
||||
@ -295,9 +303,10 @@ function rsio.digital_write_active(port, active)
|
||||
end
|
||||
|
||||
-- returns true if the level corresponds to active
|
||||
---@param port IO_PORT
|
||||
---@param level IO_LVL
|
||||
---@return boolean|nil
|
||||
---@nodiscard
|
||||
---@param port IO_PORT port (to determine active low/high)
|
||||
---@param level IO_LVL logic level
|
||||
---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided
|
||||
function rsio.digital_is_active(port, level)
|
||||
if not util.is_int(port) then
|
||||
return nil
|
||||
@ -313,6 +322,7 @@ end
|
||||
----------------
|
||||
|
||||
-- read an analog value scaled from min to max
|
||||
---@nodiscard
|
||||
---@param rs_value number redstone reading (0 to 15)
|
||||
---@param min number minimum of range
|
||||
---@param max number maximum of range
|
||||
@ -323,6 +333,7 @@ function rsio.analog_read(rs_value, min, max)
|
||||
end
|
||||
|
||||
-- write an analog value from the provided scale range
|
||||
---@nodiscard
|
||||
---@param value number value to write (from min to max range)
|
||||
---@param min number minimum of range
|
||||
---@param max number maximum of range
|
||||
|
@ -19,8 +19,6 @@ function tcallbackdsp.dispatch(time, f)
|
||||
duration = time,
|
||||
expiry = time + util.time_s()
|
||||
}
|
||||
|
||||
-- log.debug(util.c("TCD: queued callback for ", f, " [timer: ", timer, "]"))
|
||||
end
|
||||
|
||||
-- request a function to be called after the specified time, aborting any registered instances of that function reference
|
||||
@ -45,8 +43,6 @@ function tcallbackdsp.dispatch_unique(time, f)
|
||||
duration = time,
|
||||
expiry = time + util.time_s()
|
||||
}
|
||||
|
||||
-- log.debug(util.c("TCD: queued callback for ", f, " [timer: ", timer, "]"))
|
||||
end
|
||||
|
||||
-- abort a requested callback
|
||||
@ -72,8 +68,7 @@ function tcallbackdsp.handle(event)
|
||||
end
|
||||
end
|
||||
|
||||
-- identify any overdo callbacks
|
||||
--
|
||||
-- identify any overdo callbacks<br>
|
||||
-- prints to log debug output
|
||||
function tcallbackdsp.diagnostics()
|
||||
for timer, entry in pairs(registry) do
|
||||
|
@ -12,12 +12,14 @@ local types = {}
|
||||
---@field amount integer
|
||||
|
||||
-- create a new tank fluid
|
||||
---@nodiscard
|
||||
---@param n string name
|
||||
---@param a integer amount
|
||||
---@return radiation_reading
|
||||
function types.new_tank_fluid(n, a) return { name = n, amount = a } end
|
||||
|
||||
-- create a new empty tank fluid
|
||||
---@nodiscard
|
||||
---@return tank_fluid
|
||||
function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0 } end
|
||||
|
||||
@ -26,12 +28,14 @@ function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0
|
||||
---@field unit string
|
||||
|
||||
-- create a new radiation reading
|
||||
---@nodiscard
|
||||
---@param r number radiaiton level
|
||||
---@param u string radiation unit
|
||||
---@return radiation_reading
|
||||
function types.new_radiation_reading(r, u) return { radiation = r, unit = u } end
|
||||
|
||||
-- create a new zeroed radiation reading
|
||||
---@nodiscard
|
||||
---@return radiation_reading
|
||||
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end
|
||||
|
||||
@ -41,6 +45,7 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv"
|
||||
---@field z integer
|
||||
|
||||
-- create a new coordinate
|
||||
---@nodiscard
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param z integer
|
||||
@ -48,11 +53,12 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv"
|
||||
function types.new_coordinate(x, y, z) return { x = x, y = y, z = z } end
|
||||
|
||||
-- create a new zero coordinate
|
||||
---@nodiscard
|
||||
---@return coordinate
|
||||
function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
|
||||
|
||||
---@class rtu_advertisement
|
||||
---@field type integer
|
||||
---@field type RTU_UNIT_TYPE
|
||||
---@field index integer
|
||||
---@field reactor integer
|
||||
---@field rsio table|nil
|
||||
@ -62,15 +68,58 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
|
||||
---@alias color integer
|
||||
|
||||
-- ENUMERATION TYPES --
|
||||
--#region
|
||||
|
||||
---@alias TRI_FAIL integer
|
||||
types.TRI_FAIL = {
|
||||
OK = 0,
|
||||
PARTIAL = 1,
|
||||
FULL = 2
|
||||
---@enum RTU_UNIT_TYPE
|
||||
types.RTU_UNIT_TYPE = {
|
||||
VIRTUAL = 0, -- virtual device
|
||||
REDSTONE = 1, -- redstone I/O
|
||||
BOILER_VALVE = 2, -- boiler mekanism 10.1+
|
||||
TURBINE_VALVE = 3, -- turbine, mekanism 10.1+
|
||||
IMATRIX = 4, -- induction matrix
|
||||
SPS = 5, -- SPS
|
||||
SNA = 6, -- SNA
|
||||
ENV_DETECTOR = 7 -- environment detector
|
||||
}
|
||||
|
||||
---@alias PROCESS integer
|
||||
types.RTU_UNIT_NAMES = {
|
||||
"redstone",
|
||||
"boiler_valve",
|
||||
"turbine_valve",
|
||||
"induction_matrix",
|
||||
"sps",
|
||||
"sna",
|
||||
"environment_detector"
|
||||
}
|
||||
|
||||
-- safe conversion of RTU UNIT TYPE to string
|
||||
---@nodiscard
|
||||
---@param utype RTU_UNIT_TYPE
|
||||
---@return string
|
||||
function types.rtu_type_to_string(utype)
|
||||
if utype == types.RTU_UNIT_TYPE.VIRTUAL then
|
||||
return "virtual"
|
||||
elseif utype == types.RTU_UNIT_TYPE.REDSTONE or
|
||||
utype == types.RTU_UNIT_TYPE.BOILER_VALVE or
|
||||
utype == types.RTU_UNIT_TYPE.TURBINE_VALVE or
|
||||
utype == types.RTU_UNIT_TYPE.IMATRIX or
|
||||
utype == types.RTU_UNIT_TYPE.SPS or
|
||||
utype == types.RTU_UNIT_TYPE.SNA or
|
||||
utype == types.RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
return types.RTU_UNIT_NAMES[utype]
|
||||
else
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
---@enum TRI_FAIL
|
||||
types.TRI_FAIL = {
|
||||
OK = 1,
|
||||
PARTIAL = 2,
|
||||
FULL = 3
|
||||
}
|
||||
|
||||
---@enum PROCESS
|
||||
types.PROCESS = {
|
||||
INACTIVE = 0,
|
||||
MAX_BURN = 1,
|
||||
@ -93,7 +142,7 @@ types.PROCESS_NAMES = {
|
||||
"GEN_RATE_FAULT_IDLE"
|
||||
}
|
||||
|
||||
---@alias WASTE_MODE integer
|
||||
---@enum WASTE_MODE
|
||||
types.WASTE_MODE = {
|
||||
AUTO = 1,
|
||||
PLUTONIUM = 2,
|
||||
@ -101,7 +150,14 @@ types.WASTE_MODE = {
|
||||
ANTI_MATTER = 4
|
||||
}
|
||||
|
||||
---@alias ALARM integer
|
||||
types.WASTE_MODE_NAMES = {
|
||||
"AUTO",
|
||||
"PLUTONIUM",
|
||||
"POLONIUM",
|
||||
"ANTI_MATTER"
|
||||
}
|
||||
|
||||
---@enum ALARM
|
||||
types.ALARM = {
|
||||
ContainmentBreach = 1,
|
||||
ContainmentRadiation = 2,
|
||||
@ -117,7 +173,7 @@ types.ALARM = {
|
||||
TurbineTrip = 12
|
||||
}
|
||||
|
||||
types.alarm_string = {
|
||||
types.ALARM_NAMES = {
|
||||
"ContainmentBreach",
|
||||
"ContainmentRadiation",
|
||||
"ReactorLost",
|
||||
@ -132,46 +188,40 @@ types.alarm_string = {
|
||||
"TurbineTrip"
|
||||
}
|
||||
|
||||
---@alias ALARM_PRIORITY integer
|
||||
---@enum ALARM_PRIORITY
|
||||
types.ALARM_PRIORITY = {
|
||||
CRITICAL = 0,
|
||||
EMERGENCY = 1,
|
||||
URGENT = 2,
|
||||
TIMELY = 3
|
||||
CRITICAL = 1,
|
||||
EMERGENCY = 2,
|
||||
URGENT = 3,
|
||||
TIMELY = 4
|
||||
}
|
||||
|
||||
types.alarm_prio_string = {
|
||||
types.ALARM_PRIORITY_NAMES = {
|
||||
"CRITICAL",
|
||||
"EMERGENCY",
|
||||
"URGENT",
|
||||
"TIMELY"
|
||||
}
|
||||
|
||||
-- map alarms to alarm priority
|
||||
types.ALARM_PRIO_MAP = {
|
||||
types.ALARM_PRIORITY.CRITICAL,
|
||||
types.ALARM_PRIORITY.CRITICAL,
|
||||
types.ALARM_PRIORITY.URGENT,
|
||||
types.ALARM_PRIORITY.CRITICAL,
|
||||
types.ALARM_PRIORITY.EMERGENCY,
|
||||
types.ALARM_PRIORITY.EMERGENCY,
|
||||
types.ALARM_PRIORITY.TIMELY,
|
||||
types.ALARM_PRIORITY.EMERGENCY,
|
||||
types.ALARM_PRIORITY.TIMELY,
|
||||
types.ALARM_PRIORITY.URGENT,
|
||||
types.ALARM_PRIORITY.TIMELY,
|
||||
types.ALARM_PRIORITY.URGENT
|
||||
---@enum ALARM_STATE
|
||||
types.ALARM_STATE = {
|
||||
INACTIVE = 1,
|
||||
TRIPPED = 2,
|
||||
ACKED = 3,
|
||||
RING_BACK = 4
|
||||
}
|
||||
|
||||
---@alias ALARM_STATE integer
|
||||
types.ALARM_STATE = {
|
||||
INACTIVE = 0,
|
||||
TRIPPED = 1,
|
||||
ACKED = 2,
|
||||
RING_BACK = 3
|
||||
types.ALARM_STATE_NAMES = {
|
||||
"INACTIVE",
|
||||
"TRIPPED",
|
||||
"ACKED",
|
||||
"RING_BACK"
|
||||
}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- STRING TYPES --
|
||||
--#region
|
||||
|
||||
---@alias os_event
|
||||
---| "alarm"
|
||||
@ -206,14 +256,28 @@ types.ALARM_STATE = {
|
||||
---| "websocket_failure"
|
||||
---| "websocket_message"
|
||||
---| "websocket_success"
|
||||
---| "clock_start" custom, added for reactor PLC
|
||||
|
||||
---@alias fluid
|
||||
---| "mekanism:empty_gas"
|
||||
---| "minecraft:water"
|
||||
---| "mekanism:sodium"
|
||||
---| "mekanism:superheated_sodium"
|
||||
|
||||
types.FLUID = {
|
||||
EMPTY_GAS = "mekanism:empty_gas",
|
||||
WATER = "minecraft:water",
|
||||
SODIUM = "mekanism:sodium",
|
||||
SUPERHEATED_SODIUM = "mekanism:superheated_sodium"
|
||||
}
|
||||
|
||||
---@alias rps_trip_cause
|
||||
---| "ok"
|
||||
---| "dmg_crit"
|
||||
---| "high_temp"
|
||||
---| "no_coolant"
|
||||
---| "full_waste"
|
||||
---| "heated_coolant_backup"
|
||||
---| "ex_waste"
|
||||
---| "ex_heated_coolant"
|
||||
---| "no_fuel"
|
||||
---| "fault"
|
||||
---| "timeout"
|
||||
@ -222,59 +286,40 @@ types.ALARM_STATE = {
|
||||
---| "sys_fail"
|
||||
---| "force_disabled"
|
||||
|
||||
---@alias fluid
|
||||
---| "mekanism:empty_gas"
|
||||
---| "minecraft:water"
|
||||
---| "mekanism:sodium"
|
||||
---| "mekanism:superheated_sodium"
|
||||
|
||||
types.fluid = {
|
||||
empty_gas = "mekanism:empty_gas",
|
||||
water = "minecraft:water",
|
||||
sodium = "mekanism:sodium",
|
||||
superheated_sodium = "mekanism:superheated_sodium"
|
||||
types.RPS_TRIP_CAUSE = {
|
||||
OK = "ok",
|
||||
DMG_CRIT = "dmg_crit",
|
||||
HIGH_TEMP = "high_temp",
|
||||
NO_COOLANT = "no_coolant",
|
||||
EX_WASTE = "ex_waste",
|
||||
EX_HCOOLANT = "ex_heated_coolant",
|
||||
NO_FUEL = "no_fuel",
|
||||
FAULT = "fault",
|
||||
TIMEOUT = "timeout",
|
||||
MANUAL = "manual",
|
||||
AUTOMATIC = "automatic",
|
||||
SYS_FAIL = "sys_fail",
|
||||
FORCE_DISABLED = "force_disabled"
|
||||
}
|
||||
|
||||
---@alias rtu_t string
|
||||
types.rtu_t = {
|
||||
redstone = "redstone",
|
||||
boiler_valve = "boiler_valve",
|
||||
turbine_valve = "turbine_valve",
|
||||
induction_matrix = "induction_matrix",
|
||||
sps = "sps",
|
||||
sna = "sna",
|
||||
env_detector = "environment_detector"
|
||||
}
|
||||
---@alias dumping_mode
|
||||
---| "IDLE"
|
||||
---| "DUMPING"
|
||||
---| "DUMPING_EXCESS"
|
||||
|
||||
---@alias rps_status_t rps_trip_cause
|
||||
types.rps_status_t = {
|
||||
ok = "ok",
|
||||
dmg_crit = "dmg_crit",
|
||||
high_temp = "high_temp",
|
||||
no_coolant = "no_coolant",
|
||||
ex_waste = "full_waste",
|
||||
ex_hcoolant = "heated_coolant_backup",
|
||||
no_fuel = "no_fuel",
|
||||
fault = "fault",
|
||||
timeout = "timeout",
|
||||
manual = "manual",
|
||||
automatic = "automatic",
|
||||
sys_fail = "sys_fail",
|
||||
force_disabled = "force_disabled"
|
||||
}
|
||||
|
||||
-- turbine steam dumping modes
|
||||
---@alias DUMPING_MODE string
|
||||
types.DUMPING_MODE = {
|
||||
IDLE = "IDLE",
|
||||
DUMPING = "DUMPING",
|
||||
DUMPING_EXCESS = "DUMPING_EXCESS"
|
||||
}
|
||||
|
||||
-- MODBUS
|
||||
--#endregion
|
||||
|
||||
-- modbus function codes
|
||||
---@alias MODBUS_FCODE integer
|
||||
-- MODBUS --
|
||||
--#region
|
||||
|
||||
-- MODBUS function codes
|
||||
---@enum MODBUS_FCODE
|
||||
types.MODBUS_FCODE = {
|
||||
READ_COILS = 0x01,
|
||||
READ_DISCRETE_INPUTS = 0x02,
|
||||
@ -287,8 +332,8 @@ types.MODBUS_FCODE = {
|
||||
ERROR_FLAG = 0x80
|
||||
}
|
||||
|
||||
-- modbus exception codes
|
||||
---@alias MODBUS_EXCODE integer
|
||||
-- MODBUS exception codes
|
||||
---@enum MODBUS_EXCODE
|
||||
types.MODBUS_EXCODE = {
|
||||
ILLEGAL_FUNCTION = 0x01,
|
||||
ILLEGAL_DATA_ADDR = 0x02,
|
||||
@ -302,4 +347,6 @@ types.MODBUS_EXCODE = {
|
||||
GATEWAY_TARGET_TIMEOUT = 0x0B
|
||||
}
|
||||
|
||||
--#endregion
|
||||
|
||||
return types
|
||||
|
@ -14,6 +14,7 @@ util.TICK_TIME_MS = 50
|
||||
--#region
|
||||
|
||||
-- trinary operator
|
||||
---@nodiscard
|
||||
---@param cond boolean|nil condition
|
||||
---@param a any return if true
|
||||
---@param b any return if false
|
||||
@ -57,6 +58,7 @@ end
|
||||
--#region
|
||||
|
||||
-- get a value as a string
|
||||
---@nodiscard
|
||||
---@param val any
|
||||
---@return string
|
||||
function util.strval(val)
|
||||
@ -69,6 +71,7 @@ function util.strval(val)
|
||||
end
|
||||
|
||||
-- repeat a string n times
|
||||
---@nodiscard
|
||||
---@param str string
|
||||
---@param n integer
|
||||
---@return string
|
||||
@ -81,6 +84,7 @@ function util.strrep(str, n)
|
||||
end
|
||||
|
||||
-- repeat a space n times
|
||||
---@nodiscard
|
||||
---@param n integer
|
||||
---@return string
|
||||
function util.spaces(n)
|
||||
@ -88,6 +92,7 @@ function util.spaces(n)
|
||||
end
|
||||
|
||||
-- pad text to a minimum width
|
||||
---@nodiscard
|
||||
---@param str string text
|
||||
---@param n integer minimum width
|
||||
---@return string
|
||||
@ -100,6 +105,7 @@ function util.pad(str, n)
|
||||
end
|
||||
|
||||
-- wrap a string into a table of lines, supporting single dash splits
|
||||
---@nodiscard
|
||||
---@param str string
|
||||
---@param limit integer line limit
|
||||
---@return table lines
|
||||
@ -147,13 +153,12 @@ function util.strwrap(str, limit)
|
||||
end
|
||||
|
||||
-- concatenation with built-in to string
|
||||
---@nodiscard
|
||||
---@vararg any
|
||||
---@return string
|
||||
function util.concat(...)
|
||||
local str = ""
|
||||
for _, v in ipairs(arg) do
|
||||
str = str .. util.strval(v)
|
||||
end
|
||||
for _, v in ipairs(arg) do str = str .. util.strval(v) end
|
||||
return str
|
||||
end
|
||||
|
||||
@ -161,15 +166,16 @@ end
|
||||
util.c = util.concat
|
||||
|
||||
-- sprintf implementation
|
||||
---@nodiscard
|
||||
---@param format string
|
||||
---@vararg any
|
||||
function util.sprintf(format, ...)
|
||||
return string.format(format, table.unpack(arg))
|
||||
end
|
||||
|
||||
-- format a number string with commas as the thousands separator
|
||||
--
|
||||
-- format a number string with commas as the thousands separator<br>
|
||||
-- subtracts from spaces at the start if present for each comma used
|
||||
---@nodiscard
|
||||
---@param num string number string
|
||||
---@return string
|
||||
function util.comma_format(num)
|
||||
@ -196,6 +202,7 @@ end
|
||||
--#region
|
||||
|
||||
-- is a value an integer
|
||||
---@nodiscard
|
||||
---@param x any value
|
||||
---@return boolean is_integer if the number is an integer
|
||||
function util.is_int(x)
|
||||
@ -203,6 +210,7 @@ function util.is_int(x)
|
||||
end
|
||||
|
||||
-- get the sign of a number
|
||||
---@nodiscard
|
||||
---@param x number value
|
||||
---@return integer sign (-1 for < 0, 1 otherwise)
|
||||
function util.sign(x)
|
||||
@ -210,12 +218,14 @@ function util.sign(x)
|
||||
end
|
||||
|
||||
-- round a number to an integer
|
||||
---@nodiscard
|
||||
---@return integer rounded
|
||||
function util.round(x)
|
||||
return math.floor(x + 0.5)
|
||||
end
|
||||
|
||||
-- get a new moving average object
|
||||
---@nodiscard
|
||||
---@param length integer history length
|
||||
---@param default number value to fill history with for first call to compute()
|
||||
function util.mov_avg(length, default)
|
||||
@ -249,6 +259,7 @@ function util.mov_avg(length, default)
|
||||
end
|
||||
|
||||
-- compute the moving average
|
||||
---@nodiscard
|
||||
---@return number average
|
||||
function public.compute()
|
||||
local sum = 0
|
||||
@ -264,6 +275,7 @@ end
|
||||
-- TIME --
|
||||
|
||||
-- current time
|
||||
---@nodiscard
|
||||
---@return integer milliseconds
|
||||
function util.time_ms()
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
@ -271,6 +283,7 @@ function util.time_ms()
|
||||
end
|
||||
|
||||
-- current time
|
||||
---@nodiscard
|
||||
---@return number seconds
|
||||
function util.time_s()
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
@ -278,10 +291,9 @@ function util.time_s()
|
||||
end
|
||||
|
||||
-- current time
|
||||
---@nodiscard
|
||||
---@return integer milliseconds
|
||||
function util.time()
|
||||
return util.time_ms()
|
||||
end
|
||||
function util.time() return util.time_ms() end
|
||||
|
||||
--#endregion
|
||||
|
||||
@ -289,6 +301,7 @@ end
|
||||
--#region
|
||||
|
||||
-- OS pull event raw wrapper with types
|
||||
---@nodiscard
|
||||
---@param target_event? string event to wait for
|
||||
---@return os_event event, any param1, any param2, any param3, any param4, any param5
|
||||
function util.pull_event(target_event)
|
||||
@ -309,6 +322,7 @@ function util.push_event(event, param1, param2, param3, param4, param5)
|
||||
end
|
||||
|
||||
-- start an OS timer
|
||||
---@nodiscard
|
||||
---@param t number timer duration in seconds
|
||||
---@return integer timer ID
|
||||
function util.start_timer(t)
|
||||
@ -336,14 +350,12 @@ function util.psleep(t)
|
||||
pcall(os.sleep, t)
|
||||
end
|
||||
|
||||
-- no-op to provide a brief pause (1 tick) to yield
|
||||
---
|
||||
-- no-op to provide a brief pause (1 tick) to yield<br>
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function util.nop()
|
||||
util.psleep(0.05)
|
||||
end
|
||||
function util.nop() util.psleep(0.05) end
|
||||
|
||||
-- attempt to maintain a minimum loop timing (duration of execution)
|
||||
---@nodiscard
|
||||
---@param target_timing integer minimum amount of milliseconds to wait for
|
||||
---@param last_update integer millisecond time of last update
|
||||
---@return integer time_now
|
||||
@ -351,9 +363,7 @@ end
|
||||
function util.adaptive_delay(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
|
||||
util.psleep(sleep_for / 1000.0)
|
||||
end
|
||||
if sleep_for >= 50 then util.psleep(sleep_for / 1000.0) end
|
||||
return util.time()
|
||||
end
|
||||
|
||||
@ -362,8 +372,7 @@ end
|
||||
-- TABLE UTILITIES --
|
||||
--#region
|
||||
|
||||
-- delete elements from a table if the passed function returns false when passed a table element
|
||||
--
|
||||
-- delete elements from a table if the passed function returns false when passed a table element<br>
|
||||
-- put briefly: deletes elements that return false, keeps elements that return true
|
||||
---@param t table table to remove elements from
|
||||
---@param f function should return false to delete an element when passed the element: f(elem) = true|false
|
||||
@ -388,6 +397,7 @@ function util.filter_table(t, f, on_delete)
|
||||
end
|
||||
|
||||
-- check if a table contains the provided element
|
||||
---@nodiscard
|
||||
---@param t table table to check
|
||||
---@param element any element to check for
|
||||
function util.table_contains(t, element)
|
||||
@ -404,11 +414,13 @@ end
|
||||
--#region
|
||||
|
||||
-- convert Joules to FE
|
||||
---@nodiscard
|
||||
---@param J number Joules
|
||||
---@return number FE Forge Energy
|
||||
function util.joules_to_fe(J) return (J * 0.4) end
|
||||
|
||||
-- convert FE to Joules
|
||||
---@nodiscard
|
||||
---@param FE number Forge Energy
|
||||
---@return number J Joules
|
||||
function util.fe_to_joules(FE) return (FE * 2.5) end
|
||||
@ -418,10 +430,11 @@ local function MFE(fe) return fe / 1000000.0 end
|
||||
local function GFE(fe) return fe / 1000000000.0 end
|
||||
local function TFE(fe) return fe / 1000000000000.0 end
|
||||
local function PFE(fe) return fe / 1000000000000000.0 end
|
||||
local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass
|
||||
local function ZFE(fe) return fe / 1000000000000000000000.0 end -- please stop
|
||||
local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass
|
||||
local function ZFE(fe) return fe / 1000000000000000000000.0 end -- please stop
|
||||
|
||||
-- format a power value into XXX.XX UNIT format (FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE)
|
||||
---@nodiscard
|
||||
---@param fe number forge energy value
|
||||
---@param combine_label? boolean if a label should be included in the string itself
|
||||
---@param format? string format override
|
||||
@ -430,9 +443,7 @@ function util.power_format(fe, combine_label, format)
|
||||
local unit
|
||||
local value
|
||||
|
||||
if type(format) ~= "string" then
|
||||
format = "%.2f"
|
||||
end
|
||||
if type(format) ~= "string" then format = "%.2f" end
|
||||
|
||||
if fe < 1000.0 then
|
||||
unit = "FE"
|
||||
@ -474,10 +485,10 @@ end
|
||||
|
||||
-- WATCHDOG --
|
||||
|
||||
-- ComputerCraft OS Timer based Watchdog
|
||||
-- OS timer based watchdog<br>
|
||||
-- triggers a timer event if not fed within 'timeout' seconds
|
||||
---@nodiscard
|
||||
---@param timeout number timeout duration
|
||||
---
|
||||
--- triggers a timer event if not fed within 'timeout' seconds
|
||||
function util.new_watchdog(timeout)
|
||||
local self = {
|
||||
timeout = timeout,
|
||||
@ -487,10 +498,10 @@ function util.new_watchdog(timeout)
|
||||
---@class watchdog
|
||||
local public = {}
|
||||
|
||||
-- check if a timer is this watchdog
|
||||
---@nodiscard
|
||||
---@param timer number timer event timer ID
|
||||
function public.is_timer(timer)
|
||||
return self.wd_timer == timer
|
||||
end
|
||||
function public.is_timer(timer) return self.wd_timer == timer end
|
||||
|
||||
-- satiate the beast
|
||||
function public.feed()
|
||||
@ -512,10 +523,10 @@ end
|
||||
|
||||
-- LOOP CLOCK --
|
||||
|
||||
-- ComputerCraft OS Timer based Loop Clock
|
||||
-- OS timer based loop clock<br>
|
||||
-- fires a timer event at the specified period, does not start at construct time
|
||||
---@nodiscard
|
||||
---@param period number clock period
|
||||
---
|
||||
--- fires a timer event at the specified period, does not start at construct time
|
||||
function util.new_clock(period)
|
||||
local self = {
|
||||
period = period,
|
||||
@ -525,24 +536,22 @@ function util.new_clock(period)
|
||||
---@class clock
|
||||
local public = {}
|
||||
|
||||
-- check if a timer is this clock
|
||||
---@nodiscard
|
||||
---@param timer number timer event timer ID
|
||||
function public.is_clock(timer)
|
||||
return self.timer == timer
|
||||
end
|
||||
function public.is_clock(timer) return self.timer == timer end
|
||||
|
||||
-- start the clock
|
||||
function public.start()
|
||||
self.timer = util.start_timer(self.period)
|
||||
end
|
||||
function public.start() self.timer = util.start_timer(self.period) end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- FIELD VALIDATOR --
|
||||
|
||||
-- create a new type validator
|
||||
--
|
||||
-- create a new type validator<br>
|
||||
-- can execute sequential checks and check valid() to see if it is still valid
|
||||
---@nodiscard
|
||||
function util.new_validator()
|
||||
local valid = true
|
||||
|
||||
@ -565,6 +574,8 @@ function util.new_validator()
|
||||
|
||||
function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end
|
||||
|
||||
-- check if all assertions passed successfully
|
||||
---@nodiscard
|
||||
function public.valid() return valid end
|
||||
|
||||
return public
|
||||
|
@ -1,3 +1,4 @@
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
@ -12,19 +13,13 @@ local PROCESS_NAMES = types.PROCESS_NAMES
|
||||
|
||||
local IO = rsio.IO
|
||||
|
||||
-- 7.14 kJ per blade for 1 mB of fissile fuel<br/>
|
||||
-- 7.14 kJ per blade for 1 mB of fissile fuel<br>
|
||||
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
|
||||
local POWER_PER_BLADE = util.joules_to_fe(7140)
|
||||
|
||||
local FLOW_STABILITY_DELAY_S = unit.FLOW_STABILITY_DELAY_MS / 1000
|
||||
local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000
|
||||
|
||||
-- background radiation 0.0000001 Sv/h (99.99 nSv/h)
|
||||
-- "green tint" radiation 0.00001 Sv/h (10 uSv/h)
|
||||
-- damaging radiation 0.00006 Sv/h (60 uSv/h)
|
||||
local RADIATION_ALARM_LEVEL = 0.00001
|
||||
|
||||
local HIGH_CHARGE = 1.0
|
||||
local RE_ENABLE_CHARGE = 0.95
|
||||
local ALARM_LIMS = const.ALARM_LIMITS
|
||||
|
||||
local AUTO_SCRAM = {
|
||||
NONE = 0,
|
||||
@ -53,6 +48,7 @@ local rate_Kd = -1.0
|
||||
local facility = {}
|
||||
|
||||
-- create a new facility management object
|
||||
---@nodiscard
|
||||
---@param num_reactors integer number of reactor units
|
||||
---@param cooling_conf table cooling configurations of reactor units
|
||||
function facility.new(num_reactors, cooling_conf)
|
||||
@ -124,6 +120,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
|
||||
-- check if all auto-controlled units completed ramping
|
||||
---@nodiscard
|
||||
local function _all_units_ramped()
|
||||
local all_ramped = true
|
||||
|
||||
@ -185,10 +182,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
unallocated = math.max(0, unallocated - ctl.br100)
|
||||
|
||||
if last ~= ctl.br100 then
|
||||
log.debug("unit " .. u.get_id() .. ": set to " .. ctl.br100 .. " (was " .. last .. ")")
|
||||
u.a_commit_br100(ramp)
|
||||
end
|
||||
if last ~= ctl.br100 then u.a_commit_br100(ramp) end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -426,7 +420,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
self.accumulator = self.accumulator + (error * (now - self.last_time))
|
||||
end
|
||||
|
||||
local runtime = now - self.time_start
|
||||
-- local runtime = now - self.time_start
|
||||
local integral = self.accumulator
|
||||
local derivative = (error - self.last_error) / (now - self.last_time)
|
||||
|
||||
@ -441,8 +435,8 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
self.saturated = output ~= out_c
|
||||
|
||||
log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }",
|
||||
runtime, avg_charge, error, integral, output, out_c, P, I, D))
|
||||
-- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }",
|
||||
-- runtime, avg_charge, error, integral, output, out_c, P, I, D))
|
||||
|
||||
_allocate_burn_rate(out_c, true)
|
||||
|
||||
@ -495,7 +489,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
self.accumulator = self.accumulator + (error * (now - self.last_time))
|
||||
end
|
||||
|
||||
local runtime = now - self.time_start
|
||||
-- local runtime = now - self.time_start
|
||||
local integral = self.accumulator
|
||||
local derivative = (error - self.last_error) / (now - self.last_time)
|
||||
|
||||
@ -513,8 +507,8 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
self.saturated = output ~= out_c
|
||||
|
||||
log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
|
||||
runtime, avg_inflow, error, integral, output, out_c, P, I, D))
|
||||
-- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
|
||||
-- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
|
||||
|
||||
_allocate_burn_rate(out_c, false)
|
||||
|
||||
@ -564,10 +558,10 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
-- check matrix fill too high
|
||||
local was_fill = astatus.matrix_fill
|
||||
astatus.matrix_fill = (db.tanks.energy_fill >= HIGH_CHARGE) or (astatus.matrix_fill and db.tanks.energy_fill > RE_ENABLE_CHARGE)
|
||||
astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
|
||||
|
||||
if was_fill and not astatus.matrix_fill then
|
||||
log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (RE_ENABLE_CHARGE * 100) .. "%")
|
||||
log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%")
|
||||
end
|
||||
|
||||
-- check for critical unit alarms
|
||||
@ -586,7 +580,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
local envd = self.envd[1] ---@type unit_session
|
||||
local e_db = envd.get_db() ---@type envd_session_db
|
||||
|
||||
astatus.radiation = e_db.radiation_raw > RADIATION_ALARM_LEVEL
|
||||
astatus.radiation = e_db.radiation_raw > ALARM_LIMS.FAC_HIGH_RAD
|
||||
else
|
||||
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
|
||||
-- operator can restart the system or hit the stop/reset button
|
||||
@ -814,6 +808,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
-- READ STATES/PROPERTIES --
|
||||
|
||||
-- get build properties of all machines
|
||||
---@nodiscard
|
||||
---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude
|
||||
function public.get_build(inc_imatrix)
|
||||
local build = {}
|
||||
@ -830,6 +825,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
|
||||
-- get automatic process control status
|
||||
---@nodiscard
|
||||
function public.get_control_status()
|
||||
local astat = self.ascram_status
|
||||
return {
|
||||
@ -851,6 +847,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
|
||||
-- get RTU statuses
|
||||
---@nodiscard
|
||||
function public.get_rtu_statuses()
|
||||
local status = {}
|
||||
|
||||
@ -889,9 +886,9 @@ function facility.new(num_reactors, cooling_conf)
|
||||
return status
|
||||
end
|
||||
|
||||
function public.get_units()
|
||||
return self.units
|
||||
end
|
||||
-- get the units in this facility
|
||||
---@nodiscard
|
||||
function public.get_units() return self.units end
|
||||
|
||||
return public
|
||||
end
|
||||
|
@ -1,18 +1,20 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local svqtypes = require("supervisor.session.svqtypes")
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
|
||||
local UNIT_COMMANDS = comms.UNIT_COMMANDS
|
||||
local FAC_COMMANDS = comms.FAC_COMMANDS
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local SV_Q_CMDS = svqtypes.SV_Q_CMDS
|
||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||
@ -45,6 +47,7 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- coordinator supervisor session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
@ -54,8 +57,6 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
local log_header = "crdn_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
in_q = in_queue,
|
||||
out_q = out_queue,
|
||||
units = facility.get_units(),
|
||||
-- connection properties
|
||||
seq_num = 0,
|
||||
@ -90,30 +91,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
end
|
||||
|
||||
-- send a CRDN packet
|
||||
---@param msg_type SCADA_CRDN_TYPES
|
||||
---@param msg_type SCADA_CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local c_pkt = comms.crdn_packet()
|
||||
|
||||
c_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
|
||||
self.out_q.push_packet(s_pkt)
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.out_q.push_packet(s_pkt)
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@ -126,12 +127,12 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
unit_builds[unit.get_id()] = unit.get_build()
|
||||
end
|
||||
|
||||
_send(SCADA_CRDN_TYPES.INITIAL_BUILDS, { facility.get_build(), unit_builds })
|
||||
_send(SCADA_CRDN_TYPE.INITIAL_BUILDS, { facility.get_build(), unit_builds })
|
||||
end
|
||||
|
||||
-- send facility builds
|
||||
local function _send_fac_builds()
|
||||
_send(SCADA_CRDN_TYPES.FAC_BUILDS, { facility.get_build() })
|
||||
_send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build() })
|
||||
end
|
||||
|
||||
-- send unit builds
|
||||
@ -143,7 +144,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
builds[unit.get_id()] = unit.get_build()
|
||||
end
|
||||
|
||||
_send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds })
|
||||
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
|
||||
end
|
||||
|
||||
-- send facility status
|
||||
@ -153,7 +154,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
facility.get_rtu_statuses()
|
||||
}
|
||||
|
||||
_send(SCADA_CRDN_TYPES.FAC_STATUS, status)
|
||||
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
|
||||
end
|
||||
|
||||
-- send unit statuses
|
||||
@ -172,7 +173,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
}
|
||||
end
|
||||
|
||||
_send(SCADA_CRDN_TYPES.UNIT_STATUSES, status)
|
||||
_send(SCADA_CRDN_TYPE.UNIT_STATUSES, status)
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
@ -192,8 +193,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
@ -210,30 +211,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then
|
||||
if pkt.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
||||
-- acknowledgement to coordinator receiving builds
|
||||
self.acks.builds = true
|
||||
elseif pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then
|
||||
elseif pkt.type == SCADA_CRDN_TYPE.FAC_BUILDS then
|
||||
-- acknowledgement to coordinator receiving builds
|
||||
self.acks.fac_builds = true
|
||||
elseif pkt.type == SCADA_CRDN_TYPES.FAC_CMD then
|
||||
elseif pkt.type == SCADA_CRDN_TYPE.FAC_CMD then
|
||||
if pkt.length >= 1 then
|
||||
local cmd = pkt.data[1]
|
||||
|
||||
if cmd == FAC_COMMANDS.SCRAM_ALL then
|
||||
if cmd == FAC_COMMAND.SCRAM_ALL then
|
||||
facility.scram_all()
|
||||
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
|
||||
elseif cmd == FAC_COMMANDS.STOP then
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
|
||||
elseif cmd == FAC_COMMAND.STOP then
|
||||
facility.auto_stop()
|
||||
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
|
||||
elseif cmd == FAC_COMMANDS.START then
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
|
||||
elseif cmd == FAC_COMMAND.START then
|
||||
if pkt.length == 6 then
|
||||
---@type coord_auto_config
|
||||
local config = {
|
||||
@ -244,23 +245,23 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
limits = pkt.data[6]
|
||||
}
|
||||
|
||||
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
facility.ack_all()
|
||||
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
|
||||
_send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command unknown")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
|
||||
elseif pkt.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
|
||||
-- acknowledgement to coordinator receiving builds
|
||||
self.acks.unit_builds = true
|
||||
elseif pkt.type == SCADA_CRDN_TYPES.UNIT_CMD then
|
||||
elseif pkt.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||
if pkt.length >= 2 then
|
||||
-- get command and unit id
|
||||
local cmd = pkt.data[1]
|
||||
@ -273,43 +274,43 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
if util.is_int(uid) and uid > 0 and uid <= #self.units then
|
||||
local unit = self.units[uid] ---@type reactor_unit
|
||||
|
||||
if cmd == UNIT_COMMANDS.START then
|
||||
self.out_q.push_data(SV_Q_DATA.START, data)
|
||||
elseif cmd == UNIT_COMMANDS.SCRAM then
|
||||
self.out_q.push_data(SV_Q_DATA.SCRAM, data)
|
||||
elseif cmd == UNIT_COMMANDS.RESET_RPS then
|
||||
self.out_q.push_data(SV_Q_DATA.RESET_RPS, data)
|
||||
elseif cmd == UNIT_COMMANDS.SET_BURN then
|
||||
if cmd == UNIT_COMMAND.START then
|
||||
out_queue.push_data(SV_Q_DATA.START, data)
|
||||
elseif cmd == UNIT_COMMAND.SCRAM then
|
||||
out_queue.push_data(SV_Q_DATA.SCRAM, data)
|
||||
elseif cmd == UNIT_COMMAND.RESET_RPS then
|
||||
out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
|
||||
elseif cmd == UNIT_COMMAND.SET_BURN then
|
||||
if pkt.length == 3 then
|
||||
self.out_q.push_data(SV_Q_DATA.SET_BURN, data)
|
||||
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command burn rate missing option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMANDS.SET_WASTE then
|
||||
elseif cmd == UNIT_COMMAND.SET_WASTE then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
|
||||
unit.set_waste(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command set waste missing option")
|
||||
end
|
||||
elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
|
||||
unit.ack_all()
|
||||
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, true })
|
||||
elseif cmd == UNIT_COMMANDS.ACK_ALARM then
|
||||
_send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, true })
|
||||
elseif cmd == UNIT_COMMAND.ACK_ALARM then
|
||||
if pkt.length == 3 then
|
||||
unit.ack_alarm(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
|
||||
end
|
||||
elseif cmd == UNIT_COMMANDS.RESET_ALARM then
|
||||
elseif cmd == UNIT_COMMAND.RESET_ALARM then
|
||||
if pkt.length == 3 then
|
||||
unit.reset_alarm(pkt.data[3])
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
|
||||
end
|
||||
elseif cmd == UNIT_COMMANDS.SET_GROUP then
|
||||
elseif cmd == UNIT_COMMAND.SET_GROUP then
|
||||
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then
|
||||
facility.set_group(unit.get_id(), pkt.data[3])
|
||||
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] })
|
||||
_send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
|
||||
else
|
||||
log.debug(log_header .. "CRDN unit command set group missing group id")
|
||||
end
|
||||
@ -332,9 +333,11 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
local public = {}
|
||||
|
||||
-- get the session ID
|
||||
---@nodiscard
|
||||
function public.get_id() return id end
|
||||
|
||||
-- check if a timer matches this session's watchdog
|
||||
---@nodiscard
|
||||
function public.check_wd(timer)
|
||||
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||
end
|
||||
@ -342,12 +345,13 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
println("connection to coordinator " .. id .. " closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
---@nodiscard
|
||||
---@return boolean connected
|
||||
function public.iterate()
|
||||
if self.connected then
|
||||
@ -357,9 +361,9 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
|
||||
local handle_start = util.time()
|
||||
|
||||
while self.in_q.ready() and self.connected do
|
||||
while in_queue.ready() and self.connected do
|
||||
-- get a new message to process
|
||||
local message = self.in_q.pop()
|
||||
local message = in_queue.pop()
|
||||
|
||||
if message ~= nil then
|
||||
if message.qtype == mqueue.TYPE.PACKET then
|
||||
@ -373,7 +377,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
|
||||
if cmd.key == CRD_S_DATA.CMD_ACK then
|
||||
local ack = cmd.val ---@type coord_ack
|
||||
_send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack })
|
||||
_send(SCADA_CRDN_TYPE.UNIT_CMD, { ack.cmd, ack.unit, ack.ack })
|
||||
elseif cmd.key == CRD_S_DATA.RESEND_PLC_BUILD then
|
||||
-- re-send PLC build
|
||||
-- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update
|
||||
@ -386,7 +390,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
local unit = self.units[unit_id] ---@type reactor_unit
|
||||
builds[unit_id] = unit.get_build(true, false, false)
|
||||
|
||||
_send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds })
|
||||
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
|
||||
elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then
|
||||
local unit_id = cmd.val.unit
|
||||
if unit_id > 0 then
|
||||
@ -398,16 +402,16 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
local builds = {}
|
||||
|
||||
local unit = self.units[unit_id] ---@type reactor_unit
|
||||
builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPES.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPES.TURBINE_VALVE)
|
||||
builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPE.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPE.TURBINE_VALVE)
|
||||
|
||||
_send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds })
|
||||
_send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
|
||||
else
|
||||
-- re-send facility RTU builds
|
||||
-- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update
|
||||
self.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD
|
||||
self.acks.fac_builds = false
|
||||
|
||||
_send(SCADA_CRDN_TYPES.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPES.IMATRIX) })
|
||||
_send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) })
|
||||
end
|
||||
else
|
||||
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
|
||||
@ -441,7 +445,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
|
@ -8,12 +8,11 @@ local svqtypes = require("supervisor.session.svqtypes")
|
||||
|
||||
local plc = {}
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local RPLC_TYPES = comms.RPLC_TYPES
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local RPLC_TYPE = comms.RPLC_TYPE
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
|
||||
|
||||
local UNIT_COMMANDS = comms.UNIT_COMMANDS
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -47,18 +46,16 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- PLC supervisor session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param for_reactor integer reactor ID
|
||||
---@param reactor_id integer reactor ID
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
|
||||
local log_header = "plc_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
for_reactor = for_reactor,
|
||||
in_q = in_queue,
|
||||
out_q = out_queue,
|
||||
commanded_state = false,
|
||||
commanded_burn_rate = 0.0,
|
||||
auto_cmd_token = 0,
|
||||
@ -244,34 +241,35 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send an RPLC packet
|
||||
---@param msg_type RPLC_TYPES
|
||||
---@param msg_type RPLC_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local r_pkt = comms.rplc_packet()
|
||||
|
||||
r_pkt.make(for_reactor, msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable())
|
||||
r_pkt.make(reactor_id, msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
|
||||
|
||||
self.out_q.push_packet(s_pkt)
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.out_q.push_packet(s_pkt)
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- get an ACK status
|
||||
---@nodiscard
|
||||
---@param pkt rplc_frame
|
||||
---@return boolean|nil ack
|
||||
local function _get_ack(pkt)
|
||||
@ -297,10 +295,10 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.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)
|
||||
if pkt.id ~= reactor_id then
|
||||
log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
|
||||
return
|
||||
end
|
||||
|
||||
@ -308,7 +306,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
self.plc_conn_watchdog.feed()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == RPLC_TYPES.STATUS then
|
||||
if pkt.type == RPLC_TYPE.STATUS then
|
||||
-- status packet received, update data
|
||||
if pkt.length >= 5 then
|
||||
self.sDB.last_status_update = pkt.data[1]
|
||||
@ -335,14 +333,14 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
else
|
||||
log.debug(log_header .. "RPLC status packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPES.MEK_STRUCT then
|
||||
elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
|
||||
-- received reactor structure, record it
|
||||
if pkt.length == 14 then
|
||||
local status = pcall(_copy_struct, pkt.data)
|
||||
if status then
|
||||
-- copied in structure data OK
|
||||
self.received_struct = true
|
||||
self.out_q.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, for_reactor)
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
|
||||
else
|
||||
-- error copying structure data
|
||||
log.error(log_header .. "failed to parse struct packet data")
|
||||
@ -350,7 +348,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
else
|
||||
log.debug(log_header .. "RPLC struct packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then
|
||||
elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
|
||||
-- burn rate acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if ack then
|
||||
@ -360,12 +358,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = self.for_reactor,
|
||||
cmd = UNIT_COMMANDS.SET_BURN,
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = reactor_id,
|
||||
cmd = UNIT_COMMAND.SET_BURN,
|
||||
ack = ack
|
||||
})
|
||||
elseif pkt.type == RPLC_TYPES.RPS_ENABLE then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_ENABLE then
|
||||
-- enable acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if ack then
|
||||
@ -375,12 +373,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = self.for_reactor,
|
||||
cmd = UNIT_COMMANDS.START,
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = reactor_id,
|
||||
cmd = UNIT_COMMAND.START,
|
||||
ack = ack
|
||||
})
|
||||
elseif pkt.type == RPLC_TYPES.RPS_SCRAM then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
|
||||
-- manual SCRAM acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if ack then
|
||||
@ -391,12 +389,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = self.for_reactor,
|
||||
cmd = UNIT_COMMANDS.SCRAM,
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = reactor_id,
|
||||
cmd = UNIT_COMMAND.SCRAM,
|
||||
ack = ack
|
||||
})
|
||||
elseif pkt.type == RPLC_TYPES.RPS_ASCRAM then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_ASCRAM then
|
||||
-- automatic SCRAM acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if ack then
|
||||
@ -405,7 +403,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
elseif ack == false then
|
||||
log.debug(log_header .. " automatic SCRAM failed!")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPES.RPS_STATUS then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_STATUS then
|
||||
-- RPS status packet received, copy data
|
||||
if pkt.length == 14 then
|
||||
local status = pcall(_copy_rps_status, pkt.data)
|
||||
@ -418,7 +416,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
else
|
||||
log.debug(log_header .. "RPLC RPS status packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPES.RPS_ALARM then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_ALARM then
|
||||
-- RPS alarm
|
||||
if pkt.length == 13 then
|
||||
local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) })
|
||||
@ -431,7 +429,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
else
|
||||
log.debug(log_header .. "RPLC RPS alarm packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPES.RPS_RESET then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_RESET then
|
||||
-- RPS reset acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if ack then
|
||||
@ -443,18 +441,18 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send acknowledgement to coordinator
|
||||
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = self.for_reactor,
|
||||
cmd = UNIT_COMMANDS.RESET_RPS,
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
|
||||
unit = reactor_id,
|
||||
cmd = UNIT_COMMAND.RESET_RPS,
|
||||
ack = ack
|
||||
})
|
||||
elseif pkt.type == RPLC_TYPES.RPS_AUTO_RESET then
|
||||
elseif pkt.type == RPLC_TYPE.RPS_AUTO_RESET then
|
||||
-- RPS auto control reset acknowledgement
|
||||
local ack = _get_ack(pkt)
|
||||
if not ack then
|
||||
log.debug(log_header .. "RPS auto reset failed")
|
||||
end
|
||||
elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then
|
||||
elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then
|
||||
if pkt.length == 1 then
|
||||
local ack = pkt.data[1]
|
||||
|
||||
@ -473,8 +471,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
else
|
||||
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.KEEP_ALIVE then
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
@ -491,7 +489,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
else
|
||||
@ -503,17 +501,22 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- get the session ID
|
||||
---@nodiscard
|
||||
function public.get_id() return id end
|
||||
|
||||
-- get the session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.sDB end
|
||||
|
||||
-- check if ramping is completed by first verifying auto command token ack
|
||||
---@nodiscard
|
||||
function public.is_ramp_complete()
|
||||
return (self.sDB.auto_ack_token == self.auto_cmd_token) and (self.commanded_burn_rate == self.sDB.mek_status.act_burn_rate)
|
||||
end
|
||||
|
||||
-- get the reactor structure
|
||||
---@nodiscard
|
||||
---@return mek_struct|table struct struct or empty table
|
||||
function public.get_struct()
|
||||
if self.received_struct then
|
||||
return self.sDB.mek_struct
|
||||
@ -523,6 +526,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- get the reactor status
|
||||
---@nodiscard
|
||||
---@return mek_status|table struct status or empty table
|
||||
function public.get_status()
|
||||
if self.received_status_cache then
|
||||
return self.sDB.mek_status
|
||||
@ -532,11 +537,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- get the reactor RPS status
|
||||
---@nodiscard
|
||||
function public.get_rps()
|
||||
return self.sDB.rps_status
|
||||
end
|
||||
|
||||
-- get the general status information
|
||||
---@nodiscard
|
||||
function public.get_general_status()
|
||||
return {
|
||||
self.sDB.last_status_update,
|
||||
@ -564,10 +571,11 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
---@param ramp boolean true to ramp, false to not
|
||||
function public.auto_set_burn(rate, ramp)
|
||||
self.ramping_rate = ramp
|
||||
self.in_q.push_data(PLC_S_DATA.AUTO_BURN_RATE, rate)
|
||||
in_queue.push_data(PLC_S_DATA.AUTO_BURN_RATE, rate)
|
||||
end
|
||||
|
||||
-- check if a timer matches this session's watchdog
|
||||
---@nodiscard
|
||||
function public.check_wd(timer)
|
||||
return self.plc_conn_watchdog.is_timer(timer) and self.connected
|
||||
end
|
||||
@ -575,12 +583,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
println("connection to reactor " .. self.for_reactor .. " PLC closed by server")
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
println("connection to reactor " .. reactor_id .. " PLC closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
---@nodiscard
|
||||
---@return boolean connected
|
||||
function public.iterate()
|
||||
if self.connected then
|
||||
@ -590,9 +599,9 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
local handle_start = util.time()
|
||||
|
||||
while self.in_q.ready() and self.connected do
|
||||
while in_queue.ready() and self.connected do
|
||||
-- get a new message to process
|
||||
local message = self.in_q.pop()
|
||||
local message = in_queue.pop()
|
||||
|
||||
if message ~= nil then
|
||||
if message.qtype == mqueue.TYPE.PACKET then
|
||||
@ -604,27 +613,27 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
if cmd == PLC_S_CMDS.ENABLE then
|
||||
-- enable reactor
|
||||
if not self.auto_lock then
|
||||
_send(RPLC_TYPES.RPS_ENABLE, {})
|
||||
_send(RPLC_TYPE.RPS_ENABLE, {})
|
||||
end
|
||||
elseif cmd == PLC_S_CMDS.SCRAM then
|
||||
-- SCRAM reactor
|
||||
self.acks.scram = false
|
||||
self.retry_times.scram_req = util.time() + INITIAL_WAIT
|
||||
_send(RPLC_TYPES.RPS_SCRAM, {})
|
||||
_send(RPLC_TYPE.RPS_SCRAM, {})
|
||||
elseif cmd == PLC_S_CMDS.ASCRAM then
|
||||
-- SCRAM reactor
|
||||
self.acks.ascram = false
|
||||
self.retry_times.ascram_req = util.time() + INITIAL_WAIT
|
||||
_send(RPLC_TYPES.RPS_ASCRAM, {})
|
||||
_send(RPLC_TYPE.RPS_ASCRAM, {})
|
||||
elseif cmd == PLC_S_CMDS.RPS_RESET then
|
||||
-- reset RPS
|
||||
self.acks.ascram = true
|
||||
self.acks.rps_reset = false
|
||||
self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT
|
||||
_send(RPLC_TYPES.RPS_RESET, {})
|
||||
_send(RPLC_TYPE.RPS_RESET, {})
|
||||
elseif cmd == PLC_S_CMDS.RPS_AUTO_RESET then
|
||||
if self.sDB.rps_status.automatic or self.sDB.rps_status.timeout then
|
||||
_send(RPLC_TYPES.RPS_AUTO_RESET, {})
|
||||
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
|
||||
end
|
||||
else
|
||||
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
|
||||
@ -642,7 +651,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
self.ramping_rate = false
|
||||
self.acks.burn_rate = false
|
||||
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
|
||||
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
|
||||
_send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
|
||||
end
|
||||
end
|
||||
elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then
|
||||
@ -655,7 +664,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
self.ramping_rate = true
|
||||
self.acks.burn_rate = false
|
||||
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
|
||||
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
|
||||
_send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
|
||||
end
|
||||
end
|
||||
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then
|
||||
@ -670,7 +679,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
self.acks.burn_rate = not self.ramping_rate
|
||||
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT
|
||||
|
||||
_send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
|
||||
_send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -688,7 +697,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("connection to reactor " .. self.for_reactor .. " PLC closed by remote host")
|
||||
println("connection to reactor " .. reactor_id .. " PLC closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
@ -705,7 +714,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
@ -722,7 +731,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
if not self.received_struct then
|
||||
if rtimes.struct_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPES.MEK_STRUCT, {})
|
||||
_send(RPLC_TYPE.MEK_STRUCT, {})
|
||||
rtimes.struct_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
@ -731,7 +740,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
if not self.received_status_cache then
|
||||
if rtimes.status_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPES.MEK_STATUS, {})
|
||||
_send(RPLC_TYPE.MEK_STATUS, {})
|
||||
rtimes.status_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
@ -742,13 +751,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
if rtimes.burn_rate_req - util.time() <= 0 then
|
||||
if self.auto_cmd_token > 0 then
|
||||
if self.auto_lock then
|
||||
_send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
|
||||
_send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
|
||||
else
|
||||
-- would have been an auto command, but disengaged, so stop retrying
|
||||
self.acks.burn_rate = true
|
||||
end
|
||||
elseif not self.auto_lock then
|
||||
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
|
||||
_send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
|
||||
else
|
||||
-- shouldn't be in this state, just pretend it was acknowledged
|
||||
self.acks.burn_rate = true
|
||||
@ -763,7 +772,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
if not self.acks.scram then
|
||||
if rtimes.scram_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPES.RPS_SCRAM, {})
|
||||
_send(RPLC_TYPE.RPS_SCRAM, {})
|
||||
rtimes.scram_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
@ -772,7 +781,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
if not self.acks.ascram then
|
||||
if rtimes.ascram_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPES.RPS_ASCRAM, {})
|
||||
_send(RPLC_TYPE.RPS_ASCRAM, {})
|
||||
rtimes.ascram_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
@ -781,7 +790,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
|
||||
|
||||
if not self.acks.rps_reset then
|
||||
if rtimes.rps_reset_req - util.time() <= 0 then
|
||||
_send(RPLC_TYPES.RPS_RESET, {})
|
||||
_send(RPLC_TYPE.RPS_RESET, {})
|
||||
rtimes.rps_reset_req = util.time() + RETRY_PERIOD
|
||||
end
|
||||
end
|
||||
|
@ -5,6 +5,7 @@
|
||||
local rsctl = {}
|
||||
|
||||
-- create a new redstone RTU I/O controller
|
||||
---@nodiscard
|
||||
---@param redstone_rtus table redstone RTU sessions
|
||||
function rsctl.new(redstone_rtus)
|
||||
---@class rs_controller
|
||||
|
@ -1,7 +1,7 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local svqtypes = require("supervisor.session.svqtypes")
|
||||
@ -18,9 +18,9 @@ local svrs_turbinev = require("supervisor.session.rtu.turbinev")
|
||||
|
||||
local rtu = {}
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -32,6 +32,7 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new RTU session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
@ -42,8 +43,6 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
local log_header = "rtu_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
in_q = in_queue,
|
||||
out_q = out_queue,
|
||||
modbus_q = mqueue.new(),
|
||||
advert = advertisement,
|
||||
fac_units = facility.get_units(),
|
||||
@ -99,7 +98,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
advert_validator.assert_type_int(unit_advert.index)
|
||||
advert_validator.assert_type_int(unit_advert.reactor)
|
||||
|
||||
if u_type == RTU_UNIT_TYPES.REDSTONE then
|
||||
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
||||
advert_validator.assert_type_table(unit_advert.rsio)
|
||||
end
|
||||
|
||||
@ -113,7 +112,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
end
|
||||
|
||||
local type_string = util.strval(u_type)
|
||||
if type(u_type) == "number" then type_string = util.strval(comms.advert_type_to_rtu_t(u_type)) end
|
||||
if type(u_type) == "number" then type_string = types.rtu_type_to_string(u_type) end
|
||||
|
||||
-- create unit by type
|
||||
|
||||
@ -124,19 +123,19 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
if unit_advert.reactor > 0 then
|
||||
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
|
||||
|
||||
if u_type == RTU_UNIT_TYPES.REDSTONE then
|
||||
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
||||
-- redstone
|
||||
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then
|
||||
elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
-- boiler (Mekanism 10.1+)
|
||||
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then
|
||||
elseif u_type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
-- turbine (Mekanism 10.1+)
|
||||
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then
|
||||
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
-- environment detector
|
||||
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then target_unit.add_envd(unit) end
|
||||
@ -144,21 +143,21 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string))
|
||||
end
|
||||
else
|
||||
if u_type == RTU_UNIT_TYPES.REDSTONE then
|
||||
if u_type == RTU_UNIT_TYPE.REDSTONE then
|
||||
-- redstone
|
||||
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then facility.add_redstone(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPES.IMATRIX then
|
||||
elseif u_type == RTU_UNIT_TYPE.IMATRIX then
|
||||
-- induction matrix
|
||||
unit = svrs_imatrix.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then facility.add_imatrix(unit) end
|
||||
elseif u_type == RTU_UNIT_TYPES.SPS then
|
||||
elseif u_type == RTU_UNIT_TYPE.SPS then
|
||||
-- super-critical phase shifter
|
||||
unit = svrs_sps.new(id, i, unit_advert, self.modbus_q)
|
||||
elseif u_type == RTU_UNIT_TYPES.SNA then
|
||||
elseif u_type == RTU_UNIT_TYPE.SNA then
|
||||
-- solar neutron activator
|
||||
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q)
|
||||
elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then
|
||||
elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
-- environment detector
|
||||
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
|
||||
if type(unit) ~= "nil" then facility.add_envd(unit) end
|
||||
@ -194,23 +193,23 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
local function _send_modbus(m_pkt)
|
||||
local s_pkt = comms.scada_packet()
|
||||
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
|
||||
|
||||
self.out_q.push_packet(s_pkt)
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPES
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.out_q.push_packet(s_pkt)
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
@ -231,15 +230,15 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
self.rtu_conn_watchdog.feed()
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
if self.units[pkt.unit_id] ~= nil then
|
||||
local unit = self.units[pkt.unit_id] ---@type unit_session
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
unit.handle_packet(pkt)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
-- handle management packet
|
||||
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
|
||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
@ -256,20 +255,17 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.RTU_ADVERT then
|
||||
-- RTU unit advertisement
|
||||
log.debug(log_header .. "received updated advertisement")
|
||||
|
||||
-- copy advertisement and remove version tag
|
||||
self.advert = pkt.data
|
||||
table.remove(self.advert, 1)
|
||||
|
||||
-- handle advertisement; this will re-create all unit sub-sessions
|
||||
_handle_advertisement()
|
||||
elseif pkt.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT then
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT then
|
||||
if pkt.length == 1 then
|
||||
local unit_id = pkt.data[1]
|
||||
if self.units[unit_id] ~= nil then
|
||||
@ -291,6 +287,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
function public.get_id() return id end
|
||||
|
||||
-- check if a timer matches this session's watchdog
|
||||
---@nodiscard
|
||||
---@param timer number
|
||||
function public.check_wd(timer)
|
||||
return self.rtu_conn_watchdog.is_timer(timer) and self.connected
|
||||
@ -299,12 +296,13 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
println(log_header .. "connection to RTU closed by server")
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
-- iterate the session
|
||||
---@nodiscard
|
||||
---@return boolean connected
|
||||
function public.iterate()
|
||||
if self.connected then
|
||||
@ -314,9 +312,9 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
|
||||
local handle_start = util.time()
|
||||
|
||||
while self.in_q.ready() and self.connected do
|
||||
while in_queue.ready() and self.connected do
|
||||
-- get a new message to process
|
||||
local msg = self.in_q.pop()
|
||||
local msg = in_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.PACKET then
|
||||
@ -365,7 +363,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
@ -389,7 +387,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
|
||||
-- instruction with body
|
||||
local cmd = msg.message ---@type queue_data
|
||||
if cmd.key == unit_session.RTU_US_DATA.BUILD_CHANGED then
|
||||
self.out_q.push_data(svqtypes.SV_Q_DATA.RTU_BUILD_CHANGED, cmd.val)
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.RTU_BUILD_CHANGED, cmd.val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,4 +1,3 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local boilerv = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local TXN_TYPES = {
|
||||
@ -32,14 +31,15 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new boilerv rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
function boilerv.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.BOILER_VALVE then
|
||||
log.error("attempt to instantiate boilerv RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
|
||||
log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -239,6 +239,7 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -1,4 +1,3 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local envd = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local TXN_TYPES = {
|
||||
@ -23,14 +22,15 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new environment detector rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer
|
||||
---@param unit_id integer
|
||||
---@param advert rtu_advertisement
|
||||
---@param out_queue mqueue
|
||||
function envd.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.ENV_DETECTOR then
|
||||
log.error("attempt to instantiate envd RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
|
||||
log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -100,6 +100,7 @@ function envd.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -1,4 +1,3 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local imatrix = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local TXN_TYPES = {
|
||||
@ -32,14 +31,15 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new imatrix rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
function imatrix.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.IMATRIX then
|
||||
log.error("attempt to instantiate imatrix RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
|
||||
log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -213,6 +213,7 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -1,6 +1,4 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
@ -9,7 +7,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local redstone = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local IO_PORT = rsio.IO
|
||||
@ -47,14 +45,15 @@ local PERIODICS = {
|
||||
---@field req IO_LVL
|
||||
|
||||
-- create a new redstone rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer
|
||||
---@param unit_id integer
|
||||
---@param advert rtu_advertisement
|
||||
---@param out_queue mqueue
|
||||
function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.REDSTONE then
|
||||
log.error("attempt to instantiate redstone RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
|
||||
log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -120,6 +119,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
---@class rs_db_dig_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
|
||||
---@param active boolean
|
||||
write = function (active) end
|
||||
@ -134,6 +134,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
---@class rs_db_dig_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end,
|
||||
---@param active boolean
|
||||
write = function (active)
|
||||
@ -151,6 +152,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
---@class rs_db_ana_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
---@return integer
|
||||
read = function () return self.phy_io.analog_in[port].phy end,
|
||||
---@param value integer
|
||||
@ -166,6 +168,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
|
||||
---@class rs_db_ana_io
|
||||
local io_f = {
|
||||
---@nodiscard
|
||||
---@return integer
|
||||
read = function () return self.phy_io.analog_out[port].phy end,
|
||||
---@param value integer
|
||||
@ -380,6 +383,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -1,4 +1,3 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local sna = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local TXN_TYPES = {
|
||||
@ -29,14 +28,15 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new sna rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
function sna.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.SNA then
|
||||
log.error("attempt to instantiate sna RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.SNA then
|
||||
log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -176,6 +176,7 @@ function sna.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -1,4 +1,3 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local sps = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
local TXN_TYPES = {
|
||||
@ -32,14 +31,15 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new sps rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
function sps.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.SPS then
|
||||
log.error("attempt to instantiate sps RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.SPS then
|
||||
log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -113,7 +113,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
|
||||
-- query the tanks of the device
|
||||
local function _request_tanks()
|
||||
-- read input registers 11 through 19 (start = 11, count = 9)
|
||||
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 })
|
||||
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
@ -223,6 +223,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -1,4 +1,3 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
@ -9,7 +8,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
|
||||
|
||||
local turbinev = {}
|
||||
|
||||
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local DUMPING_MODE = types.DUMPING_MODE
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
|
||||
@ -44,14 +43,15 @@ local PERIODICS = {
|
||||
}
|
||||
|
||||
-- create a new turbinev rtu session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer RTU unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement table
|
||||
---@param out_queue mqueue RTU unit message out queue
|
||||
function turbinev.new(session_id, unit_id, advert, out_queue)
|
||||
-- type check
|
||||
if advert.type ~= RTU_UNIT_TYPES.TURBINE_VALVE then
|
||||
log.error("attempt to instantiate turbinev RTU for type '" .. advert.type .. "'. this is a bug.")
|
||||
if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
|
||||
log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -92,7 +92,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
|
||||
flow_rate = 0,
|
||||
prod_rate = 0,
|
||||
steam_input_rate = 0,
|
||||
dumping_mode = DUMPING_MODE.IDLE ---@type DUMPING_MODE
|
||||
dumping_mode = DUMPING_MODE.IDLE ---@type dumping_mode
|
||||
},
|
||||
tanks = {
|
||||
last_update = 0,
|
||||
@ -123,7 +123,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- set the dumping mode
|
||||
---@param mode DUMPING_MODE
|
||||
---@param mode dumping_mode
|
||||
local function _set_dump_mode(mode)
|
||||
-- write holding register 1
|
||||
self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
|
||||
@ -310,6 +310,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db() return self.db end
|
||||
|
||||
return public
|
||||
|
@ -6,9 +6,10 @@ local util = require("scada-common.util")
|
||||
|
||||
local txnctrl = {}
|
||||
|
||||
local TIMEOUT = 2000 -- 2000ms max wait
|
||||
local TIMEOUT = 2000 -- 2000ms max wait
|
||||
|
||||
-- create a new transaction controller
|
||||
---@nodiscard
|
||||
function txnctrl.new()
|
||||
local self = {
|
||||
list = {},
|
||||
@ -22,16 +23,19 @@ function txnctrl.new()
|
||||
local remove = table.remove
|
||||
|
||||
-- get the length of the transaction list
|
||||
---@nodiscard
|
||||
function public.length()
|
||||
return #self.list
|
||||
end
|
||||
|
||||
-- check if there are no active transactions
|
||||
---@nodiscard
|
||||
function public.empty()
|
||||
return #self.list == 0
|
||||
end
|
||||
|
||||
-- create a new transaction of the given type
|
||||
---@nodiscard
|
||||
---@param txn_type integer
|
||||
---@return integer txn_id
|
||||
function public.create(txn_type)
|
||||
@ -49,6 +53,7 @@ function txnctrl.new()
|
||||
end
|
||||
|
||||
-- mark a transaction as resolved to get its transaction type
|
||||
---@nodiscard
|
||||
---@param txn_id integer
|
||||
---@return integer txn_type
|
||||
function public.resolve(txn_id)
|
||||
|
@ -8,7 +8,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl")
|
||||
|
||||
local unit_session = {}
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local MODBUS_FCODE = types.MODBUS_FCODE
|
||||
local MODBUS_EXCODE = types.MODBUS_EXCODE
|
||||
|
||||
@ -23,6 +23,7 @@ unit_session.RTU_US_CMDS = RTU_US_CMDS
|
||||
unit_session.RTU_US_DATA = RTU_US_DATA
|
||||
|
||||
-- create a new unit session runner
|
||||
---@nodiscard
|
||||
---@param session_id integer RTU session ID
|
||||
---@param unit_id integer MODBUS unit ID
|
||||
---@param advert rtu_advertisement RTU advertisement for this unit
|
||||
@ -31,12 +32,8 @@ unit_session.RTU_US_DATA = RTU_US_DATA
|
||||
---@param txn_tags table transaction log tags
|
||||
function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags)
|
||||
local self = {
|
||||
log_tag = log_tag,
|
||||
txn_tags = txn_tags,
|
||||
unit_id = unit_id,
|
||||
device_index = advert.index,
|
||||
reactor = advert.reactor,
|
||||
out_q = out_queue,
|
||||
transaction_controller = txnctrl.new(),
|
||||
connected = true,
|
||||
device_fail = false
|
||||
@ -61,21 +58,22 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
||||
local m_pkt = comms.modbus_packet()
|
||||
local txn_id = self.transaction_controller.create(txn_type)
|
||||
|
||||
m_pkt.make(txn_id, self.unit_id, f_code, register_param)
|
||||
m_pkt.make(txn_id, unit_id, f_code, register_param)
|
||||
|
||||
self.out_q.push_packet(m_pkt)
|
||||
out_queue.push_packet(m_pkt)
|
||||
|
||||
return txn_id
|
||||
end
|
||||
|
||||
-- try to resolve a MODBUS transaction
|
||||
---@nodiscard
|
||||
---@param m_pkt modbus_frame MODBUS packet
|
||||
---@return integer|false txn_type, integer txn_id transaction type or false on error/busy, transaction ID
|
||||
function protected.try_resolve(m_pkt)
|
||||
if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
if m_pkt.unit_id == self.unit_id then
|
||||
if m_pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
if m_pkt.unit_id == unit_id then
|
||||
local txn_type = self.transaction_controller.resolve(m_pkt.txn_id)
|
||||
local txn_tag = " (" .. util.strval(self.txn_tags[txn_type]) .. ")"
|
||||
local txn_tag = " (" .. util.strval(txn_tags[txn_type]) .. ")"
|
||||
|
||||
if bit.band(m_pkt.func_code, MODBUS_FCODE.ERROR_FLAG) ~= 0 then
|
||||
-- transaction incomplete or failed
|
||||
@ -135,26 +133,35 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
||||
end
|
||||
|
||||
-- get the public interface
|
||||
---@nodiscard
|
||||
function protected.get() return public end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- get the unit ID
|
||||
---@nodiscard
|
||||
function public.get_session_id() return session_id end
|
||||
-- get the unit ID
|
||||
function public.get_unit_id() return self.unit_id end
|
||||
---@nodiscard
|
||||
function public.get_unit_id() return unit_id end
|
||||
-- get the device index
|
||||
---@nodiscard
|
||||
function public.get_device_idx() return self.device_index end
|
||||
-- get the reactor ID
|
||||
---@nodiscard
|
||||
function public.get_reactor() return self.reactor end
|
||||
-- get the command queue
|
||||
---@nodiscard
|
||||
function public.get_cmd_queue() return protected.in_q end
|
||||
|
||||
-- close this unit
|
||||
---@nodiscard
|
||||
function public.close() self.connected = false end
|
||||
-- check if this unit is connected
|
||||
---@nodiscard
|
||||
function public.is_connected() return self.connected end
|
||||
-- check if this unit is faulted
|
||||
---@nodiscard
|
||||
function public.is_faulted() return self.device_fail end
|
||||
|
||||
-- PUBLIC TEMPLATE FUNCTIONS --
|
||||
@ -179,6 +186,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
|
||||
end
|
||||
|
||||
-- get the unit session database
|
||||
---@nodiscard
|
||||
function public.get_db()
|
||||
log.debug("template unit_session.get_db() called", true)
|
||||
return {}
|
||||
|
@ -183,9 +183,10 @@ local function _free_closed(sessions)
|
||||
end
|
||||
|
||||
-- find a session by remote port
|
||||
---@nodiscard
|
||||
---@param list table
|
||||
---@param port integer
|
||||
---@return plc_session_struct|rtu_session_struct|nil
|
||||
---@return plc_session_struct|rtu_session_struct|coord_session_struct|nil
|
||||
local function _find_session(list, port)
|
||||
for i = 1, #list do
|
||||
if list[i].r_port == port then return list[i] end
|
||||
@ -212,54 +213,63 @@ function svsessions.relink_modem(modem)
|
||||
end
|
||||
|
||||
-- find an RTU session by the remote port
|
||||
---@nodiscard
|
||||
---@param remote_port integer
|
||||
---@return rtu_session_struct|nil
|
||||
function svsessions.find_rtu_session(remote_port)
|
||||
-- check RTU sessions
|
||||
---@diagnostic disable-next-line: return-type-mismatch
|
||||
return _find_session(self.rtu_sessions, remote_port)
|
||||
local session = _find_session(self.rtu_sessions, remote_port)
|
||||
---@cast session rtu_session_struct
|
||||
return session
|
||||
end
|
||||
|
||||
-- find a PLC session by the remote port
|
||||
---@nodiscard
|
||||
---@param remote_port integer
|
||||
---@return plc_session_struct|nil
|
||||
function svsessions.find_plc_session(remote_port)
|
||||
-- check PLC sessions
|
||||
---@diagnostic disable-next-line: return-type-mismatch
|
||||
return _find_session(self.plc_sessions, remote_port)
|
||||
local session = _find_session(self.plc_sessions, remote_port)
|
||||
---@cast session plc_session_struct
|
||||
return session
|
||||
end
|
||||
|
||||
-- find a PLC/RTU session by the remote port
|
||||
---@nodiscard
|
||||
---@param remote_port integer
|
||||
---@return plc_session_struct|rtu_session_struct|nil
|
||||
function svsessions.find_device_session(remote_port)
|
||||
-- check RTU sessions
|
||||
local s = _find_session(self.rtu_sessions, remote_port)
|
||||
local session = _find_session(self.rtu_sessions, remote_port)
|
||||
|
||||
-- check PLC sessions
|
||||
if s == nil then s = _find_session(self.plc_sessions, remote_port) end
|
||||
if session == nil then session = _find_session(self.plc_sessions, remote_port) end
|
||||
---@cast session plc_session_struct|rtu_session_struct|nil
|
||||
|
||||
return s
|
||||
return session
|
||||
end
|
||||
|
||||
-- find a coordinator session by the remote port
|
||||
--
|
||||
-- find a coordinator session by the remote port<br>
|
||||
-- only one coordinator is allowed, but this is kept to be consistent with all other session tables
|
||||
---@nodiscard
|
||||
---@param remote_port integer
|
||||
---@return nil
|
||||
---@return coord_session_struct|nil
|
||||
function svsessions.find_coord_session(remote_port)
|
||||
-- check coordinator sessions
|
||||
---@diagnostic disable-next-line: return-type-mismatch
|
||||
return _find_session(self.coord_sessions, remote_port)
|
||||
local session = _find_session(self.coord_sessions, remote_port)
|
||||
---@cast session coord_session_struct
|
||||
return session
|
||||
end
|
||||
|
||||
-- get the a coordinator session if exists
|
||||
---@nodiscard
|
||||
---@return coord_session_struct|nil
|
||||
function svsessions.get_coord_session()
|
||||
return self.coord_sessions[1]
|
||||
end
|
||||
|
||||
-- get a session by reactor ID
|
||||
---@nodiscard
|
||||
---@param reactor integer
|
||||
---@return plc_session_struct|nil session
|
||||
function svsessions.get_reactor_session(reactor)
|
||||
@ -275,6 +285,7 @@ function svsessions.get_reactor_session(reactor)
|
||||
end
|
||||
|
||||
-- establish a new PLC session
|
||||
---@nodiscard
|
||||
---@param local_port integer
|
||||
---@param remote_port integer
|
||||
---@param for_reactor integer
|
||||
@ -314,6 +325,7 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
|
||||
end
|
||||
|
||||
-- establish a new RTU session
|
||||
---@nodiscard
|
||||
---@param local_port integer
|
||||
---@param remote_port integer
|
||||
---@param advertisement table
|
||||
@ -344,6 +356,7 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement
|
||||
end
|
||||
|
||||
-- establish a new coordinator session
|
||||
---@nodiscard
|
||||
---@param local_port integer
|
||||
---@param remote_port integer
|
||||
---@param version string
|
||||
|
@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions")
|
||||
local config = require("supervisor.config")
|
||||
local supervisor = require("supervisor.supervisor")
|
||||
|
||||
local SUPERVISOR_VERSION = "beta-v0.12.2"
|
||||
local SUPERVISOR_VERSION = "v0.13.1"
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -81,7 +81,7 @@ local function main()
|
||||
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
println("boot> wireless modem not found")
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
return
|
||||
end
|
||||
@ -110,7 +110,7 @@ local function main()
|
||||
-- we only care if this is our wireless modem
|
||||
if device == modem then
|
||||
println_ts("wireless modem disconnected!")
|
||||
log.error("comms modem disconnected!")
|
||||
log.warning("comms modem disconnected")
|
||||
else
|
||||
log.warning("non-comms modem disconnected")
|
||||
end
|
||||
@ -127,9 +127,9 @@ local function main()
|
||||
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
|
||||
end
|
||||
|
@ -6,10 +6,10 @@ local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local supervisor = {}
|
||||
|
||||
local PROTOCOLS = comms.PROTOCOLS
|
||||
local DEVICE_TYPES = comms.DEVICE_TYPES
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
@ -17,6 +17,7 @@ local print_ts = util.print_ts
|
||||
local println_ts = util.println_ts
|
||||
|
||||
-- supervisory controller communications
|
||||
---@nodiscard
|
||||
---@param version string supervisor version
|
||||
---@param num_reactors integer number of reactors
|
||||
---@param cooling_conf table cooling configuration table
|
||||
@ -26,32 +27,24 @@ local println_ts = util.println_ts
|
||||
---@param range integer trusted device connection range
|
||||
function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen, coord_listen, range)
|
||||
local self = {
|
||||
version = version,
|
||||
num_reactors = num_reactors,
|
||||
modem = modem,
|
||||
dev_listen = dev_listen,
|
||||
coord_listen = coord_listen,
|
||||
reactor_struct_cache = nil
|
||||
last_est_acks = {}
|
||||
}
|
||||
|
||||
---@class superv_comms
|
||||
local public = {}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
self.modem.closeAll()
|
||||
self.modem.open(self.dev_listen)
|
||||
self.modem.open(self.coord_listen)
|
||||
modem.closeAll()
|
||||
modem.open(dev_listen)
|
||||
modem.open(coord_listen)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
|
||||
-- link modem to svsessions
|
||||
svsessions.init(self.modem, num_reactors, cooling_conf)
|
||||
svsessions.init(modem, num_reactors, cooling_conf)
|
||||
|
||||
-- send an establish request response to a PLC/RTU
|
||||
---@param dest integer
|
||||
@ -60,10 +53,10 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg)
|
||||
s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
|
||||
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(dest, self.dev_listen, s_pkt.raw_sendable())
|
||||
modem.transmit(dest, dev_listen, s_pkt.raw_sendable())
|
||||
end
|
||||
|
||||
-- send coordinator connection establish response
|
||||
@ -74,24 +67,27 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
local s_pkt = comms.scada_packet()
|
||||
local c_pkt = comms.mgmt_packet()
|
||||
|
||||
c_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg)
|
||||
s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, c_pkt.raw_sendable())
|
||||
c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
|
||||
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable())
|
||||
|
||||
self.modem.transmit(dest, self.coord_listen, s_pkt.raw_sendable())
|
||||
modem.transmit(dest, coord_listen, s_pkt.raw_sendable())
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class superv_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
|
||||
svsessions.relink_modem(self.modem)
|
||||
---@param new_modem table
|
||||
function public.reconnect_modem(new_modem)
|
||||
modem = new_modem
|
||||
svsessions.relink_modem(new_modem)
|
||||
_conf_channels()
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
---@nodiscard
|
||||
---@param side string
|
||||
---@param sender integer
|
||||
---@param reply_to integer
|
||||
@ -107,25 +103,25 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
-- get as MODBUS TCP packet
|
||||
if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then
|
||||
if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
|
||||
local m_pkt = comms.modbus_packet()
|
||||
if m_pkt.decode(s_pkt) then
|
||||
pkt = m_pkt.get()
|
||||
end
|
||||
-- get as RPLC packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.RPLC then
|
||||
elseif s_pkt.protocol() == PROTOCOL.RPLC then
|
||||
local rplc_pkt = comms.rplc_packet()
|
||||
if rplc_pkt.decode(s_pkt) then
|
||||
pkt = rplc_pkt.get()
|
||||
end
|
||||
-- get as SCADA management packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
if mgmt_pkt.decode(s_pkt) then
|
||||
pkt = mgmt_pkt.get()
|
||||
end
|
||||
-- get as coordinator packet
|
||||
elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
local crdn_pkt = comms.crdn_packet()
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
@ -147,8 +143,9 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
-- device (RTU/PLC) listening channel
|
||||
if l_port == self.dev_listen then
|
||||
if protocol == PROTOCOLS.MODBUS_TCP then
|
||||
if l_port == dev_listen then
|
||||
if protocol == PROTOCOL.MODBUS_TCP then
|
||||
---@cast packet modbus_frame
|
||||
-- look for an associated session
|
||||
local session = svsessions.find_rtu_session(r_port)
|
||||
|
||||
@ -160,7 +157,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug("discarding MODBUS_TCP packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOLS.RPLC then
|
||||
elseif protocol == PROTOCOL.RPLC then
|
||||
---@cast packet rplc_frame
|
||||
-- look for an associated session
|
||||
local session = svsessions.find_plc_session(r_port)
|
||||
|
||||
@ -173,7 +171,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink")
|
||||
_send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY })
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_MGMT then
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
-- look for an associated session
|
||||
local session = svsessions.find_device_session(r_port)
|
||||
|
||||
@ -181,7 +180,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- establish a new session
|
||||
local next_seq_id = packet.scada_frame.seq_num() + 1
|
||||
|
||||
@ -192,13 +191,13 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
local dev_type = packet.data[3]
|
||||
|
||||
if comms_v ~= comms.version then
|
||||
log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v,
|
||||
" (expected v", comms.version, ")"))
|
||||
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||
return
|
||||
end
|
||||
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info(util.c("dropping device establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
|
||||
self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
|
||||
end
|
||||
|
||||
if dev_type == DEVICE_TYPES.PLC then
|
||||
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||
elseif dev_type == DEVICE_TYPE.PLC then
|
||||
-- PLC linking request
|
||||
if packet.length == 4 and type(packet.data[4]) == "number" then
|
||||
local reactor_id = packet.data[4]
|
||||
@ -206,19 +205,25 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
|
||||
if plc_id == false then
|
||||
-- reactor already has a PLC assigned
|
||||
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
|
||||
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
|
||||
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
|
||||
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
|
||||
end
|
||||
|
||||
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
|
||||
else
|
||||
-- got an ID; assigned to a reactor successfully
|
||||
println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected"))
|
||||
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [:", r_port, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
|
||||
|
||||
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
|
||||
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
|
||||
end
|
||||
else
|
||||
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
|
||||
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||
end
|
||||
elseif dev_type == DEVICE_TYPES.RTU then
|
||||
elseif dev_type == DEVICE_TYPE.RTU then
|
||||
if packet.length == 4 then
|
||||
-- this is an RTU advertisement for a new session
|
||||
local rtu_advert = packet.data[4]
|
||||
@ -226,6 +231,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
|
||||
println(util.c("RTU (", firmware_v, ") [:", r_port, "] \xbb connected"))
|
||||
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
|
||||
|
||||
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
|
||||
else
|
||||
log.debug("RTU_ESTABLISH: packet length mismatch")
|
||||
@ -247,16 +253,17 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
log.debug("illegal packet type " .. protocol .. " on device listening channel")
|
||||
end
|
||||
-- coordinator listening channel
|
||||
elseif l_port == self.coord_listen then
|
||||
elseif l_port == coord_listen then
|
||||
-- look for an associated session
|
||||
local session = svsessions.find_coord_session(r_port)
|
||||
|
||||
if protocol == PROTOCOLS.SCADA_MGMT then
|
||||
if protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
-- SCADA management packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
-- establish a new session
|
||||
local next_seq_id = packet.scada_frame.seq_num() + 1
|
||||
|
||||
@ -267,32 +274,39 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
local dev_type = packet.data[3]
|
||||
|
||||
if comms_v ~= comms.version then
|
||||
log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v,
|
||||
" (expected v", comms.version, ")"))
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||
return
|
||||
elseif dev_type ~= DEVICE_TYPES.CRDN then
|
||||
log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel"))
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||
return
|
||||
end
|
||||
|
||||
-- this is an attempt to establish a new session
|
||||
local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
|
||||
|
||||
if s_id ~= false then
|
||||
local config = { self.num_reactors }
|
||||
for i = 1, #cooling_conf do
|
||||
table.insert(config, cooling_conf[i].BOILERS)
|
||||
table.insert(config, cooling_conf[i].TURBINES)
|
||||
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
|
||||
self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
|
||||
end
|
||||
|
||||
println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected"))
|
||||
log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config })
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||
elseif dev_type ~= DEVICE_TYPE.CRDN then
|
||||
log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel"))
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||
else
|
||||
log.debug("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator")
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
|
||||
-- this is an attempt to establish a new session
|
||||
local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
|
||||
|
||||
if s_id ~= false then
|
||||
local config = { num_reactors }
|
||||
for i = 1, #cooling_conf do
|
||||
table.insert(config, cooling_conf[i].BOILERS)
|
||||
table.insert(config, cooling_conf[i].TURBINES)
|
||||
end
|
||||
|
||||
println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected"))
|
||||
log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
|
||||
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config })
|
||||
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
|
||||
else
|
||||
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
|
||||
log.info("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator")
|
||||
self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
|
||||
end
|
||||
|
||||
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
|
||||
end
|
||||
end
|
||||
else
|
||||
log.debug("CRDN_ESTABLISH: establish packet length mismatch")
|
||||
@ -302,7 +316,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOLS.SCADA_CRDN then
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
-- coordinator packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
|
@ -11,21 +11,16 @@ local rsctl = require("supervisor.session.rsctl")
|
||||
---@class reactor_control_unit
|
||||
local unit = {}
|
||||
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local DUMPING_MODE = types.DUMPING_MODE
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local ALARM = types.ALARM
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
|
||||
local IO = rsio.IO
|
||||
|
||||
local FLOW_STABILITY_DELAY_MS = 15000
|
||||
|
||||
local DT_KEYS = {
|
||||
ReactorBurnR = "RBR",
|
||||
ReactorTemp = "RTP",
|
||||
@ -41,18 +36,16 @@ local DT_KEYS = {
|
||||
TurbinePower = "TPR"
|
||||
}
|
||||
|
||||
---@alias ALARM_INT_STATE integer
|
||||
---@enum ALARM_INT_STATE
|
||||
local AISTATE = {
|
||||
INACTIVE = 0,
|
||||
TRIPPING = 1,
|
||||
TRIPPED = 2,
|
||||
ACKED = 3,
|
||||
RING_BACK = 4,
|
||||
RING_BACK_TRIPPING = 5
|
||||
INACTIVE = 1,
|
||||
TRIPPING = 2,
|
||||
TRIPPED = 3,
|
||||
ACKED = 4,
|
||||
RING_BACK = 5,
|
||||
RING_BACK_TRIPPING = 6
|
||||
}
|
||||
|
||||
unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS
|
||||
|
||||
---@class alarm_def
|
||||
---@field state ALARM_INT_STATE internal alarm state
|
||||
---@field trip_time integer time (ms) when first tripped
|
||||
@ -61,19 +54,19 @@ unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS
|
||||
---@field tier integer alarm urgency tier (0 = highest)
|
||||
|
||||
-- create a new reactor unit
|
||||
---@param for_reactor integer reactor unit number
|
||||
---@nodiscard
|
||||
---@param reactor_id integer reactor unit number
|
||||
---@param num_boilers integer number of boilers expected
|
||||
---@param num_turbines integer number of turbines expected
|
||||
function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
---@class _unit_self
|
||||
local self = {
|
||||
r_id = for_reactor,
|
||||
r_id = reactor_id,
|
||||
plc_s = nil, ---@class plc_session_struct
|
||||
plc_i = nil, ---@class plc_session
|
||||
num_boilers = num_boilers,
|
||||
num_turbines = num_turbines,
|
||||
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
|
||||
defs = { FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS },
|
||||
-- rtus
|
||||
redstone = {},
|
||||
boilers = {},
|
||||
@ -278,6 +271,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
local function _reset_dt(key) self.deltas[key] = nil end
|
||||
|
||||
-- get the delta t of a value
|
||||
---@nodiscard
|
||||
---@param key string value key
|
||||
---@return number value value or 0 if not known
|
||||
function self._get_dt(key) if self.deltas[key] then return self.deltas[key].dt else return 0.0 end end
|
||||
@ -326,7 +320,6 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
--#region redstone I/O
|
||||
|
||||
local __rs_w = self.io_ctl.digital_write
|
||||
local __rs_r = self.io_ctl.digital_read
|
||||
|
||||
-- valves
|
||||
local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end }
|
||||
@ -525,9 +518,9 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
end
|
||||
end
|
||||
|
||||
-- get the actual limit of this unit
|
||||
--
|
||||
-- get the actual limit of this unit<br>
|
||||
-- if it is degraded or not ready, the limit will be 0
|
||||
---@nodiscard
|
||||
---@return integer lim_br100
|
||||
function public.a_get_effective_limit()
|
||||
if not self.db.control.ready or self.db.control.degraded or self.plc_cache.rps_trip then
|
||||
@ -551,6 +544,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
end
|
||||
|
||||
-- check if ramping is complete (burn rate is same as target)
|
||||
---@nodiscard
|
||||
---@return boolean complete
|
||||
function public.a_ramp_complete()
|
||||
if self.plc_i ~= nil then
|
||||
@ -610,7 +604,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
-- acknowledge an alarm (if possible)
|
||||
---@param id ALARM alarm ID
|
||||
function public.ack_alarm(id)
|
||||
if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.TRIPPED) then
|
||||
if type(id) == "number" and self.db.alarm_states[id] == ALARM_STATE.TRIPPED then
|
||||
self.db.alarm_states[id] = ALARM_STATE.ACKED
|
||||
end
|
||||
end
|
||||
@ -618,7 +612,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
-- reset an alarm (if possible)
|
||||
---@param id ALARM alarm ID
|
||||
function public.reset_alarm(id)
|
||||
if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.RING_BACK) then
|
||||
if type(id) == "number" and self.db.alarm_states[id] == ALARM_STATE.RING_BACK then
|
||||
self.db.alarm_states[id] = ALARM_STATE.INACTIVE
|
||||
end
|
||||
end
|
||||
@ -675,6 +669,8 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
--#region
|
||||
|
||||
-- check if a critical alarm is tripped
|
||||
---@nodiscard
|
||||
---@return boolean tripped
|
||||
function public.has_critical_alarm()
|
||||
for _, alarm in pairs(self.alarms) do
|
||||
if alarm.tier == PRIO.CRITICAL and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then
|
||||
@ -686,6 +682,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
end
|
||||
|
||||
-- get build properties of all machines
|
||||
---@nodiscard
|
||||
---@param inc_plc boolean? true/nil to include PLC build, false to exclude
|
||||
---@param inc_boilers boolean? true/nil to include boiler builds, false to exclude
|
||||
---@param inc_turbines boolean? true/nil to include turbine builds, false to exclude
|
||||
@ -718,6 +715,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
end
|
||||
|
||||
-- get reactor status
|
||||
---@nodiscard
|
||||
function public.get_reactor_status()
|
||||
local status = {}
|
||||
if self.plc_i ~= nil then
|
||||
@ -728,6 +726,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
end
|
||||
|
||||
-- get RTU statuses
|
||||
---@nodiscard
|
||||
function public.get_rtu_statuses()
|
||||
local status = {}
|
||||
|
||||
@ -769,20 +768,25 @@ function unit.new(for_reactor, num_boilers, num_turbines)
|
||||
end
|
||||
|
||||
-- get the annunciator status
|
||||
---@nodiscard
|
||||
function public.get_annunciator() return self.db.annunciator end
|
||||
|
||||
-- get the alarm states
|
||||
---@nodiscard
|
||||
function public.get_alarms() return self.db.alarm_states end
|
||||
|
||||
-- get information required for automatic reactor control
|
||||
---@nodiscard
|
||||
function public.get_control_inf() return self.db.control end
|
||||
|
||||
-- get unit state
|
||||
---@nodiscard
|
||||
function public.get_state()
|
||||
return { self.status_text[1], self.status_text[2], self.waste_mode, self.db.control.ready, self.db.control.degraded }
|
||||
end
|
||||
|
||||
-- get the reactor ID
|
||||
---@nodiscard
|
||||
function public.get_id() return self.r_id end
|
||||
|
||||
--#endregion
|
||||
|
@ -1,3 +1,4 @@
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local types = require("scada-common.types")
|
||||
@ -5,17 +6,16 @@ local util = require("scada-common.util")
|
||||
|
||||
local plc = require("supervisor.session.plc")
|
||||
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local TRI_FAIL = types.TRI_FAIL
|
||||
local DUMPING_MODE = types.DUMPING_MODE
|
||||
local PRIO = types.ALARM_PRIORITY
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
local IO = rsio.IO
|
||||
|
||||
local PLC_S_CMDS = plc.PLC_S_CMDS
|
||||
|
||||
local aistate_string = {
|
||||
local AISTATE_NAMES = {
|
||||
"INACTIVE",
|
||||
"TRIPPING",
|
||||
"TRIPPED",
|
||||
@ -24,11 +24,10 @@ local aistate_string = {
|
||||
"RING_BACK_TRIPPING"
|
||||
}
|
||||
|
||||
-- background radiation 0.0000001 Sv/h (99.99 nSv/h)
|
||||
-- "green tint" radiation 0.00001 Sv/h (10 uSv/h)
|
||||
-- damaging radiation 0.00006 Sv/h (60 uSv/h)
|
||||
local RADIATION_ALERT_LEVEL = 0.00001 -- 10 uSv/h
|
||||
local RADIATION_ALARM_LEVEL = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good
|
||||
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
|
||||
|
||||
local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
|
||||
local ALARM_LIMS = const.ALARM_LIMITS
|
||||
|
||||
---@class unit_logic_extension
|
||||
local logic = {}
|
||||
@ -108,15 +107,15 @@ function logic.update_annunciator(self)
|
||||
|
||||
-- update other annunciator fields
|
||||
self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped
|
||||
self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.manual
|
||||
self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.automatic
|
||||
self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
|
||||
self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC
|
||||
self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.no_cool)
|
||||
self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < -2.0
|
||||
self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.4
|
||||
self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000
|
||||
self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100
|
||||
self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= 0.01
|
||||
self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= 0.85
|
||||
self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < ANNUNC_LIMS.RCSFlowLow
|
||||
self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
|
||||
self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
|
||||
self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
|
||||
self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
|
||||
self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
|
||||
|
||||
-- this warning applies when no coolant is buffered (which we can't easily determine without running)
|
||||
--[[
|
||||
@ -129,7 +128,7 @@ function logic.update_annunciator(self)
|
||||
such as when a burn rate consumes half the coolant in the tank, meaning that:
|
||||
50% at some point will be in the boiler, and 50% in a tube, so that leaves 0% in the reactor
|
||||
]]--
|
||||
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.fluid.sodium, 200000, 20000)
|
||||
local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
|
||||
local high_rate = (plc_db.mek_status.ccool_amnt / (plc_db.mek_status.burn_rate * heating_rate_conv)) < 4
|
||||
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate
|
||||
|
||||
@ -150,7 +149,7 @@ function logic.update_annunciator(self)
|
||||
for i = 1, #self.envd do
|
||||
local envd = self.envd[i] ---@type unit_session
|
||||
self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3)
|
||||
self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > RADIATION_ALERT_LEVEL
|
||||
self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > ANNUNC_LIMS.RadiationWarning
|
||||
break
|
||||
end
|
||||
|
||||
@ -299,7 +298,7 @@ function logic.update_annunciator(self)
|
||||
self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
|
||||
|
||||
-- check for steam feed mismatch and max return rate
|
||||
local sfmismatch = math.abs(total_flow_rate - total_input_rate) > 10
|
||||
local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch
|
||||
sfmismatch = sfmismatch or boiler_steam_dt_sum > 2.0 or boiler_water_dt_sum < -2.0
|
||||
self.db.annunciator.SteamFeedMismatch = sfmismatch
|
||||
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
|
||||
@ -367,8 +366,8 @@ local function _update_alarm_state(self, tripped, alarm)
|
||||
else
|
||||
alarm.state = AISTATE.TRIPPED
|
||||
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
|
||||
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ",
|
||||
types.alarm_prio_string[alarm.tier + 1],"]"))
|
||||
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
|
||||
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
|
||||
end
|
||||
else
|
||||
alarm.trip_time = util.time_ms()
|
||||
@ -381,8 +380,8 @@ local function _update_alarm_state(self, tripped, alarm)
|
||||
if elapsed > (alarm.hold_time * 1000) then
|
||||
alarm.state = AISTATE.TRIPPED
|
||||
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
|
||||
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ",
|
||||
types.alarm_prio_string[alarm.tier + 1],"]"))
|
||||
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
|
||||
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
|
||||
end
|
||||
elseif int_state == AISTATE.RING_BACK_TRIPPING then
|
||||
alarm.trip_time = 0
|
||||
@ -431,8 +430,8 @@ local function _update_alarm_state(self, tripped, alarm)
|
||||
|
||||
-- check for state change
|
||||
if alarm.state ~= int_state then
|
||||
local change_str = util.c(aistate_string[int_state + 1], " -> ", aistate_string[alarm.state + 1])
|
||||
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): ", change_str))
|
||||
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
|
||||
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
|
||||
end
|
||||
end
|
||||
|
||||
@ -449,7 +448,7 @@ function logic.update_alarms(self)
|
||||
-- Containment Radiation
|
||||
local rad_alarm = false
|
||||
for i = 1, #self.envd do
|
||||
rad_alarm = self.envd[i].get_db().radiation_raw > RADIATION_ALARM_LEVEL
|
||||
rad_alarm = self.envd[i].get_db().radiation_raw > ALARM_LIMS.HIGH_RADIATION
|
||||
break
|
||||
end
|
||||
_update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation)
|
||||
@ -469,14 +468,14 @@ function logic.update_alarms(self)
|
||||
_update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp)
|
||||
|
||||
-- High Temperature
|
||||
_update_alarm_state(self, plc_cache.temp > 1150, self.alarms.ReactorHighTemp)
|
||||
_update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp)
|
||||
|
||||
-- Waste Leak
|
||||
_update_alarm_state(self, plc_cache.waste >= 0.99, self.alarms.ReactorWasteLeak)
|
||||
_update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak)
|
||||
|
||||
-- High Waste
|
||||
local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste
|
||||
_update_alarm_state(self, (plc_cache.waste > 0.50) or rps_high_waste, self.alarms.ReactorHighWaste)
|
||||
_update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste)
|
||||
|
||||
-- RPS Transient (excludes timeouts and manual trips)
|
||||
local rps_alarm = false
|
||||
@ -501,7 +500,7 @@ function logic.update_alarms(self)
|
||||
-- annunciator indicators for these states may not indicate a real issue when:
|
||||
-- > flow is ramping up right after reactor start
|
||||
-- > flow is ramping down after reactor shutdown
|
||||
if ((util.time_ms() - self.last_rate_change_ms) > self.defs.FLOW_STABILITY_DELAY_MS) and plc_cache.active then
|
||||
if ((util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS) and plc_cache.active then
|
||||
rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch
|
||||
end
|
||||
|
||||
@ -530,8 +529,8 @@ function logic.update_auto_safety(public, self)
|
||||
for _, alarm in pairs(self.alarms) do
|
||||
if alarm.tier <= PRIO.URGENT and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then
|
||||
if not self.auto_was_alarmed then
|
||||
log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.alarm_string[alarm.id], ") [PRIORITY ",
|
||||
types.alarm_prio_string[alarm.tier + 1],"]"))
|
||||
log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], ") [PRIORITY ",
|
||||
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
|
||||
end
|
||||
|
||||
alarmed = true
|
||||
@ -555,6 +554,7 @@ function logic.update_status_text(self)
|
||||
local AISTATE = self.types.AISTATE
|
||||
|
||||
-- check if an alarm is active (tripped or ack'd)
|
||||
---@nodiscard
|
||||
---@param alarm table alarm entry
|
||||
---@return boolean active
|
||||
local function is_active(alarm)
|
||||
@ -620,7 +620,7 @@ function logic.update_status_text(self)
|
||||
self.status_text[2] = "insufficient fuel input rate"
|
||||
elseif self.db.annunciator.WasteLineOcclusion then
|
||||
self.status_text[2] = "insufficient waste output rate"
|
||||
elseif (util.time_ms() - self.last_rate_change_ms) <= self.defs.FLOW_STABILITY_DELAY_MS then
|
||||
elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
|
||||
self.status_text[2] = "awaiting flow stability"
|
||||
else
|
||||
self.status_text[2] = "system nominal"
|
||||
@ -636,9 +636,9 @@ function logic.update_status_text(self)
|
||||
cause = "core temperature high"
|
||||
elseif plc_db.rps_trip_cause == "no_coolant" then
|
||||
cause = "insufficient coolant"
|
||||
elseif plc_db.rps_trip_cause == "full_waste" then
|
||||
elseif plc_db.rps_trip_cause == "ex_waste" then
|
||||
cause = "excess waste"
|
||||
elseif plc_db.rps_trip_cause == "heated_coolant_backup" then
|
||||
elseif plc_db.rps_trip_cause == "ex_heated_coolant" then
|
||||
cause = "excess heated coolant"
|
||||
elseif plc_db.rps_trip_cause == "no_fuel" then
|
||||
cause = "insufficient fuel"
|
||||
@ -670,7 +670,7 @@ function logic.update_status_text(self)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.status_text = { "Reactor Off-line", "awaiting connection..." }
|
||||
self.status_text = { "REACTOR OFF-LINE", "awaiting connection..." }
|
||||
end
|
||||
end
|
||||
|
||||
@ -680,6 +680,7 @@ function logic.handle_redstone(self)
|
||||
local AISTATE = self.types.AISTATE
|
||||
|
||||
-- check if an alarm is active (tripped or ack'd)
|
||||
---@nodiscard
|
||||
---@param alarm table alarm entry
|
||||
---@return boolean active
|
||||
local function is_active(alarm)
|
||||
|
Loading…
Reference in New Issue
Block a user