cc-mek-scada/reactor-plc/plc.lua

888 lines
32 KiB
Lua

local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
local util = require("scada-common.util")
local plc = {}
local rps_status_t = types.rps_status_t
local PROTOCOLS = comms.PROTOCOLS
local DEVICE_TYPES = comms.DEVICE_TYPES
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local RPLC_TYPES = comms.RPLC_TYPES
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
-- I sure hope the devs don't change this error message, not that it would have safety implications
-- I wish they didn't change it to be like this
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.02
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
---@param reactor table
---@param is_formed boolean
function plc.rps_init(reactor, is_formed)
local state_keys = {
dmg_crit = 1,
high_temp = 2,
no_coolant = 3,
ex_waste = 4,
ex_hcoolant = 5,
no_fuel = 6,
fault = 7,
timeout = 8,
manual = 9,
automatic = 10,
sys_fail = 11,
force_disabled = 12
}
local self = {
reactor = reactor,
state = { false, false, false, false, false, false, false, false, false, false, false, false },
reactor_enabled = false,
formed = is_formed,
force_disabled = false,
tripped = false,
trip_cause = "" ---@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
self.state[state_keys.fault] = true
end
end
-- clear reactor access fault flag
local function _clear_fault()
self.state[state_keys.fault] = false
end
-- check if the reactor is formed
local function _is_formed()
local formed = self.reactor.isFormed()
if formed == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
else
self.formed = formed
if not self.state[state_keys.sys_fail] then
self.state[state_keys.sys_fail] = not formed
end
end
end
-- check if the reactor is force disabled
local function _is_force_disabled()
local disabled = self.reactor.isForceDisabled()
if disabled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
else
self.force_disabled = disabled
if not self.state[state_keys.force_disabled] then
self.state[state_keys.force_disabled] = disabled
end
end
end
-- check for critical damage
local function _damage_critical()
local damage_percent = self.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
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()
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
end
end
-- check if there is no coolant (<2% filled)
local function _no_coolant()
local coolant_filled = self.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
end
end
-- check for excess waste (>80% filled)
local function _excess_waste()
local w_filled = self.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
end
end
-- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant()
local hc_filled = self.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
end
end
-- check if there is no fuel
local function _insufficient_fuel()
local fuel = self.reactor.getFuel()
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
end
end
-- PUBLIC FUNCTIONS --
-- re-link a reactor after a peripheral re-connect
---@diagnostic disable-next-line: redefined-local
function public.reconnect_reactor(reactor)
self.reactor = reactor
end
-- trip for lost peripheral
function public.trip_fault()
_set_fault()
end
-- trip for a PLC comms timeout
function public.trip_timeout()
self.state[state_keys.timeout] = true
end
-- manually SCRAM the reactor
function public.trip_manual()
self.state[state_keys.manual] = true
end
-- automatic SCRAM commanded by supervisor/coordinator
function public.trip_auto()
self.state[state_keys.automatic] = true
end
-- trip for unformed reactor
function public.trip_sys_fail()
self.state[state_keys.fault] = true
self.state[state_keys.sys_fail] = true
end
-- SCRAM the reactor now
---@return boolean success
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
log.error("RPS: failed reactor SCRAM")
return false
else
self.reactor_enabled = false
return true
end
end
-- start the reactor
---@return boolean success
function public.activate()
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
log.error("RPS: failed reactor start")
else
self.reactor_enabled = true
return true
end
else
log.debug(util.c("RPS: failed start, RPS tripped: ", self.trip_cause))
end
return false
end
-- check all safety conditions
---@return boolean tripped, rps_status_t trip_status, boolean first_trip
function public.check()
local status = rps_status_t.ok
local was_tripped = self.tripped
local first_trip = false
if self.formed then
-- update state
parallel.waitForAll(
_is_formed,
_is_force_disabled,
_damage_critical,
_high_temp,
_no_coolant,
_excess_waste,
_excess_heated_coolant,
_insufficient_fuel
)
else
-- check to see if its now formed
_is_formed()
end
-- check system states in order of severity
if self.tripped then
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
elseif self.state[state_keys.force_disabled] then
log.warning("RPS: reactor was force disabled")
status = rps_status_t.force_disabled
elseif self.state[state_keys.dmg_crit] then
log.warning("RPS: damage critical")
status = rps_status_t.dmg_crit
elseif self.state[state_keys.high_temp] then
log.warning("RPS: high temperature")
status = rps_status_t.high_temp
elseif self.state[state_keys.no_coolant] then
log.warning("RPS: no coolant")
status = rps_status_t.no_coolant
elseif self.state[state_keys.ex_waste] then
log.warning("RPS: full waste")
status = rps_status_t.ex_waste
elseif self.state[state_keys.ex_hcoolant] then
log.warning("RPS: heated coolant backup")
status = rps_status_t.ex_hcoolant
elseif self.state[state_keys.no_fuel] then
log.warning("RPS: no fuel")
status = rps_status_t.no_fuel
elseif self.state[state_keys.fault] then
log.warning("RPS: reactor access fault")
status = rps_status_t.fault
elseif self.state[state_keys.timeout] then
log.warning("RPS: supervisor connection timeout")
status = rps_status_t.timeout
elseif self.state[state_keys.manual] then
log.warning("RPS: manual SCRAM requested")
status = rps_status_t.manual
elseif self.state[state_keys.automatic] then
log.warning("RPS: automatic SCRAM requested")
status = rps_status_t.automatic
else
self.tripped = false
end
-- if a new trip occured...
if (not was_tripped) and (status ~= rps_status_t.ok) then
first_trip = true
self.tripped = true
self.trip_cause = status
-- in the case that the reactor is detected to be active, it will be scrammed shortly after this in the main RPS loop if we don't here
if self.formed then
if not self.force_disabled then
public.scram()
else
log.warning("RPS: skipping SCRAM due to reactor being force disabled")
end
else
log.warning("RPS: skipping SCRAM due to not being formed")
end
end
return self.tripped, status, first_trip
end
function public.status() return self.state end
function public.is_tripped() return self.tripped end
function public.is_active() return self.reactor_enabled end
function public.is_formed() return self.formed end
function public.is_force_disabled() return self.force_disabled end
-- reset the RPS
---@param quiet? boolean true to suppress the info log message
function public.reset(quiet)
self.tripped = false
self.trip_cause = rps_status_t.ok
for i = 1, #self.state do
self.state[i] = false
end
if not quiet then log.info("RPS: reset") end
end
return public
end
-- Reactor PLC Communications
---@param id integer
---@param version string
---@param modem table
---@param local_port integer
---@param server_port integer
---@param reactor table
---@param rps rps
---@param conn_watchdog watchdog
function plc.comms(id, version, modem, local_port, server_port, reactor, rps, conn_watchdog)
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,
status_cache = nil,
resend_build = false,
max_burn_rate = nil
}
-- configure modem channels
local function _conf_channels()
self.modem.closeAll()
self.modem.open(self.l_port)
end
_conf_channels()
---@class plc_comms
local public = {}
-- PRIVATE FUNCTIONS --
-- send an RPLC packet
---@param msg_type RPLC_TYPES
---@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())
self.modem.transmit(self.s_port, self.l_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 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())
self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
self.seq_num = self.seq_num + 1
end
-- variable reactor status information, excluding heating rate
---@return table data_table, boolean faulted
local function _reactor_status()
local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {
false, -- getStatus
0, -- getBurnRate
0, -- getActualBurnRate
0, -- getTemperature
0, -- getDamagePercent
0, -- getBoilEfficiency
0, -- getEnvironmentalLoss
0, -- fuel_amnt
0, -- getFuelFilledPercentage
0, -- waste_amnt
0, -- getWasteFilledPercentage
"", -- coolant_name
0, -- coolant_amnt
0, -- getCoolantFilledPercentage
"", -- hcoolant_name
0, -- hcoolant_amnt
0 -- getHeatedCoolantFilledPercentage
}
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
}
parallel.waitForAll(table.unpack(tasks))
if fuel ~= nil then
data_table[8] = fuel.amount
end
if waste ~= nil then
data_table[10] = waste.amount
end
if coolant ~= nil then
data_table[12] = coolant.name
data_table[13] = coolant.amount
end
if hcoolant ~= nil then
data_table[15] = hcoolant.name
data_table[16] = hcoolant.amount
end
return data_table, self.reactor.__p_is_faulted()
end
-- update the status cache if changed
---@return boolean changed
local function _update_status_cache()
local status, faulted = _reactor_status()
local changed = false
if self.status_cache ~= nil then
if not faulted then
for i = 1, #status do
if status[i] ~= self.status_cache[i] then
changed = true
break
end
end
end
else
changed = true
end
if changed and not faulted then
self.status_cache = status
end
return changed
end
-- 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() })
end
-- general ack
---@param msg_type RPLC_TYPES
---@param succeeded boolean
local function _send_ack(msg_type, succeeded)
_send(msg_type, { succeeded })
end
-- send structure properties (these should not change, server will cache these)
local function _send_struct()
local min_pos = { x = 0, y = 0, z = 0 }
local max_pos = { x = 0, y = 0, z = 0 }
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
}
parallel.waitForAll(table.unpack(tasks))
if not self.reactor.__p_is_faulted() then
_send(RPLC_TYPES.MEK_STRUCT, mek_data)
self.resend_build = false
else
log.error("failed to send structure: PPM fault")
end
end
-- PUBLIC FUNCTIONS --
-- reconnect a newly connected modem
---@param modem table
---@diagnostic disable-next-line: redefined-local
function public.reconnect_modem(modem)
self.modem = 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
self.status_cache = nil
self.resend_build = true
end
-- unlink from the server
function public.unlink()
self.linked = false
self.r_seq_num = nil
self.status_cache = nil
end
-- close the connection to the server
function public.close()
conn_watchdog.cancel()
public.unlink()
_send_mgmt(SCADA_MGMT_TYPES.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 })
end
-- send live status information
---@param no_reactor boolean PLC lost reactor connection
---@param formed boolean reactor formed (from PLC state)
function public.send_status(no_reactor, formed)
if self.linked then
local mek_data = nil ---@type table
local heating_rate = 0.0 ---@type number
if (not no_reactor) and rps.is_formed() then
if _update_status_cache() then
mek_data = self.status_cache
end
heating_rate = self.reactor.getHeatingRate()
end
local sys_status = {
util.time(), -- timestamp
(not self.scrammed), -- requested control state
rps.is_tripped(), -- rps_tripped
no_reactor, -- no reactor peripheral connected
formed, -- reactor formed
heating_rate, -- heating rate
mek_data -- mekanism status data
}
_send(RPLC_TYPES.STATUS, sys_status)
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.status())
end
end
-- send reactor protection system alarm
---@param cause rps_status_t 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)
end
end
-- parse an RPLC packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
---@return rplc_frame|mgmt_frame|nil packet
function public.parse_packet(side, sender, reply_to, message, distance)
local pkt = nil
local s_pkt = comms.scada_packet()
-- parse packet as generic SCADA packet
s_pkt.receive(side, sender, reply_to, message, distance)
if s_pkt.is_valid() then
-- get as RPLC packet
if s_pkt.protocol() == PROTOCOLS.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
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
else
log.error("illegal packet type " .. s_pkt.protocol(), true)
end
end
return pkt
end
-- handle an RPLC packet
---@param packet rplc_frame|mgmt_frame packet frame
---@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
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
elseif self.linked and self.r_seq_num >= packet.scada_frame.seq_num() then
log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
return
else
self.r_seq_num = packet.scada_frame.seq_num()
end
-- feed the watchdog first so it doesn't uhh...eat our packets :)
conn_watchdog.feed()
local protocol = packet.scada_frame.protocol()
-- handle packet
if protocol == PROTOCOLS.RPLC then
if self.linked then
if packet.type == RPLC_TYPES.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
-- 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
-- set the burn rate
if packet.length == 2 then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = self.reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
self.reactor.setBurnRate(burn_rate)
success = not self.reactor.__p_is_faulted()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch")
end
elseif packet.type == RPLC_TYPES.RPS_ENABLE then
-- enable the reactor
self.scrammed = false
_send_ack(packet.type, rps.activate())
elseif packet.type == RPLC_TYPES.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
-- 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
-- reset the RPS status
rps.reset()
_send_ack(packet.type, true)
else
log.warning("received unknown RPLC packet type " .. packet.type)
end
else
log.debug("discarding RPLC packet before linked")
end
elseif protocol == PROTOCOLS.SCADA_MGMT then
if self.linked then
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
-- link request confirmation
if packet.length == 1 then
log.debug("received unsolicited establish response")
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then
self.status_cache = nil
_send_struct()
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("re-sent initial status data")
elseif est_ack == ESTABLISH_ACK.DENY then
println_ts("received unsolicited link denial, unlinking")
log.info("unsolicited establish request denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("received unsolicited link collision, unlinking")
log.warning("unsolicited establish request collision")
else
println_ts("invalid unsolicited link response")
log.error("unsolicited unknown establish request response")
end
self.linked = est_ack == ESTABLISH_ACK.ALLOW
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
elseif packet.type == SCADA_MGMT_TYPES.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]
local trip_time = util.time() - timestamp
if trip_time > 500 then
log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)")
end
-- log.debug("RPLC RTT = " .. trip_time .. "ms")
_send_keep_alive_ack(timestamp)
else
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
-- handle session close
conn_watchdog.cancel()
public.unlink()
println_ts("server connection closed by remote host")
log.warning("server connection closed by remote host")
else
log.warning("received unsupported SCADA_MGMT packet type " .. packet.type)
end
elseif packet.type == SCADA_MGMT_TYPES.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")
-- reset remote sequence number and cache
self.r_seq_num = nil
self.status_cache = nil
if plc_state.reactor_formed then _send_struct() end
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("sent initial status data")
elseif est_ack == ESTABLISH_ACK.DENY then
println_ts("link request denied, retrying...")
log.debug("establish request denied")
elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("reactor PLC ID collision (check config), retrying...")
log.warning("establish request collision")
else
println_ts("invalid link response, bad channel? retrying...")
log.error("unknown establish request response")
end
self.linked = est_ack == ESTABLISH_ACK.ALLOW
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
else
log.debug("discarding non-link SCADA_MGMT packet before linked")
end
else
-- should be unreachable assuming packet is from parse_packet()
log.error("illegal packet type " .. protocol, true)
end
end
end
function public.is_scrammed() return self.scrammed end
function public.is_linked() return self.linked end
return public
end
return plc