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")
|
2023-06-03 21:40:57 +00:00
|
|
|
local tcd = require("scada-common.tcd")
|
2023-02-22 04:50:43 +00:00
|
|
|
local types = require("scada-common.types")
|
|
|
|
local util = require("scada-common.util")
|
2022-09-21 19:53:51 +00:00
|
|
|
|
2023-04-21 00:40:28 +00:00
|
|
|
local databus = require("rtu.databus")
|
|
|
|
local modbus = require("rtu.modbus")
|
|
|
|
local renderer = require("rtu.renderer")
|
2023-07-27 00:48:44 +00:00
|
|
|
local rtu = require("rtu.rtu")
|
2023-04-21 00:40:28 +00:00
|
|
|
|
2023-02-22 04:50:43 +00:00
|
|
|
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
|
2023-07-11 21:27:03 +00:00
|
|
|
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
|
2023-02-22 04:50:43 +00:00
|
|
|
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-04-21 00:40:28 +00:00
|
|
|
local core = require("graphics.core")
|
2022-05-04 17:37:01 +00:00
|
|
|
|
|
|
|
local threads = {}
|
|
|
|
|
2023-02-21 17:27:16 +00:00
|
|
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
2023-04-21 00:40:28 +00:00
|
|
|
local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
2022-04-27 16:46:04 +00:00
|
|
|
|
2023-07-29 21:57:51 +00:00
|
|
|
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
2022-05-15 00:07:26 +00:00
|
|
|
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
2022-04-27 16:46:04 +00:00
|
|
|
|
2023-11-06 14:25:44 +00:00
|
|
|
---@param smem rtu_shared_memory
|
|
|
|
---@param println_ts function
|
|
|
|
---@param iface string
|
|
|
|
---@param type string
|
|
|
|
---@param device table
|
|
|
|
---@param unit rtu_unit_registry_entry
|
|
|
|
local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
|
|
|
|
local sys = smem.rtu_sys
|
|
|
|
|
|
|
|
-- find disconnected device to reconnect
|
|
|
|
-- note: cannot check isFormed as that would yield this coroutine and consume events
|
|
|
|
if unit.name == iface then
|
2023-11-12 19:21:48 +00:00
|
|
|
local resend_advert, faulted, unknown, invalid = false, false, false, false
|
|
|
|
|
|
|
|
local function fail(msg)
|
|
|
|
invalid = true
|
|
|
|
log.error(msg .. " in config")
|
|
|
|
end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
-- found, re-link
|
|
|
|
unit.device = device
|
|
|
|
|
|
|
|
if unit.type == RTU_UNIT_TYPE.VIRTUAL then
|
|
|
|
resend_advert = true
|
|
|
|
if type == "boilerValve" then
|
|
|
|
-- boiler multiblock
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("boiler '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
|
|
|
if (unit.index == false) or unit.index < 1 or unit.index > 2 then fail(util.c("boiler '", unit.name, "' cannot init, invalid index provided")) end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
unit.type = RTU_UNIT_TYPE.BOILER_VALVE
|
|
|
|
elseif type == "turbineValve" then
|
|
|
|
-- turbine multiblock
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("turbine '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
|
|
|
if (unit.index == false) or unit.index < 1 or unit.index > 3 then fail(util.c("turbine '", unit.name, "' cannot init, invalid index provided")) end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
|
|
|
|
elseif type == "dynamicValve" then
|
|
|
|
-- dynamic tank multiblock
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("dynamic tank '", unit.name, "' cannot init, no valid assignment provided")) end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
if (unit.reactor == 0 and ((unit.index == false) or unit.index < 1 or unit.index > 4)) or
|
|
|
|
(unit.reactor > 0 and unit.index ~= 1) then
|
2023-11-12 19:21:48 +00:00
|
|
|
fail(util.c("dynamic tank '", unit.name, "' cannot init, invalid index provided"))
|
2023-11-06 14:25:44 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
|
|
|
|
elseif type == "inductionPort" then
|
|
|
|
-- induction matrix multiblock
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
unit.type = RTU_UNIT_TYPE.IMATRIX
|
|
|
|
elseif type == "spsPort" then
|
|
|
|
-- SPS multiblock
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor ~= 0 then fail(util.c("SPS '", unit.name, "' cannot init, not assigned to facility")) end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
unit.type = RTU_UNIT_TYPE.SPS
|
|
|
|
elseif type == "solarNeutronActivator" then
|
|
|
|
-- SNA
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
|
2023-11-06 14:25:44 +00:00
|
|
|
|
|
|
|
unit.type = RTU_UNIT_TYPE.SNA
|
|
|
|
elseif type == "environmentDetector" then
|
|
|
|
-- advanced peripherals environment detector
|
2023-11-12 19:21:48 +00:00
|
|
|
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
|
|
|
|
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
|
2023-11-12 16:54:47 +00:00
|
|
|
|
2023-11-06 14:25:44 +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
|
|
|
|
|
|
|
|
databus.tx_unit_hw_type(unit.uid, unit.type)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- if disconnected on startup, config wouldn't have been validated
|
|
|
|
-- checking now that it has connected; the config isn't valid, so don't connect it
|
|
|
|
if invalid then
|
|
|
|
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
|
|
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- note for multiblock structures: if not formed, indexing the multiblock functions results in a PPM fault
|
|
|
|
|
|
|
|
if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
|
|
|
unit.rtu, faulted = boilerv_rtu.new(device)
|
|
|
|
unit.formed = util.trinary(faulted, false, nil)
|
|
|
|
elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
|
|
|
|
unit.rtu, faulted = turbinev_rtu.new(device)
|
|
|
|
unit.formed = util.trinary(faulted, false, nil)
|
|
|
|
elseif unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
|
|
|
unit.rtu, faulted = dynamicv_rtu.new(device)
|
|
|
|
unit.formed = util.trinary(faulted, false, nil)
|
|
|
|
elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
|
|
|
|
unit.rtu, faulted = imatrix_rtu.new(device)
|
|
|
|
unit.formed = util.trinary(faulted, false, nil)
|
|
|
|
elseif unit.type == RTU_UNIT_TYPE.SPS then
|
|
|
|
unit.rtu, faulted = sps_rtu.new(device)
|
|
|
|
unit.formed = util.trinary(faulted, false, nil)
|
|
|
|
elseif unit.type == RTU_UNIT_TYPE.SNA then
|
|
|
|
unit.rtu, faulted = sna_rtu.new(device)
|
|
|
|
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
|
|
|
|
unit.rtu, faulted = envd_rtu.new(device)
|
|
|
|
else
|
|
|
|
unknown = true
|
|
|
|
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
|
|
|
|
end
|
|
|
|
|
|
|
|
if unit.is_multiblock then
|
|
|
|
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
|
|
|
if unit.formed == false then
|
|
|
|
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
|
|
|
end
|
|
|
|
elseif faulted then
|
|
|
|
unit.hw_state = UNIT_HW_STATE.FAULTED
|
|
|
|
elseif not unknown then
|
|
|
|
unit.hw_state = UNIT_HW_STATE.OK
|
|
|
|
else
|
|
|
|
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
|
|
|
end
|
|
|
|
|
|
|
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
|
|
|
|
|
|
|
if not unknown then
|
|
|
|
unit.modbus_io = modbus.new(unit.rtu, true)
|
|
|
|
|
|
|
|
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
|
|
|
|
sys.rtu_comms.send_advertisement(sys.units)
|
|
|
|
else
|
|
|
|
sys.rtu_comms.send_remounted(unit.uid)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
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
|
2022-05-31 18:54:55 +00:00
|
|
|
function threads.thread__main(smem)
|
2023-04-21 00:40:28 +00:00
|
|
|
-- print a log message to the terminal as long as the UI isn't running
|
|
|
|
local function println_ts(message) if not smem.rtu_state.fp_ok then util.println_ts(message) end end
|
|
|
|
|
2023-02-21 17:27:16 +00:00
|
|
|
---@class parallel_thread
|
|
|
|
local public = {}
|
2022-05-21 17:56:14 +00:00
|
|
|
|
2022-04-27 16:46:04 +00:00
|
|
|
-- execute thread
|
2022-05-31 18:54:55 +00:00
|
|
|
function public.exec()
|
2023-04-21 00:40:28 +00:00
|
|
|
databus.tx_rt_status("main", true)
|
2022-05-04 17:37:01 +00:00
|
|
|
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
|
2023-07-27 00:48:44 +00:00
|
|
|
local sounders = smem.rtu_dev.sounders
|
2023-06-25 16:59:38 +00:00
|
|
|
local nic = smem.rtu_sys.nic
|
2022-04-29 14:19:05 +00:00
|
|
|
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
|
|
|
|
2022-09-17 21:04:57 +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
|
2022-07-19 19:18:11 +00:00
|
|
|
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
|
2023-04-21 00:40:28 +00:00
|
|
|
-- blink heartbeat indicator
|
|
|
|
databus.heartbeat()
|
|
|
|
|
2023-07-29 21:57:51 +00:00
|
|
|
-- update speaker states
|
|
|
|
for _, sounder in pairs(sounders) do
|
|
|
|
-- re-compute output if needed, then play audio if available
|
|
|
|
if sounder.stream.is_recompute_needed() then
|
|
|
|
sounder.stream.compute_buffer()
|
2023-08-26 23:01:22 +00:00
|
|
|
if sounder.stream.any_active() then sounder.play() else sounder.stop() end
|
2023-07-29 21:57:51 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
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
|
|
|
|
2022-11-13 19:13:30 +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
|
2022-11-13 19:13:30 +00:00
|
|
|
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
|
2023-08-26 01:42:35 +00:00
|
|
|
-- haven't heard from server recently? close connection
|
|
|
|
rtu_comms.close(rtu_state)
|
2023-06-03 21:40:57 +00:00
|
|
|
elseif event == "timer" then
|
|
|
|
-- notify timer callback dispatcher if no other timer case claimed this event
|
|
|
|
tcd.handle(param1)
|
2022-04-29 14:19:05 +00:00
|
|
|
elseif event == "peripheral_detach" then
|
2022-04-27 16:46:04 +00:00
|
|
|
-- handle loss of a device
|
2022-05-10 21:08:38 +00:00
|
|
|
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
|
2023-06-25 16:59:38 +00:00
|
|
|
if nic.is_modem(device) then
|
|
|
|
nic.disconnect()
|
|
|
|
|
2022-05-10 21:08:38 +00:00
|
|
|
println_ts("wireless modem disconnected!")
|
2023-07-11 22:22:09 +00:00
|
|
|
log.warning("comms modem disconnected")
|
|
|
|
|
|
|
|
local other_modem = ppm.get_wireless_modem()
|
|
|
|
if other_modem then
|
|
|
|
log.info("found another wireless modem, using it for comms")
|
|
|
|
nic.connect(other_modem)
|
|
|
|
else
|
|
|
|
databus.tx_hw_modem(false)
|
|
|
|
end
|
2022-05-10 21:08:38 +00:00
|
|
|
else
|
|
|
|
log.warning("non-comms modem disconnected")
|
|
|
|
end
|
2023-07-27 00:48:44 +00:00
|
|
|
elseif type == "speaker" then
|
|
|
|
for i = 1, #sounders do
|
|
|
|
if sounders[i].speaker == device then
|
|
|
|
table.remove(sounders, i)
|
|
|
|
|
|
|
|
log.warning(util.c("speaker ", param1, " disconnected"))
|
|
|
|
println_ts("speaker disconnected")
|
|
|
|
|
|
|
|
databus.tx_hw_spkr_count(#sounders)
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2022-04-29 13:27:05 +00:00
|
|
|
else
|
2022-05-10 21:08:38 +00:00
|
|
|
for i = 1, #units do
|
|
|
|
-- find disconnected device
|
|
|
|
if units[i].device == device then
|
2023-06-22 19:46:17 +00:00
|
|
|
-- will let the PPM prevent crashes, which will indicate failures in MODBUS queries
|
2023-04-21 00:40:28 +00:00
|
|
|
local unit = units[i] ---@type rtu_unit_registry_entry
|
2023-02-21 17:27:16 +00:00
|
|
|
local type_name = types.rtu_type_to_string(unit.type)
|
2023-06-22 19:46:17 +00:00
|
|
|
|
2023-02-21 17:27:16 +00:00
|
|
|
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))
|
2023-06-22 19:46:17 +00:00
|
|
|
|
|
|
|
unit.hw_state = UNIT_HW_STATE.OFFLINE
|
|
|
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
2022-11-13 19:13:30 +00:00
|
|
|
break
|
2022-05-10 21:08:38 +00:00
|
|
|
end
|
2022-04-29 13:27:05 +00:00
|
|
|
end
|
2022-04-27 16:46:04 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
elseif event == "peripheral" then
|
2022-04-29 13:27:05 +00:00
|
|
|
-- peripheral connect
|
2022-04-27 16:46:04 +00:00
|
|
|
local type, device = ppm.mount(param1)
|
|
|
|
|
2022-05-10 21:08:38 +00:00
|
|
|
if type ~= nil and device ~= nil then
|
|
|
|
if type == "modem" then
|
2023-07-11 22:22:09 +00:00
|
|
|
if device.isWireless() and not nic.is_connected() then
|
2022-05-10 21:08:38 +00:00
|
|
|
-- reconnected modem
|
2023-06-25 16:59:38 +00:00
|
|
|
nic.connect(device)
|
2022-05-10 21:08:38 +00:00
|
|
|
|
|
|
|
println_ts("wireless modem reconnected.")
|
2023-02-22 04:50:43 +00:00
|
|
|
log.info("comms modem reconnected")
|
2023-04-21 00:40:28 +00:00
|
|
|
|
|
|
|
databus.tx_hw_modem(true)
|
2023-07-12 01:06:47 +00:00
|
|
|
elseif device.isWireless() then
|
|
|
|
log.info("unused wireless modem reconnected")
|
2022-05-10 21:08:38 +00:00
|
|
|
else
|
2023-02-22 04:50:43 +00:00
|
|
|
log.info("wired modem reconnected")
|
2022-05-10 21:08:38 +00:00
|
|
|
end
|
2023-07-27 00:48:44 +00:00
|
|
|
elseif type == "speaker" then
|
|
|
|
table.insert(sounders, rtu.init_sounder(device))
|
|
|
|
|
|
|
|
println_ts("speaker connected")
|
|
|
|
log.info(util.c("connected speaker ", param1))
|
|
|
|
|
|
|
|
databus.tx_hw_spkr_count(#sounders)
|
2022-04-29 13:27:05 +00:00
|
|
|
else
|
2022-05-10 21:08:38 +00:00
|
|
|
-- relink lost peripheral to correct unit entry
|
|
|
|
for i = 1, #units do
|
2023-11-06 14:25:44 +00:00
|
|
|
handle_unit_mount(smem, println_ts, param1, type, device, units[i])
|
2022-04-29 13:27:05 +00:00
|
|
|
end
|
2022-04-27 16:46:04 +00:00
|
|
|
end
|
|
|
|
end
|
2023-10-04 02:52:13 +00:00
|
|
|
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
2023-09-23 16:58:09 +00:00
|
|
|
event == "double_click" then
|
2023-05-10 19:21:54 +00:00
|
|
|
-- handle a mouse event
|
|
|
|
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
2023-07-27 00:48:44 +00:00
|
|
|
elseif event == "speaker_audio_empty" then
|
|
|
|
-- handle empty speaker audio buffer
|
|
|
|
for i = 1, #sounders do
|
|
|
|
local sounder = sounders[i] ---@type rtu_speaker_sounder
|
|
|
|
if sounder.name == param1 then
|
|
|
|
sounder.continue()
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
2022-04-27 16:46:04 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- check for termination request
|
|
|
|
if event == "terminate" or ppm.should_terminate() then
|
|
|
|
rtu_state.shutdown = true
|
2022-05-04 17:37:01 +00:00
|
|
|
log.info("terminate requested, main thread exiting")
|
2022-04-27 16:46:04 +00:00
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-05-21 17:56:14 +00:00
|
|
|
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
2022-05-31 18:54:55 +00:00
|
|
|
function public.p_exec()
|
2022-05-21 17:56:14 +00:00
|
|
|
local rtu_state = smem.rtu_state
|
|
|
|
|
|
|
|
while not rtu_state.shutdown do
|
|
|
|
local status, result = pcall(public.exec)
|
|
|
|
if status == false then
|
2022-09-21 19:53:51 +00:00
|
|
|
log.fatal(util.strval(result))
|
2022-05-21 17:56:14 +00:00
|
|
|
end
|
|
|
|
|
2023-04-21 00:40:28 +00:00
|
|
|
databus.tx_rt_status("main", false)
|
|
|
|
|
2022-05-21 17:56:14 +00:00
|
|
|
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
|
2022-05-31 18:54:55 +00:00
|
|
|
function threads.thread__comms(smem)
|
2023-02-21 17:27:16 +00:00
|
|
|
---@class parallel_thread
|
|
|
|
local public = {}
|
2022-05-21 17:56:14 +00:00
|
|
|
|
2022-04-27 16:46:04 +00:00
|
|
|
-- execute thread
|
2022-05-31 18:54:55 +00:00
|
|
|
function public.exec()
|
2023-04-21 00:40:28 +00:00
|
|
|
databus.tx_rt_status("comms", true)
|
2022-05-04 17:37:01 +00:00
|
|
|
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
|
2023-07-27 00:48:44 +00:00
|
|
|
local sounders = smem.rtu_dev.sounders
|
2023-02-22 04:50:43 +00:00
|
|
|
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
|
2023-07-19 02:28:43 +00:00
|
|
|
local handle_start = util.time()
|
|
|
|
|
|
|
|
-- check for messages in the message queue while not shut down
|
2022-04-29 13:27:05 +00:00
|
|
|
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
|
2023-07-27 00:48:44 +00:00
|
|
|
-- handle the packet (rtu_state passed to allow setting link flag, sounders passed to manage alarm audio)
|
|
|
|
rtu_comms.handle_packet(msg.message, units, rtu_state, sounders)
|
2022-05-11 16:03:15 +00:00
|
|
|
end
|
2022-04-27 16:46:04 +00:00
|
|
|
end
|
|
|
|
|
2023-07-19 02:28:43 +00:00
|
|
|
-- max 100ms spent processing queue
|
|
|
|
if util.time() - handle_start > 100 then
|
|
|
|
log.warning("comms thread exceeded 100ms queue process limit")
|
|
|
|
break
|
|
|
|
end
|
2022-04-27 16:46:04 +00:00
|
|
|
end
|
|
|
|
|
2023-07-19 02:28:43 +00:00
|
|
|
-- quick yield
|
|
|
|
util.nop()
|
|
|
|
|
2022-04-27 16:46:04 +00:00
|
|
|
-- check for termination request
|
|
|
|
if rtu_state.shutdown then
|
2022-05-04 17:37:01 +00:00
|
|
|
rtu_comms.close(rtu_state)
|
|
|
|
log.info("comms thread exiting")
|
2022-04-27 16:46:04 +00:00
|
|
|
break
|
|
|
|
end
|
|
|
|
|
2022-04-27 23:06:01 +00:00
|
|
|
-- 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
|
|
|
|
2022-05-21 17:56:14 +00:00
|
|
|
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
2022-05-31 18:54:55 +00:00
|
|
|
function public.p_exec()
|
2022-05-21 17:56:14 +00:00
|
|
|
local rtu_state = smem.rtu_state
|
|
|
|
|
|
|
|
while not rtu_state.shutdown do
|
|
|
|
local status, result = pcall(public.exec)
|
|
|
|
if status == false then
|
2022-09-21 19:53:51 +00:00
|
|
|
log.fatal(util.strval(result))
|
2022-05-21 17:56:14 +00:00
|
|
|
end
|
|
|
|
|
2023-04-21 00:40:28 +00:00
|
|
|
databus.tx_rt_status("comms", false)
|
|
|
|
|
2022-05-21 17:56:14 +00:00
|
|
|
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
|
2022-05-31 18:54:55 +00:00
|
|
|
function threads.thread__unit_comms(smem, unit)
|
2023-02-21 17:27:16 +00:00
|
|
|
---@class parallel_thread
|
|
|
|
local public = {}
|
2022-05-21 17:56:14 +00:00
|
|
|
|
2022-04-29 17:19:01 +00:00
|
|
|
-- execute thread
|
2022-05-31 18:54:55 +00:00
|
|
|
function public.exec()
|
2023-04-21 00:40:28 +00:00
|
|
|
databus.tx_rt_status("unit_" .. unit.uid, true)
|
|
|
|
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()
|
|
|
|
|
2022-11-11 19:59:53 +00:00
|
|
|
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, ")")
|
2022-11-11 19:59:53 +00:00
|
|
|
|
2022-09-21 19:53:51 +00:00
|
|
|
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
|
|
|
|
|
2023-07-19 02:28:43 +00:00
|
|
|
-- check for termination request
|
|
|
|
if rtu_state.shutdown then
|
|
|
|
log.info("rtu unit thread exiting -> " .. short_name)
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
2022-11-12 06:35:31 +00:00
|
|
|
-- check if multiblock is still formed if this is a multiblock
|
2023-02-12 18:06:44 +00:00
|
|
|
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()
|
|
|
|
|
2023-02-12 18:06:44 +00:00
|
|
|
last_f_check = util.time_ms()
|
|
|
|
|
2023-04-21 00:40:28 +00:00
|
|
|
if unit.formed == nil then
|
|
|
|
unit.formed = is_formed
|
2023-06-22 19:46:17 +00:00
|
|
|
if is_formed then unit.hw_state = UNIT_HW_STATE.OK end
|
2023-04-21 00:40:28 +00:00
|
|
|
end
|
|
|
|
|
2023-06-22 19:46:17 +00:00
|
|
|
if not unit.formed then unit.hw_state = UNIT_HW_STATE.UNFORMED end
|
2023-02-12 18:06:44 +00:00
|
|
|
|
2022-11-28 03:44:47 +00:00
|
|
|
if (not unit.formed) and is_formed then
|
2022-11-11 19:59:53 +00:00
|
|
|
-- 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)
|
2023-04-06 16:52:25 +00:00
|
|
|
local faulted = false
|
2022-11-11 19:59:53 +00:00
|
|
|
|
|
|
|
if device ~= nil then
|
2023-02-21 17:27:16 +00:00
|
|
|
if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
|
2022-11-11 19:59:53 +00:00
|
|
|
-- boiler multiblock
|
|
|
|
unit.device = device
|
2023-04-06 16:52:25 +00:00
|
|
|
unit.rtu, faulted = boilerv_rtu.new(device)
|
2022-11-11 19:59:53 +00:00
|
|
|
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
|
2022-11-11 19:59:53 +00:00
|
|
|
-- turbine multiblock
|
|
|
|
unit.device = device
|
2023-04-06 16:52:25 +00:00
|
|
|
unit.rtu, faulted = turbinev_rtu.new(device)
|
2022-11-11 19:59:53 +00:00
|
|
|
unit.formed = device.isFormed()
|
|
|
|
unit.modbus_io = modbus.new(unit.rtu, true)
|
2023-07-11 21:27:03 +00:00
|
|
|
elseif type == "dynamicValve" and unit.type == RTU_UNIT_TYPE.DYNAMIC_VALVE then
|
|
|
|
-- dynamic tank multiblock
|
|
|
|
unit.device = device
|
|
|
|
unit.rtu, faulted = dynamicv_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
|
2022-11-11 19:59:53 +00:00
|
|
|
-- induction matrix multiblock
|
|
|
|
unit.device = device
|
2023-04-06 16:52:25 +00:00
|
|
|
unit.rtu, faulted = imatrix_rtu.new(device)
|
2022-11-11 19:59:53 +00:00
|
|
|
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
|
2022-11-11 19:59:53 +00:00
|
|
|
-- SPS multiblock
|
|
|
|
unit.device = device
|
2023-04-06 16:52:25 +00:00
|
|
|
unit.rtu, faulted = sps_rtu.new(device)
|
2022-11-11 19:59:53 +00:00
|
|
|
unit.formed = device.isFormed()
|
|
|
|
unit.modbus_io = modbus.new(unit.rtu, true)
|
|
|
|
else
|
2023-04-21 00:40:28 +00:00
|
|
|
log.error("illegal remount of non-multiblock RTU or type change attempted for " .. short_name, true)
|
2022-11-11 19:59:53 +00:00
|
|
|
end
|
|
|
|
|
2023-04-06 16:52:25 +00:00
|
|
|
if unit.formed and faulted then
|
|
|
|
-- something is still wrong = can't mark as formed yet
|
|
|
|
unit.formed = false
|
2023-06-22 19:46:17 +00:00
|
|
|
unit.hw_state = UNIT_HW_STATE.UNFORMED
|
2023-04-21 00:40:28 +00:00
|
|
|
log.info(util.c("assuming ", unit.name, " is not formed due to PPM faults while initializing"))
|
2023-04-06 16:52:25 +00:00
|
|
|
else
|
2023-06-22 19:46:17 +00:00
|
|
|
unit.hw_state = UNIT_HW_STATE.OK
|
2023-04-06 16:52:25 +00:00
|
|
|
rtu_comms.send_remounted(unit.uid)
|
|
|
|
end
|
2023-04-21 00:40:28 +00:00
|
|
|
|
|
|
|
local type_name = types.rtu_type_to_string(unit.type)
|
|
|
|
log.info(util.c("reconnected the ", type_name, " on interface ", unit.name))
|
2022-11-11 19:59:53 +00:00
|
|
|
else
|
|
|
|
-- fully lost the peripheral now :(
|
|
|
|
log.error(util.c(unit.name, " lost (failed reconnect)"))
|
|
|
|
end
|
|
|
|
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
|
2022-11-11 19:59:53 +00:00
|
|
|
end
|
|
|
|
|
2023-06-22 19:46:17 +00:00
|
|
|
-- check hardware status
|
|
|
|
if unit.device.__p_is_healthy() then
|
|
|
|
if unit.hw_state == UNIT_HW_STATE.FAULTED then unit.hw_state = UNIT_HW_STATE.OK end
|
|
|
|
else
|
|
|
|
if unit.hw_state == UNIT_HW_STATE.OK then unit.hw_state = UNIT_HW_STATE.FAULTED end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- update hw status
|
|
|
|
databus.tx_unit_hw_status(unit.uid, unit.hw_state)
|
|
|
|
|
2022-04-29 17:19:01 +00:00
|
|
|
-- delay before next check
|
|
|
|
last_update = util.adaptive_delay(COMMS_SLEEP, last_update)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-05-21 17:56:14 +00:00
|
|
|
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
2022-05-31 18:54:55 +00:00
|
|
|
function public.p_exec()
|
2022-05-21 17:56:14 +00:00
|
|
|
local rtu_state = smem.rtu_state
|
|
|
|
|
|
|
|
while not rtu_state.shutdown do
|
|
|
|
local status, result = pcall(public.exec)
|
|
|
|
if status == false then
|
2022-09-21 19:53:51 +00:00
|
|
|
log.fatal(util.strval(result))
|
2022-05-21 17:56:14 +00:00
|
|
|
end
|
|
|
|
|
2023-04-21 00:40:28 +00:00
|
|
|
databus.tx_rt_status("unit_" .. unit.uid, false)
|
|
|
|
|
2022-05-21 17:56:14 +00:00
|
|
|
if not rtu_state.shutdown then
|
2023-04-21 00:40:28 +00:00
|
|
|
log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), " (", unit.name, ") restarting in 5 seconds..."))
|
2022-05-21 17:56:14 +00:00
|
|
|
util.psleep(5)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return public
|
2022-04-29 17:19:01 +00:00
|
|
|
end
|
2022-05-04 17:37:01 +00:00
|
|
|
|
|
|
|
return threads
|