diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index a8d6e5e..9eb5450 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -470,10 +470,11 @@ function coordinator.comms(version, nic, sv_watchdog) elseif packet.type == MGMT_TYPE.ESTABLISH then -- establish a new session -- validate packet and continue - if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] - local firmware_v = packet.data[2] + if packet.length == 4 then + local comms_v = util.strval(packet.data[1]) + local firmware_v = util.strval(packet.data[2]) local dev_type = packet.data[3] + local api_v = util.strval(packet.data[4]) if comms_v ~= comms.version then if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then @@ -481,6 +482,12 @@ function coordinator.comms(version, nic, sv_watchdog) end _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif api_v ~= comms.api_version then + if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_API_VERSION then + log.info(util.c("dropping API establish packet with incorrect api version v", comms_v, " (expected v", comms.version, ")")) + end + + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_API_VERSION) elseif dev_type == DEVICE_TYPE.PKT then -- pocket linking request local id = apisessions.establish_session(src_addr, firmware_v) diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 18fccc7..83ebdce 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -126,19 +126,15 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) if pkt.type == CRDN_TYPE.API_GET_FAC then local fac = db.facility - ---@class api_fac local data = { - num_units = fac.num_units, - num_tanks = util.table_len(fac.tank_data_tbl), - tank_mode = fac.tank_mode, - tank_defs = fac.tank_defs, - sys_ok = fac.all_sys_ok, - rtu_count = fac.rtu_count, - radiation = fac.radiation, - auto = { fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated }, - waste = { fac.auto_current_waste_product, fac.auto_pu_fallback_active }, - has_matrix = fac.induction_data_tbl[1] ~= nil, - has_sps = fac.sps_data_tbl[1] ~= nil, + fac.all_sys_ok, + fac.rtu_count, + fac.radiation, + { fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated }, + { fac.auto_current_waste_product, fac.auto_pu_fallback_active }, + util.table_len(fac.tank_data_tbl), + fac.induction_data_tbl[1] ~= nil, + fac.sps_data_tbl[1] ~= nil, } _send(CRDN_TYPE.API_GET_FAC, data) diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index 38d1d20..7efc7a5 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -161,7 +161,60 @@ function iocontrol.init_core(comms) end -- initialize facility-dependent components of pocket iocontrol -function iocontrol.init_fac() end +---@param conf facility_conf configuration +---@param comms pocket_comms comms reference +---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R) +function iocontrol.init_fac(conf, comms, temp_scale) + -- temperature unit label and conversion function (from Kelvin) + if temp_scale == 2 then + io.temp_label = "\xb0C" + io.temp_convert = function (t) return t - 273.15 end + elseif temp_scale == 3 then + io.temp_label = "\xb0F" + io.temp_convert = function (t) return (1.8 * (t - 273.15)) + 32 end + elseif temp_scale == 4 then + io.temp_label = "\xb0R" + io.temp_convert = function (t) return 1.8 * t end + else + io.temp_label = "K" + io.temp_convert = function (t) return t end + end + + -- facility data structure + ---@class pioctl_facility + io.facility = { + num_units = conf.num_units, + tank_mode = conf.cooling.fac_tank_mode, + tank_defs = conf.cooling.fac_tank_defs, + all_sys_ok = false, + rtu_count = 0, + + auto_ready = false, + auto_active = false, + auto_ramping = false, + auto_saturated = false, + + ---@type WASTE_PRODUCT + auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM, + auto_pu_fallback_active = false, + + radiation = types.new_zero_radiation_reading(), + + ps = psil.create(), + + induction_ps_tbl = {}, + induction_data_tbl = {}, + + sps_ps_tbl = {}, + sps_data_tbl = {}, + + tank_ps_tbl = {}, + tank_data_tbl = {}, + + env_d_ps = psil.create(), + env_d_data = {} + } +end -- set network link state ---@param state POCKET_LINK_STATE @@ -203,6 +256,39 @@ function iocontrol.report_crd_tt(trip_time) io.ps.publish("crd_conn_quality", state) end +-- populate facility data from API_GET_FAC +---@param data table +---@return boolean valid +function iocontrol.record_facility_data(data) + local valid = true + + local fac = io.facility + + fac.all_sys_ok = data[1] + fac.rtu_count = data[2] + fac.radiation = data[3] + + -- auto control + if type(data[4]) == "table" and #data[4] == 4 then + fac.auto_ready = data[4][1] + fac.auto_active = data[4][2] + fac.auto_ramping = data[4][3] + fac.auto_saturated = data[4][4] + end + + -- waste + if type(data[5]) == "table" and #data[5] == 2 then + fac.auto_current_waste_product = data[5][1] + fac.auto_pu_fallback_active = data[5][2] + end + + fac.num_tanks = data[6] + fac.has_imatrix = data[7] + fac.has_sps = data[8] + + return valid +end + -- get the IO controller database function iocontrol.get_db() return io end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 6f5b73c..402bd19 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -8,6 +8,7 @@ local PROTOCOL = comms.PROTOCOL local DEVICE_TYPE = comms.DEVICE_TYPE local ESTABLISH_ACK = comms.ESTABLISH_ACK local MGMT_TYPE = comms.MGMT_TYPE +local CRDN_TYPE = comms.CRDN_TYPE local LINK_STATE = iocontrol.LINK_STATE @@ -246,6 +247,25 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) return pkt end + ---@param packet mgmt_frame|crdn_frame + ---@param length integer + ---@param max integer? + ---@return boolean + local function _check_length(packet, length, max) + local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= max) + if not ok then + local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d" + log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type)) + end + return ok + end + + ---@param packet mgmt_frame|crdn_frame + local function _fail_type(packet) + local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: unrecognized packet type" + log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type)) + end + -- handle a packet ---@param packet mgmt_frame|crdn_frame|nil function public.handle_packet(packet) @@ -268,7 +288,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) return elseif self.api.linked and (src_addr ~= self.api.addr) then log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. - "); channel in use by another system?") + "); channel in use by another system?") return else self.api.r_seq_num = packet.scada_frame.seq_num() @@ -277,12 +297,24 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) -- feed watchdog on valid sequence number api_watchdog.feed() - if protocol == PROTOCOL.SCADA_MGMT then + if protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame + if self.api.linked then + if packet.type == CRDN_TYPE.API_GET_FAC then + if _check_length(packet, 11) then + iocontrol.record_facility_data(packet.data) + end + elseif packet.type == CRDN_TYPE.API_GET_UNITS then + else _fail_type(packet) end + else + log.debug("discarding coordinator SCADA_CRDN packet before linked") + end + elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame if self.api.linked then if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back - if packet.length == 1 then + if _check_length(packet, 1) then local timestamp = packet.data[1] local trip_time = util.time() - timestamp @@ -295,8 +327,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) _send_api_keep_alive_ack(timestamp) iocontrol.report_crd_tt(trip_time) - else - log.debug("coordinator SCADA keep alive packet length mismatch") end elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close @@ -305,24 +335,38 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.api.r_seq_num = nil self.api.addr = comms.BROADCAST log.info("coordinator server connection closed by remote host") - else - log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") - end + else _fail_type(packet) end elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with coordinator established - if packet.length == 1 then + if _check_length(packet, 1, 2) then local est_ack = packet.data[1] if est_ack == ESTABLISH_ACK.ALLOW then - log.info("coordinator connection established") - self.establish_delay_counter = 0 - self.api.linked = true - self.api.addr = src_addr + if packet.length == 2 then + local fac_config = packet.data[2] - if self.sv.linked then - iocontrol.report_link_state(LINK_STATE.LINKED) + if type(fac_config) == "table" and #fac_config == 2 then + -- get configuration + local conf = { num_units = fac_config[1], cooling = fac_config[2] } + + ---@todo + iocontrol.init_fac(conf, public, 0) + + log.info("coordinator connection established") + self.establish_delay_counter = 0 + self.api.linked = true + self.api.addr = src_addr + + if self.sv.linked then + iocontrol.report_link_state(LINK_STATE.LINKED) + else + iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) + end + else + log.debug("invalid facility configuration table received from coordinator, establish failed") + end else - iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY) + log.debug("received coordinator establish allow without facility configuration") end elseif est_ack == ESTABLISH_ACK.DENY then if self.api.last_est_ack ~= est_ack then @@ -336,13 +380,15 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if self.api.last_est_ack ~= est_ack then log.info("coordinator comms version mismatch") end + elseif est_ack == ESTABLISH_ACK.BAD_API_VERSION then + if self.api.last_est_ack ~= est_ack then + log.info("coordinator api version mismatch") + end else log.debug("coordinator SCADA_MGMT establish packet reply unsupported") end self.api.last_est_ack = est_ack - else - log.debug("coordinator SCADA_MGMT establish packet length mismatch") end else log.debug("discarding coordinator non-link SCADA_MGMT packet before linked") @@ -374,7 +420,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if self.sv.linked then if packet.type == MGMT_TYPE.KEEP_ALIVE then -- keep alive request received, echo back - if packet.length == 1 then + if _check_length(packet, 1) then local timestamp = packet.data[1] local trip_time = util.time() - timestamp @@ -387,8 +433,6 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) _send_sv_keep_alive_ack(timestamp) iocontrol.report_svr_tt(trip_time) - else - log.debug("supervisor SCADA keep alive packet length mismatch") end elseif packet.type == MGMT_TYPE.CLOSE then -- handle session close @@ -398,12 +442,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.sv.addr = comms.BROADCAST log.info("supervisor server connection closed by remote host") elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then - if packet.length == 8 then + if _check_length(packet, 8) then for i = 1, #packet.data do diag.tone_test.tone_indicators[i].update(packet.data[i] == true) end - else - log.debug("supervisor SCADA diag alarm states packet length mismatch") end elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then if packet.length == 1 and packet.data[1] == false then @@ -442,12 +484,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) else log.debug("supervisor SCADA diag alarm set packet length/type mismatch") end - else - log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") - end + else _fail_type(packet) end elseif packet.type == MGMT_TYPE.ESTABLISH then -- connection with supervisor established - if packet.length == 1 then + if _check_length(packet, 1) then local est_ack = packet.data[1] if est_ack == ESTABLISH_ACK.ALLOW then @@ -478,15 +518,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) end self.sv.last_est_ack = est_ack - else - log.debug("supervisor SCADA_MGMT establish packet length mismatch") end else log.debug("discarding supervisor non-link SCADA_MGMT packet before linked") end - else - log.debug("illegal packet type " .. protocol .. " from supervisor", true) - end + else _fail_type(packet) end else log.debug("received packet from unconfigured channel " .. r_chan, true) end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 86f98ed..9e2148b 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -16,8 +16,9 @@ local max_distance = nil ---@class comms local comms = {} --- protocol/data version (protocol/data independent changes tracked by util.lua version) +-- protocol/data versions (protocol/data independent changes tracked by util.lua version) comms.version = "2.4.5" +comms.api_version = "0.0.1" ---@enum PROTOCOL local PROTOCOL = { @@ -74,7 +75,8 @@ local ESTABLISH_ACK = { ALLOW = 0, -- link approved DENY = 1, -- link denied COLLISION = 2, -- link denied due to existing active link - BAD_VERSION = 3 -- link denied due to comms version mismatch + BAD_VERSION = 3, -- link denied due to comms version mismatch + BAD_API_VERSION = 4 -- link denied due to api version mismatch } ---@enum DEVICE_TYPE device types for establish messages