diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index eed5cdc..bb86b05 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -3,6 +3,9 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") +-- supervisor rtu sessions (svrs) +local svrs_boiler = require("supervisor.session.rtu.boiler") + local rtu = {} local PROTOCOLS = comms.PROTOCOLS diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua new file mode 100644 index 0000000..83ecd8b --- /dev/null +++ b/supervisor/session/rtu/boiler.lua @@ -0,0 +1,210 @@ +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 txnctrl = require("supervisor.session.rtu.txnctrl") + +local boiler = {} + +local PROTOCOLS = comms.PROTOCOLS +local MODBUS_FCODE = types.MODBUS_FCODE + +local rtu_t = types.rtu_t + +local TXN_TYPES = { + BUILD = 0, + STATE = 1, + TANKS = 2 +} + +local PERIODICS = { + BUILD = 1000, + STATE = 500, + TANKS = 1000 +} + +-- create a new boiler rtu session runner +---@param advert rtu_advertisement +---@param out_queue mqueue +boiler.new = function (advert, out_queue) + -- type check + if advert.type ~= rtu_t.boiler then + log.error("attempt to instantiate boiler RTU for non boiler type '" .. advert.type .. "'. this is a bug.") + return nil + end + + local log_tag = "session.rtu.boiler(" .. advert.index .. "): " + + local self = { + uid = advert.index, + reactor = advert.reactor, + out_q = out_queue, + transaction_controller = txnctrl.new(), + has_build = false, + periodics = { + next_build_req = 0, + next_state_req = 0, + next_tanks_req = 0, + }, + ---@class boiler_session_db + db = { + build = { + boil_cap = 0.0, + steam_cap = 0, + water_cap = 0, + hcoolant_cap = 0, + ccoolant_cap = 0, + superheaters = 0, + max_boil_rate = 0.0 + }, + state = { + temperature = 0.0, + boil_rate = 0.0 + }, + tanks = { + steam = 0, + steam_need = 0, + steam_fill = 0.0, + water = 0, + water_need = 0, + water_fill = 0.0, + hcool = 0, + hcool_need = 0, + hcool_fill = 0.0, + ccool = 0, + ccool_need = 0, + ccool_fill = 0.0 + } + } + } + + ---@class rtu_session__boiler + local public = {} + + -- PRIVATE FUNCTIONS -- + + -- query the build of the device + local _request_build = function () + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(TXN_TYPES.BUILD) + + -- read input registers 1 through 7 (start = 1, count = 7) + m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) + + self.out_q.push_packet(m_pkt) + end + + -- query the state of the device + local _request_state = function () + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(TXN_TYPES.STATE) + + -- read input registers 8 through 9 (start = 8, count = 2) + m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) + + self.out_q.push_packet(m_pkt) + end + + -- query the tanks of the device + local _request_tanks = function () + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(TXN_TYPES.TANKS) + + -- read input registers 10 through 21 (start = 10, count = 12) + m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) + + self.out_q.push_packet(m_pkt) + end + + -- PUBLIC FUNCTIONS -- + + -- handle a packet + ---@param m_pkt modbus_frame + public.handle_packet = function (m_pkt) + local success = false + + if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if m_pkt.unit_id == self.uid then + local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) + if txn_type == TXN_TYPES.BUILD then + -- build response + if m_pkt.length == 7 then + self.db.build.boil_cap = m_pkt.data[1] + self.db.build.steam_cap = m_pkt.data[2] + self.db.build.water_cap = m_pkt.data[3] + self.db.build.hcoolant_cap = m_pkt.data[4] + self.db.build.ccoolant_cap = m_pkt.data[5] + self.db.build.superheaters = m_pkt.data[6] + self.db.build.max_boil_rate = m_pkt.data[7] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.build)") + end + elseif txn_type == TXN_TYPES.STATE then + -- state response + if m_pkt.length == 2 then + self.db.state.temperature = m_pkt.data[1] + self.db.state.boil_rate = m_pkt.data[2] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.state)") + end + elseif txn_type == TXN_TYPES.TANKS then + -- tanks response + if m_pkt.length == 12 then + self.db.tanks.steam = m_pkt.data[1] + self.db.tanks.steam_need = m_pkt.data[2] + self.db.tanks.steam_fill = m_pkt.data[3] + self.db.tanks.water = m_pkt.data[4] + self.db.tanks.water_need = m_pkt.data[5] + self.db.tanks.water_fill = m_pkt.data[6] + self.db.tanks.hcool = m_pkt.data[7] + self.db.tanks.hcool_need = m_pkt.data[8] + self.db.tanks.hcool_fill = m_pkt.data[9] + self.db.tanks.ccool = m_pkt.data[10] + self.db.tanks.ccool_need = m_pkt.data[11] + self.db.tanks.ccool_fill = m_pkt.data[12] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.tanks)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") + else + log.error(log_tag .. "unknown transaction type " .. txn_type) + end + else + log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + end + else + log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + end + + return success + end + + public.get_uid = function () return self.uid end + public.get_reactor = function () return self.reactor end + public.get_db = function () return self.db end + + -- update this runner + ---@param time_now integer milliseconds + public.update = function (time_now) + if not self.has_build and self.next_build_req <= time_now then + _request_build() + self.next_build_req = time_now + PERIODICS.BUILD + end + + if self.next_state_req <= time_now then + _request_state() + self.next_state_req = time_now + PERIODICS.STATE + end + + if self.next_tanks_req <= time_now then + _request_tanks() + self.next_tanks_req = time_now + PERIODICS.TANKS + end + end + + return public +end + +return boiler diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua new file mode 100644 index 0000000..3d74484 --- /dev/null +++ b/supervisor/session/rtu/txnctrl.lua @@ -0,0 +1,95 @@ +-- +-- MODBUS Transaction Controller +-- + +local util = require("scada-common.util") + +local txnctrl = {} + +local TIMEOUT = 3000 -- 3000ms max wait + +-- create a new transaction controller +txnctrl.new = function () + local self = { + list = {}, + next_id = 0 + } + + ---@class transaction_controller + local public = {} + + local insert = table.insert + + -- get the length of the transaction list + public.length = function () + return #self.list + end + + -- check if there are no active transactions + public.empty = function () + return #self.list == 0 + end + + -- create a new transaction of the given type + ---@param txn_type integer + ---@return integer txn_id + public.create = function (txn_type) + local txn_id = self.next_id + + insert(self.list, { + txn_id = txn_id, + txn_type = txn_type, + expiry = util.time() + TIMEOUT + }) + + self.next_id = self.next_id + 1 + + return txn_id + end + + -- mark a transaction as resolved to get its transaction type + ---@param txn_id integer + ---@return integer txn_type + public.resolve = function (txn_id) + local txn_type = nil + + for i = 1, public.length() do + if self.list[i].txn_id == txn_id then + txn_type = self.list[i].txn_type + self.list[i] = nil + end + end + + return txn_type + end + + -- close timed-out transactions + public.cleanup = function () + local now = util.time() + + local move_to = 1 + for i = 1, public.length() do + local txn = self.list[i] + if txn ~= nil then + if txn.expiry <= now then + self.list[i] = nil + else + if self.list[move_to] == nil then + self.list[move_to] = txn + self.list[i] = nil + end + move_to = move_to + 1 + end + end + end + end + + -- clear the transaction list + public.clear = function () + self.list = {} + end + + return public +end + +return txnctrl