cc-mek-scada/rtu/threads.lua

458 lines
19 KiB
Lua
Raw Normal View History

2023-02-22 04:50:43 +00:00
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")
2023-02-22 04:50:43 +00:00
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")
2022-05-11 15:31:02 +00:00
2023-02-22 04:50:43 +00:00
local modbus = require("rtu.modbus")
local threads = {}
2023-02-21 17:27:16 +00:00
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
2022-04-27 16:46:04 +00:00
local println_ts = util.println_ts
2022-04-27 21:59:25 +00:00
local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
2022-04-27 16:46:04 +00:00
-- main thread
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param smem rtu_shared_memory
function threads.thread__main(smem)
2023-02-21 17:27:16 +00:00
---@class parallel_thread
local public = {}
2022-04-27 16:46:04 +00:00
-- execute thread
function public.exec()
log.debug("main thread start")
2022-04-27 19:52:34 +00:00
2022-05-10 17:06:13 +00:00
-- main loop clock
local loop_clock = util.new_clock(MAIN_CLOCK)
2022-04-27 16:46:04 +00:00
-- load in from shared memory
2022-04-29 14:19:05 +00:00
local rtu_state = smem.rtu_state
local rtu_dev = smem.rtu_dev
local rtu_comms = smem.rtu_sys.rtu_comms
2022-05-11 16:03:15 +00:00
local conn_watchdog = smem.rtu_sys.conn_watchdog
2022-04-29 14:19:05 +00:00
local units = smem.rtu_sys.units
2022-04-27 16:46:04 +00:00
-- start unlinked (in case of restart)
rtu_comms.unlink(rtu_state)
2022-05-10 17:06:13 +00:00
-- start clock
loop_clock.start()
2022-04-27 16:46:04 +00:00
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
2022-04-27 16:46:04 +00:00
2022-05-10 17:06:13 +00:00
if event == "timer" and loop_clock.is_clock(param1) then
2022-04-29 14:19:05 +00:00
-- start next clock timer
2022-05-10 17:06:13 +00:00
loop_clock.start()
2022-04-29 14:19:05 +00:00
-- period tick, if we are not linked send establish request
2022-05-10 17:06:13 +00:00
if not rtu_state.linked then
2022-04-29 14:19:05 +00:00
-- advertise units
rtu_comms.send_establish(units)
2022-04-29 14:19:05 +00:00
end
elseif event == "modem_message" then
-- got a packet
local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then
-- pass the packet onto the comms message queue
smem.q.mq_comms.push_packet(packet)
end
2022-05-10 17:06:13 +00:00
elseif event == "timer" and conn_watchdog.is_timer(param1) then
2022-04-29 14:19:05 +00:00
-- haven't heard from server recently? unlink
rtu_comms.unlink(rtu_state)
elseif event == "peripheral_detach" then
2022-04-27 16:46:04 +00:00
-- handle loss of a device
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
-- we only care if this is our wireless modem
if device == rtu_dev.modem then
println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected!")
else
log.warning("non-comms modem disconnected")
end
else
for i = 1, #units do
-- find disconnected device
if units[i].device == device then
-- we are going to let the PPM prevent crashes
-- return fault flags/codes to MODBUS queries
local unit = units[i]
2023-02-21 17:27:16 +00:00
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
2022-04-27 16:46:04 +00:00
end
end
elseif event == "peripheral" then
-- peripheral connect
2022-04-27 16:46:04 +00:00
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
if type == "modem" then
if device.isWireless() then
-- reconnected modem
rtu_dev.modem = device
rtu_comms.reconnect_modem(rtu_dev.modem)
println_ts("wireless modem reconnected.")
2023-02-22 04:50:43 +00:00
log.info("comms modem reconnected")
else
2023-02-22 04:50:43 +00:00
log.info("wired modem reconnected")
end
else
-- relink lost peripheral to correct unit entry
for i = 1, #units do
2022-05-11 16:03:15 +00:00
local unit = units[i] ---@type rtu_unit_registry_entry
2022-04-27 16:46:04 +00:00
-- find disconnected device to reconnect
-- note: cannot check isFormed as that would yield this coroutine and consume events
if unit.name == param1 then
local resend_advert = false
-- found, re-link
unit.device = device
2023-02-21 17:27:16 +00:00
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
2023-02-21 17:27:16 +00:00
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
2023-02-21 17:27:16 +00:00
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
2023-02-21 17:27:16 +00:00
unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
2023-02-21 17:27:16 +00:00
unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
2023-02-21 17:27:16 +00:00
unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
2023-02-21 17:27:16 +00:00
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
2023-02-21 17:27:16 +00:00
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)
2023-02-21 17:27:16 +00:00
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)
2023-02-21 17:27:16 +00:00
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)
2023-02-21 17:27:16 +00:00
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)
2023-02-21 17:27:16 +00:00
elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu = sna_rtu.new(device)
2023-02-21 17:27:16 +00:00
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)
end
if unit.is_multiblock and (unit.formed == false) then
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
end
2022-05-11 16:03:15 +00:00
unit.modbus_io = modbus.new(unit.rtu, true)
2023-02-21 17:27:16 +00:00
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)
else
rtu_comms.send_remounted(unit.uid)
end
end
end
2022-04-27 16:46:04 +00:00
end
end
end
-- check for termination request
if event == "terminate" or ppm.should_terminate() then
rtu_state.shutdown = true
log.info("terminate requested, main thread exiting")
2022-04-27 16:46:04 +00:00
break
end
end
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
if not rtu_state.shutdown then
log.info("main thread restarting in 5 seconds...")
util.psleep(5)
end
end
end
return public
2022-04-27 16:46:04 +00:00
end
-- communications handler thread
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param smem rtu_shared_memory
function threads.thread__comms(smem)
2023-02-21 17:27:16 +00:00
---@class parallel_thread
local public = {}
2022-04-27 16:46:04 +00:00
-- execute thread
function public.exec()
log.debug("comms thread start")
2022-04-27 19:52:34 +00:00
2022-04-27 16:46:04 +00:00
-- load in from shared memory
2023-02-22 04:50:43 +00:00
local rtu_state = smem.rtu_state
local rtu_comms = smem.rtu_sys.rtu_comms
local units = smem.rtu_sys.units
2022-04-27 16:46:04 +00:00
2023-02-22 04:50:43 +00:00
local comms_queue = smem.q.mq_comms
2022-04-27 16:46:04 +00:00
2023-02-22 04:50:43 +00:00
local last_update = util.time()
2022-04-27 16:46:04 +00:00
-- thread loop
while true do
-- check for messages in the message queue
while comms_queue.ready() and not rtu_state.shutdown do
2022-04-27 16:46:04 +00:00
local msg = comms_queue.pop()
2022-05-11 16:03:15 +00:00
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
elseif msg.qtype == mqueue.TYPE.PACKET then
-- received a packet
-- handle the packet (rtu_state passed to allow setting link flag)
rtu_comms.handle_packet(msg.message, units, rtu_state)
end
2022-04-27 16:46:04 +00:00
end
2022-04-27 21:59:25 +00:00
-- quick yield
util.nop()
2022-04-27 16:46:04 +00:00
end
-- check for termination request
if rtu_state.shutdown then
rtu_comms.close(rtu_state)
log.info("comms thread exiting")
2022-04-27 16:46:04 +00:00
break
end
-- delay before next check
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
2022-04-27 16:46:04 +00:00
end
end
2022-04-27 19:52:34 +00:00
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
if not rtu_state.shutdown then
log.info("comms thread restarting in 5 seconds...")
util.psleep(5)
end
end
end
return public
2022-04-27 16:46:04 +00:00
end
2022-04-29 17:19:01 +00:00
-- per-unit communications handler thread
2023-02-22 04:50:43 +00:00
---@nodiscard
2022-05-11 16:03:15 +00:00
---@param smem rtu_shared_memory
---@param unit rtu_unit_registry_entry
function threads.thread__unit_comms(smem, unit)
2023-02-21 17:27:16 +00:00
---@class parallel_thread
local public = {}
2022-04-29 17:19:01 +00:00
-- execute thread
function public.exec()
2023-02-21 17:27:16 +00:00
log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), "(", unit.name, ")"))
2022-04-29 17:19:01 +00:00
-- load in from shared memory
local rtu_state = smem.rtu_state
2022-05-10 16:01:56 +00:00
local rtu_comms = smem.rtu_sys.rtu_comms
2022-04-29 17:19:01 +00:00
local packet_queue = unit.pkt_queue
local last_update = util.time()
local last_f_check = 0
2023-02-21 17:27:16 +00:00
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)
return
end
2022-04-29 17:19:01 +00:00
-- thread loop
while true do
-- check for messages in the message queue
while packet_queue.ready() and not rtu_state.shutdown do
local msg = packet_queue.pop()
2022-05-11 16:03:15 +00:00
if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command
elseif msg.qtype == mqueue.TYPE.DATA then
-- received data
elseif msg.qtype == mqueue.TYPE.PACKET then
-- received a packet
local _, reply = unit.modbus_io.handle_packet(msg.message)
rtu_comms.send_modbus(reply)
end
2022-04-29 17:19:01 +00:00
end
-- quick yield
util.nop()
end
-- check if multiblock is still formed if this is a multiblock
if unit.is_multiblock and (util.time_ms() - last_f_check > 250) then
2022-11-28 03:44:47 +00:00
local is_formed = unit.device.isFormed()
last_f_check = util.time_ms()
if unit.formed == nil then unit.formed = is_formed end
2022-11-28 03:44:47 +00:00
if (not unit.formed) and is_formed then
-- newly re-formed
local iface = ppm.get_iface(unit.device)
if iface then
log.info(util.c("unmounting and remounting reformed RTU unit ", detail_name))
ppm.unmount(unit.device)
local type, device = ppm.mount(iface)
local faulted = false
if device ~= nil then
2023-02-21 17:27:16 +00:00
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler multiblock
unit.device = device
unit.rtu, faulted = boilerv_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
2023-02-21 17:27:16 +00:00
elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
-- turbine multiblock
unit.device = device
unit.rtu, faulted = turbinev_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
2023-02-21 17:27:16 +00:00
elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock
unit.device = device
unit.rtu, faulted = imatrix_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
2023-02-21 17:27:16 +00:00
elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
-- SPS multiblock
unit.device = device
unit.rtu, faulted = sps_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
else
log.error("illegal remount of non-multiblock RTU attempted for " .. short_name, true)
end
if unit.formed and faulted then
-- something is still wrong = can't mark as formed yet
unit.formed = false
else
rtu_comms.send_remounted(unit.uid)
end
else
-- fully lost the peripheral now :(
log.error(util.c(unit.name, " lost (failed reconnect)"))
end
log.info(util.c("reconnected the ", unit.type, " on interface ", unit.name))
else
log.error("failed to get interface of previously connected RTU unit " .. detail_name, true)
end
end
2022-11-28 03:44:47 +00:00
unit.formed = is_formed
end
2022-04-29 17:19:01 +00:00
-- check for termination request
if rtu_state.shutdown then
log.info("rtu unit thread exiting -> " .. short_name)
2022-04-29 17:19:01 +00:00
break
end
-- delay before next check
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
end
end
-- execute the thread in a protected mode, retrying it on return if not shutting down
function public.p_exec()
local rtu_state = smem.rtu_state
while not rtu_state.shutdown do
local status, result = pcall(public.exec)
if status == false then
log.fatal(util.strval(result))
end
if not rtu_state.shutdown then
2023-02-21 17:27:16 +00:00
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
end
return public
2022-04-29 17:19:01 +00:00
end
return threads