Compare commits
72 Commits
v1.8.12-be
...
v1.8.14-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fe6792804 | ||
|
|
25dc47d520 | ||
|
|
f958b0e3b7 | ||
|
|
f621ff2482 | ||
|
|
eb45ff899b | ||
|
|
e91fd2fcaa | ||
|
|
d35b824458 | ||
|
|
165d1497f8 | ||
|
|
50bf057ca6 | ||
|
|
6f768ef6b3 | ||
|
|
826086951e | ||
|
|
35bf56663f | ||
|
|
7b8cea4a5c | ||
|
|
51d4a22532 | ||
|
|
fb85c2f05b | ||
|
|
712d018806 | ||
|
|
00a8d64a88 | ||
|
|
d9efd5b8d2 | ||
|
|
a786404092 | ||
|
|
1bd03c0b1a | ||
|
|
0d8d7aeb15 | ||
|
|
0b60dc9fa4 | ||
|
|
48b91ac808 | ||
|
|
4c9efc78b1 | ||
|
|
3c5857cd68 | ||
|
|
473b0dd10d | ||
|
|
d2df7db18b | ||
|
|
0ca002316f | ||
|
|
9fe34648c2 | ||
|
|
57dc20692b | ||
|
|
f22d699baf | ||
|
|
23b31e0049 | ||
|
|
2b4309afa7 | ||
|
|
89cd5a07f2 | ||
|
|
99213da760 | ||
|
|
f23f69d441 | ||
|
|
a99b7912f8 | ||
|
|
23ca5fb69e | ||
|
|
c243d064ef | ||
|
|
878c3b92e1 | ||
|
|
2aa52d2e2c | ||
|
|
dfc1ee6497 | ||
|
|
a6a1a61954 | ||
|
|
d0b50c834c | ||
|
|
612a06ba98 | ||
|
|
65d43d55c7 | ||
|
|
4fcd375ee2 | ||
|
|
a22f5562cf | ||
|
|
0365ea5e8a | ||
|
|
da300607f1 | ||
|
|
cc3174ee76 | ||
|
|
98c37caecd | ||
|
|
cc50e4a020 | ||
|
|
f734c4307b | ||
|
|
45573be8c7 | ||
|
|
eab1ffbe03 | ||
|
|
1504247b33 | ||
|
|
92a4277f73 | ||
|
|
31a663e86b | ||
|
|
5c0e2c32ee | ||
|
|
1d7104ae74 | ||
|
|
598f6f08af | ||
|
|
f0f2aadf53 | ||
|
|
ce37a672a3 | ||
|
|
526a54903e | ||
|
|
e1a632dcc7 | ||
|
|
d5d818e625 | ||
|
|
03de90c3d8 | ||
|
|
99096e0fc9 | ||
|
|
0395aa95b6 | ||
|
|
c624baed2c | ||
|
|
1a40321c0f |
10
README.md
10
README.md
@@ -6,9 +6,9 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||

|
||||

|
||||
|
||||
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
|
||||
### Join [the Discord](https://discord.gg/R9NSCkhcwt)!
|
||||
|
||||

|
||||

|
||||
|
||||
## Released Component Versions
|
||||
|
||||
@@ -46,6 +46,12 @@ You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get sqUN6VUb ccmsi.lua`
|
||||
|
||||
## Contributing
|
||||
|
||||
Please reach out to me via Discord or email (or GitHub in some way) if you are thinking of making any contributions at this time. I started this project as a challenge for myself and have been enjoying having something I can work on in my own way.
|
||||
|
||||
Once this is out of beta I will be more open to contributions, but for now I am hoping to keep them to a minimum as the remaining challenges are ones I am looking forward to solving.
|
||||
|
||||
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
|
||||
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ function coordinator.load_config()
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
@@ -192,7 +192,7 @@ end
|
||||
---@return function? update, function? done
|
||||
local function log_dmesg(message, dmesg_tag, working)
|
||||
local colors = {
|
||||
GRAPHICS = colors.green,
|
||||
RENDER = colors.green,
|
||||
SYSTEM = colors.cyan,
|
||||
BOOT = colors.blue,
|
||||
COMMS = colors.purple,
|
||||
@@ -206,7 +206,7 @@ local function log_dmesg(message, dmesg_tag, working)
|
||||
end
|
||||
end
|
||||
|
||||
function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
|
||||
function coordinator.log_render(message) log_dmesg(message, "RENDER") end
|
||||
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
|
||||
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
|
||||
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
|
||||
@@ -279,11 +279,12 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
-- send an API establish request response
|
||||
---@param packet scada_packet
|
||||
---@param ack ESTABLISH_ACK
|
||||
local function _send_api_establish_ack(packet, ack)
|
||||
---@param data any?
|
||||
local function _send_api_establish_ack(packet, ack, data)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack, data })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
|
||||
@@ -347,6 +348,7 @@ function coordinator.comms(version, nic, sv_watchdog)
|
||||
|
||||
ok = false
|
||||
elseif self.sv_config_err then
|
||||
self.est_task_done(false)
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
ok = false
|
||||
elseif (os.clock() - self.est_last) > 1.0 then
|
||||
@@ -470,10 +472,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,12 +484,19 @@ 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", api_v, " (expected v", comms.api_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)
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
|
||||
local conf = iocontrol.get_db().facility.conf
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW, { conf.num_units, conf.cooling })
|
||||
else
|
||||
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
|
||||
@@ -67,6 +67,7 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
-- facility data structure
|
||||
---@class ioctl_facility
|
||||
io.facility = {
|
||||
conf = conf,
|
||||
num_units = conf.num_units,
|
||||
tank_mode = conf.cooling.fac_tank_mode,
|
||||
tank_defs = conf.cooling.fac_tank_defs,
|
||||
@@ -91,6 +92,7 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
---@type WASTE_PRODUCT
|
||||
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
|
||||
auto_pu_fallback_active = false,
|
||||
auto_sps_disabled = false,
|
||||
|
||||
radiation = types.new_zero_radiation_reading(),
|
||||
|
||||
@@ -279,18 +281,18 @@ function iocontrol.init(conf, comms, temp_scale)
|
||||
|
||||
---@type alarms
|
||||
alarms = {
|
||||
ALARM_STATE.INACTIVE, -- containment breach
|
||||
ALARM_STATE.INACTIVE, -- containment radiation
|
||||
ALARM_STATE.INACTIVE, -- reactor lost
|
||||
ALARM_STATE.INACTIVE, -- damage critical
|
||||
ALARM_STATE.INACTIVE, -- reactor taking damage
|
||||
ALARM_STATE.INACTIVE, -- reactor over temperature
|
||||
ALARM_STATE.INACTIVE, -- reactor high temperature
|
||||
ALARM_STATE.INACTIVE, -- waste leak
|
||||
ALARM_STATE.INACTIVE, -- waste level high
|
||||
ALARM_STATE.INACTIVE, -- RPS transient
|
||||
ALARM_STATE.INACTIVE, -- RCS transient
|
||||
ALARM_STATE.INACTIVE -- turbine trip
|
||||
ALARM_STATE.INACTIVE, -- containment breach
|
||||
ALARM_STATE.INACTIVE, -- containment radiation
|
||||
ALARM_STATE.INACTIVE, -- reactor lost
|
||||
ALARM_STATE.INACTIVE, -- damage critical
|
||||
ALARM_STATE.INACTIVE, -- reactor taking damage
|
||||
ALARM_STATE.INACTIVE, -- reactor over temperature
|
||||
ALARM_STATE.INACTIVE, -- reactor high temperature
|
||||
ALARM_STATE.INACTIVE, -- waste leak
|
||||
ALARM_STATE.INACTIVE, -- waste level high
|
||||
ALARM_STATE.INACTIVE, -- RPS transient
|
||||
ALARM_STATE.INACTIVE, -- RCS transient
|
||||
ALARM_STATE.INACTIVE -- turbine trip
|
||||
},
|
||||
|
||||
annunciator = {}, ---@type annunciator
|
||||
@@ -376,6 +378,13 @@ function iocontrol.fp_monitor_state(id, connected)
|
||||
end
|
||||
end
|
||||
|
||||
-- report thread (routine) statuses
|
||||
---@param thread string thread name
|
||||
---@param ok boolean thread state
|
||||
function iocontrol.fp_rt_status(thread, ok)
|
||||
io.fp.ps.publish(util.c("routine__", thread), ok)
|
||||
end
|
||||
|
||||
-- report PKT firmware version and PKT session connection state
|
||||
---@param session_id integer PKT session
|
||||
---@param fw string firmware version
|
||||
@@ -585,7 +594,7 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
local ctl_status = status[1]
|
||||
|
||||
if type(ctl_status) == "table" and #ctl_status == 16 then
|
||||
if type(ctl_status) == "table" and #ctl_status == 17 then
|
||||
fac.all_sys_ok = ctl_status[1]
|
||||
fac.auto_ready = ctl_status[2]
|
||||
|
||||
@@ -636,9 +645,11 @@ function iocontrol.update_facility_status(status)
|
||||
|
||||
fac.auto_current_waste_product = ctl_status[15]
|
||||
fac.auto_pu_fallback_active = ctl_status[16]
|
||||
fac.auto_sps_disabled = ctl_status[17]
|
||||
|
||||
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
|
||||
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
|
||||
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
|
||||
else
|
||||
log.debug(log_header .. "control status not a table or length mismatch")
|
||||
valid = false
|
||||
@@ -655,10 +666,27 @@ function iocontrol.update_facility_status(status)
|
||||
fac.rtu_count = rtu_statuses.count
|
||||
|
||||
-- power statistics
|
||||
if type(rtu_statuses.power) == "table" then
|
||||
fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1])
|
||||
fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2])
|
||||
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
|
||||
if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then
|
||||
local data = fac.induction_data_tbl[1] ---@type imatrix_session_db
|
||||
local ps = fac.induction_ps_tbl[1] ---@type psil
|
||||
|
||||
local chg = tonumber(rtu_statuses.power[1])
|
||||
local in_f = tonumber(rtu_statuses.power[2])
|
||||
local out_f = tonumber(rtu_statuses.power[3])
|
||||
local eta = tonumber(rtu_statuses.power[4])
|
||||
|
||||
ps.publish("avg_charge", chg)
|
||||
ps.publish("avg_inflow", in_f)
|
||||
ps.publish("avg_outflow", out_f)
|
||||
ps.publish("eta_ms", eta)
|
||||
|
||||
ps.publish("is_charging", in_f > out_f)
|
||||
ps.publish("is_discharging", out_f > in_f)
|
||||
|
||||
if data and data.build then
|
||||
local cap = util.joules_to_fe(data.build.transfer_cap)
|
||||
ps.publish("at_max_io", in_f >= cap or out_f >= cap)
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "power statistics list not a table")
|
||||
valid = false
|
||||
|
||||
@@ -29,7 +29,8 @@ local self = {
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false
|
||||
pu_fallback = false,
|
||||
sps_low_power = false
|
||||
},
|
||||
waste_modes = {},
|
||||
priority_groups = {}
|
||||
@@ -65,6 +66,7 @@ function process.init(iocontrol, coord_comms)
|
||||
ctl_proc.limits = config.limits
|
||||
ctl_proc.waste_product = config.waste_product
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
ctl_proc.sps_low_power = config.sps_low_power
|
||||
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
@@ -72,6 +74,7 @@ function process.init(iocontrol, coord_comms)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
|
||||
|
||||
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
@@ -83,6 +86,7 @@ function process.init(iocontrol, coord_comms)
|
||||
-- notify supervisor of auto waste config
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power)
|
||||
end
|
||||
|
||||
-- unit waste states
|
||||
@@ -259,6 +263,18 @@ function process.set_pu_fallback(enabled)
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control SPS usage at low power
|
||||
---@param enabled boolean whether to enable SPS usage at low power
|
||||
function process.set_sps_low_power(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.sps_low_power = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- save process control settings
|
||||
---@param mode PROCESS control mode
|
||||
---@param burn_target number burn rate target
|
||||
|
||||
@@ -2,22 +2,26 @@
|
||||
-- Graphics Rendering Control
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
local log_render = coordinator.log_render
|
||||
|
||||
---@class coord_renderer
|
||||
local renderer = {}
|
||||
@@ -195,18 +199,21 @@ function renderer.try_start_ui()
|
||||
if engine.monitors.main ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.main,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- show flow view on flow monitor
|
||||
if engine.monitors.flow ~= nil then
|
||||
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
util.nop()
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -247,6 +254,11 @@ function renderer.close_ui()
|
||||
-- clear unit monitors
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end
|
||||
|
||||
if not engine.disable_flow_view then
|
||||
-- clear flow monitor
|
||||
engine.monitors.flow.clear()
|
||||
end
|
||||
|
||||
-- re-draw dmesg
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
@@ -383,12 +395,15 @@ function renderer.handle_resize(name)
|
||||
engine.dmesg_window.setVisible(not engine.ui_ready)
|
||||
|
||||
if engine.ui_ready then
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(ui.main_display)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
if ok then
|
||||
log_render("main view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.main_display then
|
||||
ui.main_display.delete()
|
||||
ui.main_display = nil
|
||||
@@ -416,14 +431,15 @@ function renderer.handle_resize(name)
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
|
||||
if engine.ui_ready then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
flow_view(ui.flow_display)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
if ok then
|
||||
log_render("flow view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.flow_display then
|
||||
ui.flow_display.delete()
|
||||
ui.flow_display = nil
|
||||
@@ -453,14 +469,15 @@ function renderer.handle_resize(name)
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
|
||||
if engine.ui_ready then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
local ok = pcall(function ()
|
||||
ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(ui.unit_displays[idx], idx)
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
if ok then
|
||||
log_render("unit " .. idx .. " view re-draw completed in " .. (util.time_ms() - draw_start) .. "ms")
|
||||
else
|
||||
if ui.unit_displays[idx] then
|
||||
ui.unit_displays[idx].delete()
|
||||
ui.unit_displays[idx] = nil
|
||||
|
||||
@@ -8,7 +8,7 @@ local iocontrol = require("coordinator.iocontrol")
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
-- local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
-- retry time constants in ms
|
||||
@@ -73,18 +73,18 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
end
|
||||
|
||||
-- send a CRDN packet
|
||||
-----@param msg_type CRDN_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local c_pkt = comms.crdn_packet()
|
||||
---@param msg_type CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local c_pkt = comms.crdn_packet()
|
||||
|
||||
-- c_pkt.make(msg_type, msg)
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
c_pkt.make(msg_type, msg)
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
|
||||
-- out_queue.push_packet(s_pkt)
|
||||
-- self.seq_num = self.seq_num + 1
|
||||
-- end
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type MGMT_TYPE
|
||||
@@ -120,8 +120,39 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == nil then
|
||||
if pkt.type == CRDN_TYPE.API_GET_FAC then
|
||||
local fac = db.facility
|
||||
|
||||
local data = {
|
||||
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)
|
||||
elseif pkt.type == CRDN_TYPE.API_GET_UNITS then
|
||||
local data = {}
|
||||
|
||||
for i = 1, #db.units do
|
||||
local u = db.units[i] ---@type ioctl_unit
|
||||
table.insert(data, {
|
||||
u.unit_id,
|
||||
u.num_boilers,
|
||||
u.num_turbines,
|
||||
u.num_snas,
|
||||
u.has_tank
|
||||
})
|
||||
end
|
||||
|
||||
_send(CRDN_TYPE.API_GET_UNITS, data)
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
|
||||
@@ -7,32 +7,29 @@ require("/initenv").init_env()
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local configure = require("coordinator.configure")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local threads = require("coordinator.threads")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local COORDINATOR_VERSION = "v1.3.5"
|
||||
local COORDINATOR_VERSION = "v1.4.5"
|
||||
|
||||
local CHUNK_LOAD_DELAY_S = 30.0
|
||||
|
||||
local println = util.println
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local log_graphics = coordinator.log_graphics
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_render = coordinator.log_render
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_crypto = coordinator.log_crypto
|
||||
|
||||
----------------------------------------
|
||||
@@ -129,16 +126,58 @@ local function main()
|
||||
-- lets get started!
|
||||
log.info("monitors ready, dmesg output incoming...")
|
||||
|
||||
log_graphics("displays connected and reset")
|
||||
log_render("displays connected and reset")
|
||||
log_sys("system start on " .. os.date("%c"))
|
||||
log_boot("starting " .. COORDINATOR_VERSION)
|
||||
|
||||
----------------------------------------
|
||||
-- memory allocation
|
||||
----------------------------------------
|
||||
|
||||
-- shared memory across threads
|
||||
---@class crd_shared_memory
|
||||
local __shared_memory = {
|
||||
-- time and date format for display
|
||||
date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y"),
|
||||
|
||||
-- coordinator system state flags
|
||||
---@class crd_state
|
||||
crd_state = {
|
||||
fp_ok = false,
|
||||
ui_ok = true, -- default true, used to abort on fail
|
||||
link_fail = false,
|
||||
shutdown = false
|
||||
},
|
||||
|
||||
-- core coordinator devices
|
||||
crd_dev = {
|
||||
speaker = ppm.get_device("speaker"),
|
||||
modem = ppm.get_wireless_modem()
|
||||
},
|
||||
|
||||
-- system objects
|
||||
crd_sys = {
|
||||
nic = nil, ---@type nic
|
||||
coord_comms = nil, ---@type coord_comms
|
||||
conn_watchdog = nil ---@type watchdog
|
||||
},
|
||||
|
||||
-- message queues
|
||||
q = {
|
||||
mq_render = mqueue.new()
|
||||
}
|
||||
}
|
||||
|
||||
local smem_dev = __shared_memory.crd_dev
|
||||
local smem_sys = __shared_memory.crd_sys
|
||||
|
||||
local crd_state = __shared_memory.crd_state
|
||||
|
||||
----------------------------------------
|
||||
-- setup alarm sounder subsystem
|
||||
----------------------------------------
|
||||
|
||||
local speaker = ppm.get_device("speaker")
|
||||
if speaker == nil then
|
||||
if smem_dev.speaker == nil then
|
||||
log_boot("annunciator alarm speaker not found")
|
||||
println("startup> speaker not found")
|
||||
log.fatal("no annunciator alarm speaker found")
|
||||
@@ -146,7 +185,7 @@ local function main()
|
||||
else
|
||||
local sounder_start = util.time_ms()
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
sounder.init(speaker, config.SpeakerVolume)
|
||||
sounder.init(smem_dev.speaker, config.SpeakerVolume)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
@@ -163,8 +202,7 @@ local function main()
|
||||
end
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
if smem_dev.modem == nil then
|
||||
log_comms("wireless modem not found")
|
||||
println("startup> wireless modem not found")
|
||||
log.fatal("no wireless modem on startup")
|
||||
@@ -175,243 +213,54 @@ local function main()
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
conn_watchdog.cancel()
|
||||
smem_sys.conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
smem_sys.conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog)
|
||||
smem_sys.nic = network.nic(smem_dev.modem)
|
||||
smem_sys.coord_comms = coordinator.comms(COORDINATOR_VERSION, smem_sys.nic, smem_sys.conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
-- base loop clock (2Hz, 10 ticks)
|
||||
local MAIN_CLOCK = 0.5
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
----------------------------------------
|
||||
-- start front panel & UI start function
|
||||
-- start front panel
|
||||
----------------------------------------
|
||||
|
||||
log_graphics("starting front panel UI...")
|
||||
log_render("starting front panel UI...")
|
||||
|
||||
local fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not fp_ok then
|
||||
log_graphics(util.c("front panel UI error: ", fp_message))
|
||||
local fp_message
|
||||
crd_state.fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not crd_state.fp_ok then
|
||||
log_render(util.c("front panel UI error: ", fp_message))
|
||||
println_ts("front panel UI creation failed")
|
||||
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
|
||||
return
|
||||
else log_graphics("front panel ready") end
|
||||
|
||||
-- start up the main UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function start_main_ui()
|
||||
log_graphics("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not ui_ok then
|
||||
log_graphics(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
else log_render("front panel ready") end
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
-- start system
|
||||
----------------------------------------
|
||||
|
||||
local link_failed = false
|
||||
local ui_ok = true
|
||||
local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
-- init threads
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local render_thread = threads.thread__render(__shared_memory)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
log.info("startup> completed")
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- main event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_disconnect(device) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_reconnect(param1, device) then
|
||||
log_sys(util.c("configured monitor ", param1, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", param1, " connected"))
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_resize" then
|
||||
local is_used, is_ok = renderer.handle_resize(param1)
|
||||
if is_used then
|
||||
log_sys(util.c("configured monitor ", param1, " resized, ", util.trinary(is_ok, "display still fits", "display no longer fits")))
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
link_failed = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, proceeding to main UI start")
|
||||
ui_ok = start_main_ui()
|
||||
if not ui_ok then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
-- free any closed sessions
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- update date and time string for main display
|
||||
if coord_comms.is_linked() then
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
-- handle supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
else link_failed = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
-- run threads
|
||||
parallel.waitForAll(main_thread.p_exec, render_thread.p_exec)
|
||||
|
||||
renderer.close_ui()
|
||||
renderer.close_fp()
|
||||
sounder.stop()
|
||||
log_sys("system shutdown")
|
||||
|
||||
if link_failed then println_ts("failed to connect to supervisor") end
|
||||
if not ui_ok then println_ts("main UI creation failed") end
|
||||
if crd_state.link_fail then println_ts("failed to connect to supervisor") end
|
||||
if not crd_state.ui_ok then println_ts("main UI creation failed") end
|
||||
|
||||
-- close on error exit (such as UI error)
|
||||
if coord_comms.is_linked() then coord_comms.close() end
|
||||
if smem_sys.coord_comms.is_linked() then smem_sys.coord_comms.close() end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
|
||||
363
coordinator/threads.lua
Normal file
363
coordinator/threads.lua
Normal file
@@ -0,0 +1,363 @@
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local log_render = coordinator.log_render
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_comms = coordinator.log_comms
|
||||
|
||||
local threads = {}
|
||||
|
||||
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||
local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
|
||||
|
||||
local MQ__RENDER_CMD = {
|
||||
START_MAIN_UI = 1
|
||||
}
|
||||
|
||||
local MQ__RENDER_DATA = {
|
||||
MON_CONNECT = 1,
|
||||
MON_DISCONNECT = 2,
|
||||
MON_RESIZE = 3
|
||||
}
|
||||
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__main(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("main", true)
|
||||
log.debug("main thread start")
|
||||
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local nic = smem.crd_sys.nic
|
||||
local coord_comms = smem.crd_sys.coord_comms
|
||||
local conn_watchdog = smem.crd_sys.conn_watchdog
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "peripheral_detach" then
|
||||
local type, device = ppm.handle_unmount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_DISCONNECT, device)
|
||||
elseif type == "speaker" then
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
local type, device = ppm.mount(param1)
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
log_sys("comms modem reconnected")
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_CONNECT, { name = param1, device = device })
|
||||
elseif type == "speaker" then
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_resize" then
|
||||
smem.q.mq_render.push_data(MQ__RENDER_DATA.MON_RESIZE, param1)
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
crd_state.link_fail = true
|
||||
crd_state.shutdown = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, dispatching main UI start")
|
||||
smem.q.mq_render.push_command(MQ__RENDER_CMD.START_MAIN_UI)
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions and free any closed ones
|
||||
apisessions.iterate_all()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
if renderer.ui_ready() then
|
||||
-- update clock used on main and flow monitors
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
-- check API watchdogs
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
end
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
sounder.continue()
|
||||
end
|
||||
|
||||
-- check for termination request or UI crash
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminate requested, main thread exiting")
|
||||
elseif not crd_state.ui_ok then
|
||||
crd_state.shutdown = true
|
||||
log.info("terminating due to fatal UI error")
|
||||
end
|
||||
|
||||
if crd_state.shutdown then
|
||||
-- handle closing supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("closing supervisor connection...")
|
||||
else crd_state.link_fail = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- execute the thread in a protected mode, retrying it on return if not shutting down
|
||||
function public.p_exec()
|
||||
local crd_state = smem.crd_state
|
||||
|
||||
while not crd_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("main", false)
|
||||
|
||||
-- if status is true, then we are probably exiting, so this won't matter
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate")
|
||||
if not crd_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
-- coordinator renderer thread, tasked with long duration re-draws
|
||||
---@nodiscard
|
||||
---@param smem crd_shared_memory
|
||||
function threads.thread__render(smem)
|
||||
---@class parallel_thread
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
iocontrol.fp_rt_status("render", true)
|
||||
log.debug("render thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local crd_state = smem.crd_state
|
||||
local render_queue = smem.q.mq_render
|
||||
|
||||
local last_update = util.time()
|
||||
|
||||
-- thread loop
|
||||
while true do
|
||||
-- check for messages in the message queue
|
||||
while render_queue.ready() and not crd_state.shutdown do
|
||||
local msg = render_queue.pop()
|
||||
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if msg.message == MQ__RENDER_CMD.START_MAIN_UI then
|
||||
-- stop the UI if it was already started
|
||||
-- this may occur on a quick supervisor disconnect -> connect
|
||||
if renderer.ui_ready() then
|
||||
log_render("closing main UI before executing new request to start")
|
||||
renderer.close_ui()
|
||||
end
|
||||
|
||||
-- start up the main UI
|
||||
log_render("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_message
|
||||
crd_state.ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not crd_state.ui_ok then
|
||||
log_render(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_render("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
local cmd = msg.message ---@type queue_data
|
||||
|
||||
if cmd.key == MQ__RENDER_DATA.MON_CONNECT then
|
||||
-- monitor connected
|
||||
if renderer.handle_reconnect(cmd.val.name, cmd.val.device) then
|
||||
log_sys(util.c("configured monitor ", cmd.val.name, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", cmd.val.name, " connected"))
|
||||
end
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_DISCONNECT then
|
||||
-- monitor disconnected
|
||||
if renderer.handle_disconnect(cmd.val) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif cmd.key == MQ__RENDER_DATA.MON_RESIZE then
|
||||
-- monitor resized
|
||||
local is_used, is_ok = renderer.handle_resize(cmd.val)
|
||||
if is_used then
|
||||
log_sys(util.c("configured monitor ", cmd.val, " resized, ", util.trinary(is_ok, "display fits", "display does not fit")))
|
||||
end
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- received a packet
|
||||
end
|
||||
end
|
||||
|
||||
-- quick yield
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if crd_state.shutdown then
|
||||
log.info("render thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
-- delay before next check
|
||||
last_update = util.adaptive_delay(RENDER_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 crd_state = smem.crd_state
|
||||
|
||||
while not crd_state.shutdown do
|
||||
local status, result = pcall(public.exec)
|
||||
if status == false then
|
||||
log.fatal(util.strval(result))
|
||||
end
|
||||
|
||||
iocontrol.fp_rt_status("render", false)
|
||||
|
||||
if not crd_state.shutdown then
|
||||
log.info("render thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return threads
|
||||
@@ -9,6 +9,7 @@ local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local PowerIndicator = require("graphics.elements.indicators.power")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
@@ -26,9 +27,13 @@ local ALIGN = core.ALIGN
|
||||
---@param ps psil ps interface
|
||||
---@param id number? matrix ID
|
||||
local function new_view(root, x, y, data, ps, id)
|
||||
local label_fg = style.theme.label_fg
|
||||
local text_fg = style.theme.text_fg
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local title = "INDUCTION MATRIX"
|
||||
if type(id) == "number" then title = title .. id end
|
||||
|
||||
@@ -42,45 +47,47 @@ local function new_view(root, x, y, data, ps, id)
|
||||
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
|
||||
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
|
||||
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
|
||||
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
||||
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
||||
|
||||
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
|
||||
avg_chg.register(ps, "avg_charge", avg_chg.update)
|
||||
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
|
||||
avg_in.register(ps, "avg_inflow", avg_in.update)
|
||||
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
|
||||
avg_out.register(ps, "avg_outflow", avg_out.update)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg}
|
||||
|
||||
local cells = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
local providers = DataIndicator{parent=rect,x=11,y=15,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
|
||||
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=style.theme.label_fg}
|
||||
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg}
|
||||
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
|
||||
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
|
||||
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
|
||||
cells.register(ps, "cells", cells.update)
|
||||
providers.register(ps, "providers", providers.update)
|
||||
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
|
||||
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
|
||||
|
||||
local chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht}
|
||||
local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht}
|
||||
local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel}
|
||||
|
||||
chging.register(ps, "is_charging", chging.update)
|
||||
dischg.register(ps, "is_discharging", dischg.update)
|
||||
max_io.register(ps, "at_max_io", max_io.update)
|
||||
|
||||
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
|
||||
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
|
||||
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
|
||||
|
||||
TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg}
|
||||
TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg}
|
||||
TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg}
|
||||
|
||||
local function calc_saturation(val)
|
||||
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
|
||||
@@ -91,6 +98,49 @@ local function new_view(root, x, y, data, ps, id)
|
||||
charge.register(ps, "energy_fill", charge.update)
|
||||
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
|
||||
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
|
||||
|
||||
local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
|
||||
|
||||
eta.register(ps, "eta_ms", function (eta_ms)
|
||||
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
|
||||
|
||||
local seconds = math.abs(eta_ms) / 1000
|
||||
local minutes = seconds / 60
|
||||
local hours = minutes / 60
|
||||
local days = hours / 24
|
||||
|
||||
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
|
||||
-- really small or NaN
|
||||
str = "No ETA"
|
||||
elseif days < 1000 then
|
||||
days = math.floor(days)
|
||||
hours = math.floor(hours % 24)
|
||||
minutes = math.floor(minutes % 60)
|
||||
seconds = math.floor(seconds % 60)
|
||||
|
||||
if days > 0 then
|
||||
str = days .. "d"
|
||||
elseif hours > 0 then
|
||||
str = hours .. "h " .. minutes .. "m"
|
||||
elseif minutes > 0 then
|
||||
str = minutes .. "m " .. seconds .. "s"
|
||||
elseif seconds > 0 then
|
||||
str = seconds .. "s"
|
||||
end
|
||||
|
||||
str = pre .. str
|
||||
else
|
||||
local years = math.floor(days / 365.25)
|
||||
|
||||
if years <= 99999999 then
|
||||
str = pre .. years .. "y"
|
||||
else
|
||||
str = pre .. "eras"
|
||||
end
|
||||
end
|
||||
|
||||
eta.set_value(str)
|
||||
end)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -145,7 +145,7 @@ local function new_view(root, x, y)
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
@@ -341,31 +341,25 @@ local function new_view(root, x, y)
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,style.theme.checkbox_bg)}
|
||||
|
||||
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht}
|
||||
local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel}
|
||||
|
||||
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
|
||||
sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update)
|
||||
|
||||
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
|
||||
|
||||
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
|
||||
TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
|
||||
|
||||
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
|
||||
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
|
||||
local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
|
||||
|
||||
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(facility.ps, "po_rate", po_rate.update)
|
||||
am_rate.register(facility.ps, "am_rate", am_rate.update)
|
||||
TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label}
|
||||
|
||||
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
|
||||
|
||||
sna_count.register(facility.ps, "sna_count", sna_count.update)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value)
|
||||
end
|
||||
|
||||
return new_view
|
||||
|
||||
@@ -352,6 +352,8 @@ local function init(parent, id)
|
||||
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
----------------------
|
||||
-- reactor controls --
|
||||
----------------------
|
||||
|
||||
@@ -250,6 +250,7 @@ local function init(main)
|
||||
local y_offset = y_ofs(i)
|
||||
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
|
||||
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
|
||||
util.nop()
|
||||
end
|
||||
|
||||
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=style.theme.bg}
|
||||
@@ -335,6 +336,8 @@ local function init(main)
|
||||
end
|
||||
end
|
||||
|
||||
util.nop()
|
||||
|
||||
---------
|
||||
-- SPS --
|
||||
---------
|
||||
|
||||
@@ -100,6 +100,14 @@ local function init(panel, num_units)
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
system.line_break()
|
||||
|
||||
local rt_main = LED{parent=system,label="RT MAIN",colors=led_grn}
|
||||
local rt_render = LED{parent=system,label="RT RENDER",colors=led_grn}
|
||||
|
||||
rt_main.register(ps, "routine__main", rt_main.update)
|
||||
rt_render.register(ps, "routine__render", rt_render.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp.disabled_fg}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
@@ -53,6 +55,8 @@ local function init(main)
|
||||
|
||||
cnc_y_start = cnc_y_start + row_1_height + 1
|
||||
|
||||
util.nop()
|
||||
|
||||
if facility.num_units >= 3 then
|
||||
-- base offset 3, spacing 1, max height of units 1 and 2
|
||||
local row_2_offset = cnc_y_start
|
||||
@@ -64,6 +68,8 @@ local function init(main)
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
|
||||
util.nop()
|
||||
end
|
||||
|
||||
-- command & control
|
||||
@@ -79,6 +85,8 @@ local function init(main)
|
||||
|
||||
process_ctl(main, 2, cnc_bottom_align_start)
|
||||
|
||||
util.nop()
|
||||
|
||||
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
|
||||
end
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.2.2"
|
||||
core.version = "2.2.3"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
@@ -49,9 +49,11 @@ local element = {}
|
||||
---|indicator_light_args
|
||||
---|power_indicator_args
|
||||
---|rad_indicator_args
|
||||
---|signal_bar_args
|
||||
---|state_indicator_args
|
||||
---|tristate_indicator_light_args
|
||||
---|vbar_args
|
||||
---|app_multipane_args
|
||||
---|colormap_args
|
||||
---|displaybox_args
|
||||
---|div_args
|
||||
|
||||
109
graphics/elements/appmultipane.lua
Normal file
109
graphics/elements/appmultipane.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
-- App Page Multi-Pane Display Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
local events = require("graphics.events")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_multipane_args
|
||||
---@field panes table panes to swap between
|
||||
---@field nav_colors cpair on/off colors (a/b respectively) for page navigator
|
||||
---@field scroll_nav boolean? true to allow scrolling to change the active pane
|
||||
---@field drag_nav boolean? true to allow mouse dragging to change the active pane (on mouse up)
|
||||
---@field callback function? function to call when pane is changed by mouse interaction
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new app multipane element
|
||||
---@nodiscard
|
||||
---@param args app_multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
element.assert(type(args.panes) == "table", "panes is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
local nav_x_start = math.floor((e.frame.w / 2) - (#args.panes / 2)) + 1
|
||||
local nav_x_end = math.floor((e.frame.w / 2) - (#args.panes / 2)) + #args.panes
|
||||
|
||||
-- show the selected pane
|
||||
function e.redraw()
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[e.value].show()
|
||||
|
||||
-- draw page indicator dots
|
||||
for i = 1, #args.panes do
|
||||
e.w_set_cur(nav_x_start + (i - 1), e.frame.h)
|
||||
e.w_set_fgd(util.trinary(i == e.value, args.nav_colors.color_a, args.nav_colors.color_b))
|
||||
e.w_write("\x07")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
local initial = e.value
|
||||
|
||||
if e.enabled then
|
||||
if event.current.y == e.frame.h and event.current.x >= nav_x_start and event.current.x <= nav_x_end then
|
||||
local id = event.current.x - nav_x_start + 1
|
||||
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
e.set_value(id)
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
e.set_value(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if args.scroll_nav then
|
||||
if event.type == events.MOUSE_CLICK.SCROLL_DOWN then
|
||||
e.set_value(e.value + 1)
|
||||
elseif event.type == events.MOUSE_CLICK.SCROLL_UP then
|
||||
e.set_value(e.value - 1)
|
||||
end
|
||||
end
|
||||
|
||||
if args.drag_nav then
|
||||
local x1, x2 = event.initial.x, event.current.x
|
||||
if event.type == events.MOUSE_CLICK.UP and e.in_frame_bounds(x1, event.initial.y) and e.in_frame_bounds(x1, event.current.y) then
|
||||
if x2 > x1 then
|
||||
e.set_value(e.value - 1)
|
||||
elseif x2 < x1 then
|
||||
e.set_value(e.value + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if e.value ~= initial and type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
|
||||
-- select which pane is shown
|
||||
---@param value integer pane to show
|
||||
function e.set_value(value)
|
||||
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
|
||||
e.value = value
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multipane
|
||||
@@ -65,7 +65,7 @@ local function switch_button(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
-- set the value (does not call the callback)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
|
||||
85
graphics/elements/indicators/signal.lua
Normal file
85
graphics/elements/indicators/signal.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- Signal Bars Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class signal_bar_args
|
||||
---@field compact? boolean true to use a single character (works better against edges that extend out colors)
|
||||
---@field colors_low_med? cpair color a for low signal quality, color b for medium signal quality
|
||||
---@field disconnect_color? color color for the 'x' on disconnect
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors (foreground is used for high signal quality)
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new signal bar
|
||||
---@nodiscard
|
||||
---@param args signal_bar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function signal_bar(args)
|
||||
args.height = 1
|
||||
args.width = util.trinary(args.compact, 1, 2)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0
|
||||
|
||||
local blit_bkg = args.fg_bg.blit_bkg
|
||||
local blit_0, blit_1, blit_2, blit_3 = args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd, args.fg_bg.blit_fgd
|
||||
|
||||
if type(args.colors_low_med) == "table" then
|
||||
blit_1 = args.colors_low_med.blit_a or blit_1
|
||||
blit_2 = args.colors_low_med.blit_b or blit_2
|
||||
end
|
||||
|
||||
if util.is_int(args.disconnect_color) then blit_0 = colors.toBlit(args.disconnect_color) end
|
||||
|
||||
-- on state change (0 = offline, 1 through 3 = low to high signal)
|
||||
---@param new_state integer signal state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set signal state (0 = offline, 1 through 3 = low to high signal)
|
||||
---@param val integer signal state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- draw label and signal bar
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.compact then
|
||||
if e.value == 1 then
|
||||
e.w_blit("\x90", blit_1, blit_bkg)
|
||||
elseif e.value == 2 then
|
||||
e.w_blit("\x94", blit_2, blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x95", blit_3, blit_bkg)
|
||||
else
|
||||
e.w_blit("x", blit_0, blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 1 then
|
||||
e.w_blit("\x9f ", blit_bkg .. blit_bkg, blit_1 .. blit_bkg)
|
||||
elseif e.value == 2 then
|
||||
e.w_blit("\x9f\x94", blit_bkg .. blit_2, blit_2 .. blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.w_blit("\x9f\x81", blit_bkg .. blit_bkg, blit_3 .. blit_3)
|
||||
else
|
||||
e.w_blit(" x", blit_0 .. blit_0, blit_bkg .. blit_bkg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return signal_bar
|
||||
@@ -2,18 +2,17 @@
|
||||
-- I/O Control for Pocket Integration with Supervisor & Coordinator
|
||||
--
|
||||
|
||||
local psil = require("scada-common.psil")
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local psil = require("scada-common.psil")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
|
||||
local iocontrol = {}
|
||||
---@todo nominal trip time is ping (0ms to 10ms usually)
|
||||
local WARN_TT = 40
|
||||
local HIGH_TT = 80
|
||||
|
||||
---@class pocket_ioctl
|
||||
local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
local iocontrol = {}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
@@ -23,23 +22,175 @@ local LINK_STATE = {
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
---@enum NAV_PAGE
|
||||
local NAV_PAGE = {
|
||||
HOME = 1,
|
||||
iocontrol.LINK_STATE = LINK_STATE
|
||||
|
||||
---@enum POCKET_APP_ID
|
||||
local APP_ID = {
|
||||
ROOT = 1,
|
||||
-- main app page
|
||||
UNITS = 2,
|
||||
REACTORS = 3,
|
||||
BOILERS = 4,
|
||||
TURBINES = 5,
|
||||
DIAG = 6,
|
||||
D_ALARMS = 7
|
||||
ABOUT = 3,
|
||||
-- diag app page
|
||||
ALARMS = 4,
|
||||
-- other
|
||||
DUMMY = 5,
|
||||
NUM_APPS = 5
|
||||
}
|
||||
|
||||
iocontrol.LINK_STATE = LINK_STATE
|
||||
iocontrol.NAV_PAGE = NAV_PAGE
|
||||
iocontrol.APP_ID = APP_ID
|
||||
|
||||
---@class pocket_ioctl
|
||||
local io = {
|
||||
version = "unknown",
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
---@class nav_tree_page
|
||||
---@field _p nav_tree_page|nil page's parent
|
||||
---@field _c table page's children
|
||||
---@field nav_to function function to navigate to this page
|
||||
---@field switcher function|nil function to switch between children
|
||||
---@field tasks table tasks to run while viewing this page
|
||||
|
||||
-- allocate the page navigation system
|
||||
function iocontrol.alloc_nav()
|
||||
local self = {
|
||||
pane = nil, ---@type graphics_element
|
||||
apps = {},
|
||||
containers = {},
|
||||
cur_app = APP_ID.ROOT
|
||||
}
|
||||
|
||||
self.cur_page = self.root
|
||||
|
||||
---@class pocket_nav
|
||||
io.nav = {}
|
||||
|
||||
-- set the root pane element to switch between apps with
|
||||
---@param root_pane graphics_element
|
||||
function io.nav.set_pane(root_pane)
|
||||
self.pane = root_pane
|
||||
end
|
||||
|
||||
-- register an app
|
||||
---@param app_id POCKET_APP_ID app ID
|
||||
---@param container graphics_element element that contains this app (usually a Div)
|
||||
---@param pane graphics_element? multipane if this is a simple paned app, then nav_to must be a number
|
||||
function io.nav.register_app(app_id, container, pane)
|
||||
---@class pocket_app
|
||||
local app = {
|
||||
root = { _p = nil, _c = {}, nav_to = function () end, tasks = {} }, ---@type nav_tree_page
|
||||
cur_page = nil, ---@type nav_tree_page
|
||||
pane = pane,
|
||||
paned_pages = {}
|
||||
}
|
||||
|
||||
-- delayed set of the pane if it wasn't ready at the start
|
||||
---@param root_pane graphics_element multipane
|
||||
function app.set_root_pane(root_pane)
|
||||
app.pane = root_pane
|
||||
end
|
||||
|
||||
-- if a pane was provided, this will switch between numbered pages
|
||||
---@param idx integer page index
|
||||
function app.switcher(idx)
|
||||
if app.paned_pages[idx] then
|
||||
app.paned_pages[idx].nav_to()
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new page entry in the app's page navigation tree
|
||||
---@param parent nav_tree_page? a parent page or nil to set this as the root
|
||||
---@param nav_to function|integer function to navigate to this page or pane index
|
||||
---@return nav_tree_page new_page this new page
|
||||
function app.new_page(parent, nav_to)
|
||||
---@type nav_tree_page
|
||||
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
|
||||
|
||||
if parent == nil then
|
||||
app.root = page
|
||||
if app.cur_page == nil then app.cur_page = page end
|
||||
end
|
||||
|
||||
if type(nav_to) == "number" then
|
||||
app.paned_pages[nav_to] = page
|
||||
|
||||
function page.nav_to()
|
||||
app.cur_page = page
|
||||
if app.pane then app.pane.set_value(nav_to) end
|
||||
end
|
||||
else
|
||||
function page.nav_to()
|
||||
app.cur_page = page
|
||||
nav_to()
|
||||
end
|
||||
end
|
||||
|
||||
-- switch between children
|
||||
---@param id integer child ID
|
||||
function page.switcher(id) if page._c[id] then page._c[id].nav_to() end end
|
||||
|
||||
if parent ~= nil then
|
||||
table.insert(page._p._c, page)
|
||||
end
|
||||
|
||||
return page
|
||||
end
|
||||
|
||||
-- get the currently active page
|
||||
function app.get_current_page() return app.cur_page end
|
||||
|
||||
-- attempt to navigate up the tree
|
||||
---@return boolean success true if successfully navigated up
|
||||
function app.nav_up()
|
||||
local parent = app.cur_page._p
|
||||
if parent then parent.nav_to() end
|
||||
return parent ~= nil
|
||||
end
|
||||
|
||||
self.apps[app_id] = app
|
||||
self.containers[app_id] = container
|
||||
|
||||
return app
|
||||
end
|
||||
|
||||
-- get a list of the app containers (usually Div elements)
|
||||
function io.nav.get_containers() return self.containers end
|
||||
|
||||
-- open a given app
|
||||
---@param app_id POCKET_APP_ID
|
||||
function io.nav.open_app(app_id)
|
||||
if self.apps[app_id] then
|
||||
self.cur_app = app_id
|
||||
self.pane.set_value(app_id)
|
||||
else
|
||||
log.debug("tried to open unknown app")
|
||||
end
|
||||
end
|
||||
|
||||
-- get the currently active page
|
||||
---@return nav_tree_page
|
||||
function io.nav.get_current_page()
|
||||
return self.apps[self.cur_app].get_current_page()
|
||||
end
|
||||
|
||||
-- attempt to navigate up
|
||||
function io.nav.nav_up()
|
||||
local app = self.apps[self.cur_app] ---@type pocket_app
|
||||
log.debug("attempting app nav up for app " .. self.cur_app)
|
||||
|
||||
if not app.nav_up() then
|
||||
log.debug("internal app nav up failed, going to home screen")
|
||||
io.nav.open_app(APP_ID.ROOT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize facility-independent components of pocket iocontrol
|
||||
---@param comms pocket_comms
|
||||
function iocontrol.init_core(comms)
|
||||
iocontrol.alloc_nav()
|
||||
|
||||
---@class pocket_ioctl_diag
|
||||
io.diag = {}
|
||||
|
||||
@@ -76,29 +227,135 @@ function iocontrol.init_core(comms)
|
||||
alarm_buttons = {},
|
||||
tone_indicators = {} -- indicators to update from supervisor tone states
|
||||
}
|
||||
|
||||
---@class pocket_nav
|
||||
io.nav = {
|
||||
page = NAV_PAGE.HOME, ---@type NAV_PAGE
|
||||
sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG },
|
||||
tasks = {}
|
||||
}
|
||||
|
||||
-- add a task to be performed periodically while on a given page
|
||||
---@param page NAV_PAGE page to add task to
|
||||
---@param task function function to execute
|
||||
function io.nav.register_task(page, task)
|
||||
if io.nav.tasks[page] == nil then io.nav.tasks[page] = {} end
|
||||
table.insert(io.nav.tasks[page], task)
|
||||
end
|
||||
end
|
||||
|
||||
-- initialize facility-dependent components of pocket iocontrol
|
||||
function iocontrol.init_fac() end
|
||||
---@param conf facility_conf configuration
|
||||
---@param temp_scale 1|2|3|4 temperature unit (1 = K, 2 = C, 3 = F, 4 = R)
|
||||
function iocontrol.init_fac(conf, 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
|
||||
function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end
|
||||
function iocontrol.report_link_state(state)
|
||||
io.ps.publish("link_state", state)
|
||||
|
||||
if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
||||
io.ps.publish("svr_conn_quality", 0)
|
||||
end
|
||||
|
||||
if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then
|
||||
io.ps.publish("crd_conn_quality", 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- determine supervisor connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_svr_tt(trip_time)
|
||||
local state = 3
|
||||
if trip_time > HIGH_TT then
|
||||
state = 1
|
||||
elseif trip_time > WARN_TT then
|
||||
state = 2
|
||||
end
|
||||
|
||||
io.ps.publish("svr_conn_quality", state)
|
||||
end
|
||||
|
||||
-- determine coordinator connection quality (trip time)
|
||||
---@param trip_time integer
|
||||
function iocontrol.report_crd_tt(trip_time)
|
||||
local state = 3
|
||||
if trip_time > HIGH_TT then
|
||||
state = 1
|
||||
elseif trip_time > WARN_TT then
|
||||
state = 2
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -125,7 +126,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
|
||||
-- attempt coordinator API connection establishment
|
||||
local function _send_api_establish()
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||
_send_crd(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT, comms.api_version })
|
||||
end
|
||||
|
||||
-- keep alive ack to supervisor
|
||||
@@ -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 or 0))
|
||||
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
|
||||
|
||||
@@ -290,11 +322,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
log.warning("pocket coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket coordinator RTT = " .. trip_time .. "ms")
|
||||
-- log.debug("pocket coordinator TT = " .. trip_time .. "ms")
|
||||
|
||||
_send_api_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("coordinator SCADA keep alive packet length mismatch")
|
||||
|
||||
iocontrol.report_crd_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
@@ -303,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 unit options
|
||||
iocontrol.init_fac(conf, 1)
|
||||
|
||||
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
|
||||
@@ -334,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")
|
||||
@@ -372,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
|
||||
|
||||
@@ -380,11 +428,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
log.warning("pocket supervisor KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("pocket supervisor RTT = " .. trip_time .. "ms")
|
||||
-- log.debug("pocket supervisor TT = " .. trip_time .. "ms")
|
||||
|
||||
_send_sv_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("supervisor SCADA keep alive packet length mismatch")
|
||||
|
||||
iocontrol.report_svr_tt(trip_time)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
@@ -394,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
|
||||
@@ -438,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
|
||||
@@ -474,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
|
||||
@@ -500,5 +540,4 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
return pocket
|
||||
|
||||
@@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol")
|
||||
local pocket = require("pocket.pocket")
|
||||
local renderer = require("pocket.renderer")
|
||||
|
||||
local POCKET_VERSION = "v0.7.3-alpha"
|
||||
local POCKET_VERSION = "v0.8.0-alpha"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -68,6 +68,9 @@ local function main()
|
||||
-- mount connected devices
|
||||
ppm.mount_all()
|
||||
|
||||
-- record version for GUI
|
||||
iocontrol.get_db().version = POCKET_VERSION
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications & clocks
|
||||
----------------------------------------
|
||||
@@ -131,7 +134,7 @@ local function main()
|
||||
-- start connection watchdogs
|
||||
conn_wd.sv.feed()
|
||||
conn_wd.api.feed()
|
||||
log.debug("startup> conn watchdog started")
|
||||
log.debug("startup> conn watchdogs started")
|
||||
|
||||
local io_db = iocontrol.get_db()
|
||||
local nav = io_db.nav
|
||||
@@ -149,11 +152,8 @@ local function main()
|
||||
pocket_comms.link_update()
|
||||
|
||||
-- update any tasks for the active page
|
||||
if (type(nav.tasks[nav.page]) == "table") then
|
||||
for i = 1, #nav.tasks[nav.page] do
|
||||
nav.tasks[nav.page][i]()
|
||||
end
|
||||
end
|
||||
local page_tasks = nav.get_current_page().tasks
|
||||
for i = 1, #page_tasks do page_tasks[i]() end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_wd.sv.is_timer(param1) then
|
||||
|
||||
@@ -1,58 +1,39 @@
|
||||
--
|
||||
-- Diagnostic Apps
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
|
||||
local App = require("graphics.elements.controls.app")
|
||||
local Checkbox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local SwitchButton = require("graphics.elements.controls.switch_button")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local NAV_PAGE = iocontrol.NAV_PAGE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new diagnostics page view
|
||||
-- create diagnostic app pages
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
local diag_home = Div{parent=main,x=1,y=1}
|
||||
|
||||
TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local alarm_test = Div{parent=main,x=1,y=1}
|
||||
|
||||
local panes = { diag_home, alarm_test }
|
||||
|
||||
local page_pane = MultiPane{parent=main,x=1,y=1,panes=panes}
|
||||
|
||||
local function navigate_diag()
|
||||
page_pane.set_value(1)
|
||||
db.nav.page = NAV_PAGE.DIAG
|
||||
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.DIAG
|
||||
end
|
||||
|
||||
local function navigate_alarm()
|
||||
page_pane.set_value(2)
|
||||
db.nav.page = NAV_PAGE.D_ALARMS
|
||||
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.D_ALARMS
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Alarm Testing Page --
|
||||
------------------------
|
||||
|
||||
db.nav.register_task(NAV_PAGE.D_ALARMS, db.diag.tone_test.get_tone_states)
|
||||
local alarm_test = Div{parent=root,x=1,y=1}
|
||||
|
||||
local alarm_app = db.nav.register_app(iocontrol.APP_ID.ALARMS, alarm_test)
|
||||
|
||||
local page = alarm_app.new_page(nil, function () end)
|
||||
page.tasks = { db.diag.tone_test.get_tone_states }
|
||||
|
||||
local ttest = db.diag.tone_test
|
||||
|
||||
@@ -67,8 +48,6 @@ local function new_view(root)
|
||||
|
||||
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
|
||||
|
||||
PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag}
|
||||
|
||||
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
TextBox{parent=tones,text="Tones",height=1,alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
|
||||
@@ -132,16 +111,6 @@ local function new_view(root)
|
||||
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
|
||||
|
||||
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
|
||||
|
||||
--------------
|
||||
-- App List --
|
||||
--------------
|
||||
|
||||
App{parent=diag_home,x=3,y=4,text="\x0f",title="Alarm",callback=navigate_alarm,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
App{parent=diag_home,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
|
||||
App{parent=diag_home,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
return create_pages
|
||||
24
pocket/ui/apps/dummy_app.lua
Normal file
24
pocket/ui/apps/dummy_app.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
--
|
||||
-- Placeholder App
|
||||
--
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- create placeholder app page
|
||||
---@param root graphics_element parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end)
|
||||
|
||||
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
|
||||
end
|
||||
|
||||
return create_pages
|
||||
102
pocket/ui/apps/sys_apps.lua
Normal file
102
pocket/ui/apps/sys_apps.lua
Normal file
@@ -0,0 +1,102 @@
|
||||
--
|
||||
-- System Apps
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local lockbox = require("lockbox")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- create system app pages
|
||||
---@param root graphics_element parent
|
||||
local function create_pages(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
----------------
|
||||
-- About Page --
|
||||
----------------
|
||||
|
||||
local about_root = Div{parent=root,x=1,y=1}
|
||||
|
||||
local about_app = db.nav.register_app(iocontrol.APP_ID.ABOUT, about_root)
|
||||
|
||||
local about_page = about_app.new_page(nil, 1)
|
||||
local fw_page = about_app.new_page(about_page, 2)
|
||||
local hw_page = about_app.new_page(about_page, 3)
|
||||
|
||||
local about = Div{parent=about_root,x=1,y=2}
|
||||
|
||||
TextBox{parent=about,y=1,text="System Information",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
local btn_fg_bg = cpair(colors.lightBlue, colors.black)
|
||||
local btn_active = cpair(colors.white, colors.black)
|
||||
local label = cpair(colors.lightGray, colors.black)
|
||||
|
||||
PushButton{parent=about,x=2,y=3,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to}
|
||||
PushButton{parent=about,x=2,y=4,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to}
|
||||
|
||||
local fw_div = Div{parent=about_root,x=1,y=2}
|
||||
TextBox{parent=fw_div,y=1,text="Firmware Versions",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
local fw_list_box = ListBox{parent=fw_div,x=1,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
|
||||
|
||||
local fw_list = Div{parent=fw_list_box,x=1,y=2,height=18}
|
||||
|
||||
TextBox{parent=fw_list,x=2,text="Pocket Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=db.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Comms Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="API Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=comms.api_version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Common Lib Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=util.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Graphics Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=core.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
fw_list.line_break()
|
||||
TextBox{parent=fw_list,x=2,text="Lockbox Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=fw_list,x=2,text=lockbox.version,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
local hw_div = Div{parent=about_root,x=1,y=2}
|
||||
TextBox{parent=hw_div,y=1,text="Host Versions",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Lua Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_VERSION,height=1,alignment=ALIGN.LEFT}
|
||||
|
||||
hw_div.line_break()
|
||||
TextBox{parent=hw_div,x=2,text="Environment",height=1,alignment=ALIGN.LEFT,fg_bg=label}
|
||||
TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT}
|
||||
|
||||
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,fw_div,hw_div}}
|
||||
|
||||
about_app.set_root_pane(root_pane)
|
||||
end
|
||||
|
||||
return create_pages
|
||||
@@ -4,27 +4,29 @@
|
||||
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
local diag_apps = require("pocket.ui.apps.diag_apps")
|
||||
local dummy_app = require("pocket.ui.apps.dummy_app")
|
||||
local sys_apps = require("pocket.ui.apps.sys_apps")
|
||||
|
||||
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||
|
||||
local boiler_page = require("pocket.ui.pages.boiler_page")
|
||||
local diag_page = require("pocket.ui.pages.diag_page")
|
||||
local home_page = require("pocket.ui.pages.home_page")
|
||||
local reactor_page = require("pocket.ui.pages.reactor_page")
|
||||
local turbine_page = require("pocket.ui.pages.turbine_page")
|
||||
local unit_page = require("pocket.ui.pages.unit_page")
|
||||
|
||||
local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local Sidebar = require("graphics.elements.controls.sidebar")
|
||||
|
||||
local SignalBar = require("graphics.elements.indicators.signal")
|
||||
|
||||
local LINK_STATE = iocontrol.LINK_STATE
|
||||
local NAV_PAGE = iocontrol.NAV_PAGE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
@@ -33,26 +35,27 @@ local cpair = core.cpair
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local nav = iocontrol.get_db().nav
|
||||
local ps = iocontrol.get_db().ps
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
-- window header message
|
||||
TextBox{parent=main,y=1,text="",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||
TextBox{parent=main,y=1,text="DEV ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
|
||||
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
|
||||
|
||||
--
|
||||
-- root panel panes (connection screens + main screen)
|
||||
--
|
||||
db.ps.subscribe("svr_conn_quality", svr_conn.set_value)
|
||||
db.ps.subscribe("crd_conn_quality", crd_conn.set_value)
|
||||
|
||||
--#region root panel panes (connection screens + main screen)
|
||||
|
||||
local root_pane_div = Div{parent=main,x=1,y=2}
|
||||
|
||||
local conn_sv_wait = conn_waiting(root_pane_div, 6, false)
|
||||
local conn_api_wait = conn_waiting(root_pane_div, 6, true)
|
||||
local main_pane = Div{parent=main,x=1,y=2}
|
||||
local root_panes = { conn_sv_wait, conn_api_wait, main_pane }
|
||||
|
||||
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
|
||||
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
|
||||
|
||||
root_pane.register(ps, "link_state", function (state)
|
||||
root_pane.register(db.ps, "link_state", function (state)
|
||||
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
|
||||
root_pane.set_value(1)
|
||||
elseif state == LINK_STATE.SV_LINK_ONLY then
|
||||
@@ -62,62 +65,33 @@ local function init(main)
|
||||
end
|
||||
end)
|
||||
|
||||
--
|
||||
-- main page panel panes & sidebar
|
||||
--
|
||||
--#endregion
|
||||
|
||||
--#region main page panel panes & sidebar
|
||||
|
||||
local page_div = Div{parent=main_pane,x=4,y=1}
|
||||
|
||||
local sidebar_tabs = {
|
||||
{
|
||||
char = "#",
|
||||
color = cpair(colors.black,colors.green)
|
||||
},
|
||||
{
|
||||
char = "U",
|
||||
color = cpair(colors.black,colors.yellow)
|
||||
},
|
||||
{
|
||||
char = "R",
|
||||
color = cpair(colors.black,colors.cyan)
|
||||
},
|
||||
{
|
||||
char = "B",
|
||||
color = cpair(colors.black,colors.lightGray)
|
||||
},
|
||||
{
|
||||
char = "T",
|
||||
color = cpair(colors.black,colors.white)
|
||||
},
|
||||
{
|
||||
char = "D",
|
||||
color = cpair(colors.black,colors.orange)
|
||||
}
|
||||
{ char = "#", color = cpair(colors.black, colors.green) }
|
||||
}
|
||||
|
||||
local panes = { home_page(page_div), unit_page(page_div), reactor_page(page_div), boiler_page(page_div), turbine_page(page_div), diag_page(page_div) }
|
||||
home_page(page_div)
|
||||
unit_page(page_div)
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
diag_apps(page_div)
|
||||
sys_apps(page_div)
|
||||
dummy_app(page_div)
|
||||
|
||||
local function navigate_sidebar(page)
|
||||
if page == 1 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.HOME]
|
||||
elseif page == 2 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.UNITS]
|
||||
elseif page == 3 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.REACTORS]
|
||||
elseif page == 4 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.BOILERS]
|
||||
elseif page == 5 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.TURBINES]
|
||||
elseif page == 6 then
|
||||
nav.page = nav.sub_pages[NAV_PAGE.DIAG]
|
||||
end
|
||||
assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
|
||||
|
||||
page_pane.set_value(page)
|
||||
end
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()}
|
||||
db.nav.set_pane(page_pane)
|
||||
|
||||
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar}
|
||||
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=db.nav.open_app}
|
||||
|
||||
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
|
||||
|
||||
--#endregion
|
||||
end
|
||||
|
||||
return init
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new boiler page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="BOILERS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,21 +1,59 @@
|
||||
local core = require("graphics.core")
|
||||
--
|
||||
-- Main Home Page
|
||||
--
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local App = require("graphics.elements.controls.app")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local AppMultiPane = require("graphics.elements.appmultipane")
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local App = require("graphics.elements.controls.app")
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local APP_ID = iocontrol.APP_ID
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new home page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
App{parent=main,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)}
|
||||
App{parent=main,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)}
|
||||
App{parent=main,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)}
|
||||
App{parent=main,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)}
|
||||
App{parent=main,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
|
||||
local main = Div{parent=root,x=1,y=1,height=19}
|
||||
|
||||
local app = db.nav.register_app(iocontrol.APP_ID.ROOT, main)
|
||||
|
||||
local apps_1 = Div{parent=main,x=1,y=1,height=15}
|
||||
local apps_2 = Div{parent=main,x=1,y=1,height=15}
|
||||
|
||||
local panes = { apps_1, apps_2 }
|
||||
|
||||
local app_pane = AppMultiPane{parent=main,x=1,y=1,height=18,panes=panes,active_color=colors.lightGray,nav_colors=cpair(colors.lightGray,colors.gray),scroll_nav=true,drag_nav=true,callback=app.switcher}
|
||||
|
||||
app.set_root_pane(app_pane)
|
||||
app.new_page(app.new_page(nil, 1), 2)
|
||||
|
||||
local function open(id) db.nav.open_app(id) end
|
||||
|
||||
local active_fg_bg = cpair(colors.white,colors.gray)
|
||||
|
||||
App{parent=apps_1,x=3,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=10,y=2,text="\x17",title="PRC",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=17,y=2,text="\x15",title="CTL",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=3,y=7,text="\x08",title="DEV",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=10,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=17,y=7,text="\xb6",title="Guide",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_1,x=3,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}
|
||||
|
||||
TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
App{parent=apps_2,x=3,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_2,x=10,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
|
||||
App{parent=apps_2,x=17,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new reactor page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="REACTOR",x=1,y=1,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,22 +0,0 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new turbine page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="TURBINES",x=1,y=1,height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -1,20 +1,29 @@
|
||||
-- local style = require("pocket.ui.style")
|
||||
--
|
||||
-- Unit Overview Page
|
||||
--
|
||||
|
||||
local core = require("graphics.core")
|
||||
local iocontrol = require("pocket.iocontrol")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
local core = require("graphics.core")
|
||||
|
||||
-- local cpair = core.cpair
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- new unit page view
|
||||
---@param root graphics_element parent
|
||||
local function new_view(root)
|
||||
local db = iocontrol.get_db()
|
||||
|
||||
local main = Div{parent=root,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="UNITS",x=1,y=1,height=1,alignment=ALIGN.CENTER}
|
||||
local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main)
|
||||
app.new_page(nil, function () end)
|
||||
|
||||
TextBox{parent=main,y=2,text="UNITS",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
TextBox{parent=main,y=4,text="work in progress",height=1,alignment=ALIGN.CENTER}
|
||||
|
||||
return main
|
||||
end
|
||||
|
||||
@@ -729,7 +729,7 @@ local function config_view(display)
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
tool_ctl.show_key_btn.enable()
|
||||
if cfg.AuthKey then tool_ctl.show_key_btn.enable() else tool_ctl.show_key_btn.disable() end
|
||||
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
@@ -740,7 +740,7 @@ local function config_view(display)
|
||||
local raw = cfg[f[1]]
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||
if f[1] == "AuthKey" and raw then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "EmerCoolColor" and raw ~= nil then val = rsio.color_name(raw)
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
|
||||
@@ -74,7 +74,7 @@ function plc.load_config()
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "v1.7.4"
|
||||
local R_PLC_VERSION = "v1.7.10"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
@@ -71,76 +71,49 @@ function threads.thread__main(smem, init)
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
-- core clock tick
|
||||
if networked then
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
-- start next clock timer
|
||||
loop_clock.start()
|
||||
|
||||
-- send updated data
|
||||
if nic.is_connected() then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
-- send updated data
|
||||
if networked and nic.is_connected() then
|
||||
if plc_comms.is_linked() then
|
||||
smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS)
|
||||
else
|
||||
if ticks_to_update == 0 then
|
||||
plc_comms.send_link_req()
|
||||
ticks_to_update = LINK_TICKS
|
||||
else
|
||||
if ticks_to_update == 0 then
|
||||
plc_comms.send_link_req()
|
||||
ticks_to_update = LINK_TICKS
|
||||
else
|
||||
ticks_to_update = ticks_to_update - 1
|
||||
end
|
||||
ticks_to_update = ticks_to_update - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- are we now formed after waiting to be formed?
|
||||
-- check for formed state change
|
||||
if (not plc_state.reactor_formed) and rps.is_formed() then
|
||||
-- push a connect event and unmount it from the PPM
|
||||
local iface = ppm.get_iface(plc_dev.reactor)
|
||||
if iface then
|
||||
log.info("unmounting and remounting unformed reactor")
|
||||
ppm.unmount(plc_dev.reactor)
|
||||
-- reactor now formed
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
local type, device = ppm.mount(iface)
|
||||
println_ts("reactor is now formed.")
|
||||
log.info("reactor is now formed")
|
||||
|
||||
if type == "fissionReactorLogicAdapter" and device ~= nil then
|
||||
-- reconnect reactor
|
||||
plc_dev.reactor = device
|
||||
-- SCRAM newly formed reactor
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
-- we need to assume formed here as we cannot check in this main loop
|
||||
-- RPS will identify if it isn't and this will get set false later
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
println_ts("reactor reconnected.")
|
||||
log.info("reactor reconnected")
|
||||
|
||||
-- SCRAM newly connected reactor
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or nic.is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
rps.reconnect_reactor(plc_dev.reactor)
|
||||
if networked then
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
rps.reset_formed()
|
||||
else
|
||||
-- fully lost the reactor now :(
|
||||
println_ts("reactor lost (failed reconnect)!")
|
||||
log.error("reactor lost (failed reconnect)")
|
||||
|
||||
plc_state.no_reactor = true
|
||||
plc_state.degraded = true
|
||||
end
|
||||
else
|
||||
log.error("failed to get interface of previously connected reactor", true)
|
||||
-- determine if we are still in a degraded state
|
||||
if (not networked) or nic.is_connected() then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
elseif not rps.is_formed() then
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
elseif plc_state.reactor_formed and not rps.is_formed() then
|
||||
-- reactor no longer formed
|
||||
println_ts("reactor is no longer formed.")
|
||||
log.info("reactor is no longer formed")
|
||||
|
||||
plc_state.reactor_formed = false
|
||||
plc_state.degraded = true
|
||||
end
|
||||
|
||||
-- update indicators
|
||||
@@ -230,7 +203,8 @@ function threads.thread__main(smem, init)
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
-- partial reset of RPS, specific to becoming formed/reconnected
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
end
|
||||
elseif networked and type == "modem" then
|
||||
@@ -368,9 +342,9 @@ function threads.thread__rps(smem)
|
||||
end
|
||||
end
|
||||
|
||||
-- if we are in standalone mode, continuously reset RPS
|
||||
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS
|
||||
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
|
||||
if not networked then rps.reset(true) end
|
||||
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
|
||||
|
||||
-- check safety (SCRAM occurs if tripped)
|
||||
if not plc_state.no_reactor then
|
||||
@@ -662,8 +636,9 @@ function threads.thread__setpoint_control(smem)
|
||||
if (type(cur_burn_rate) == "number") and (setpoints.burn_rate ~= cur_burn_rate) and rps.is_active() then
|
||||
last_burn_sp = setpoints.burn_rate
|
||||
|
||||
-- update without ramp if <= 2.5 mB/t change
|
||||
running = math.abs(setpoints.burn_rate - cur_burn_rate) > 2.5
|
||||
-- update without ramp if <= 2.5 mB/t increase
|
||||
-- no need to ramp down, as the ramp up poses the safety risks
|
||||
running = (setpoints.burn_rate - cur_burn_rate) > 2.5
|
||||
|
||||
if running then
|
||||
log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t"))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-- Configuration GUI
|
||||
--
|
||||
|
||||
local constants = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local rsio = require("scada-common.rsio")
|
||||
@@ -33,45 +34,50 @@ local tri = util.trinary
|
||||
local cpair = core.cpair
|
||||
|
||||
local IO = rsio.IO
|
||||
local IO_LVL = rsio.IO_LVL
|
||||
local IO_MODE = rsio.IO_MODE
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- rsio port descriptions
|
||||
local PORT_DESC = {
|
||||
"Facility SCRAM",
|
||||
"Facility Acknowledge",
|
||||
"Reactor SCRAM",
|
||||
"Reactor RPS Reset",
|
||||
"Reactor Enable",
|
||||
"Unit Acknowledge",
|
||||
"Facility Alarm (high prio)",
|
||||
"Facility Alarm (any)",
|
||||
"Waste Plutonium Valve",
|
||||
"Waste Polonium Valve",
|
||||
"Waste Po Pellets Valve",
|
||||
"Waste Antimatter Valve",
|
||||
"Reactor Active",
|
||||
"Reactor in Auto Control",
|
||||
"RPS Tripped",
|
||||
"RPS Auto SCRAM",
|
||||
"RPS High Damage",
|
||||
"RPS High Temperature",
|
||||
"RPS Low Coolant",
|
||||
"RPS Excess Heated Coolant",
|
||||
"RPS Excess Waste",
|
||||
"RPS Insufficient Fuel",
|
||||
"RPS PLC Fault",
|
||||
"RPS Supervisor Timeout",
|
||||
"Unit Alarm",
|
||||
"Unit Emergency Cool. Valve"
|
||||
local PORT_DESC_MAP = {
|
||||
{ IO.F_SCRAM, "Facility SCRAM" },
|
||||
{ IO.F_ACK, "Facility Acknowledge" },
|
||||
{ IO.R_SCRAM, "Reactor SCRAM" },
|
||||
{ IO.R_RESET, "Reactor RPS Reset" },
|
||||
{ IO.R_ENABLE, "Reactor Enable" },
|
||||
{ IO.U_ACK, "Unit Acknowledge" },
|
||||
{ IO.F_ALARM, "Facility Alarm (high prio)" },
|
||||
{ IO.F_ALARM_ANY, "Facility Alarm (any)" },
|
||||
{ IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" },
|
||||
{ IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" },
|
||||
{ IO.F_MATRIX_CHG, "Induction Matrix Charge %" },
|
||||
{ IO.WASTE_PU, "Waste Plutonium Valve" },
|
||||
{ IO.WASTE_PO, "Waste Polonium Valve" },
|
||||
{ IO.WASTE_POPL, "Waste Po Pellets Valve" },
|
||||
{ IO.WASTE_AM, "Waste Antimatter Valve" },
|
||||
{ IO.R_ACTIVE, "Reactor Active" },
|
||||
{ IO.R_AUTO_CTRL, "Reactor in Auto Control" },
|
||||
{ IO.R_SCRAMMED, "RPS Tripped" },
|
||||
{ IO.R_AUTO_SCRAM, "RPS Auto SCRAM" },
|
||||
{ IO.R_HIGH_DMG, "RPS High Damage" },
|
||||
{ IO.R_HIGH_TEMP, "RPS High Temperature" },
|
||||
{ IO.R_LOW_COOLANT, "RPS Low Coolant" },
|
||||
{ IO.R_EXCESS_HC, "RPS Excess Heated Coolant" },
|
||||
{ IO.R_EXCESS_WS, "RPS Excess Waste" },
|
||||
{ IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" },
|
||||
{ IO.R_PLC_FAULT, "RPS PLC Fault" },
|
||||
{ IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" },
|
||||
{ IO.U_ALARM, "Unit Alarm" },
|
||||
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" }
|
||||
}
|
||||
|
||||
-- designation (0 = facility, 1 = unit)
|
||||
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
|
||||
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }
|
||||
|
||||
assert(#PORT_DESC == rsio.NUM_PORTS)
|
||||
assert(#PORT_DESC_MAP == rsio.NUM_PORTS)
|
||||
assert(#PORT_DSGN == rsio.NUM_PORTS)
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
@@ -442,7 +448,7 @@ local function config_view(display)
|
||||
TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"}
|
||||
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||
local function censor_key(enable) censor(tri(enable, "*", nil)) end
|
||||
|
||||
local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
@@ -555,7 +561,7 @@ local function config_view(display)
|
||||
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local function back_from_colors()
|
||||
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
|
||||
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4))
|
||||
tool_ctl.jumped_to_color = false
|
||||
recolor(1)
|
||||
end
|
||||
@@ -897,7 +903,7 @@ local function config_view(display)
|
||||
tool_ctl.p_desc.reposition(1, 8)
|
||||
tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.")
|
||||
elseif type == "inductionPort" or type == "spsPort" then
|
||||
local dev = util.trinary(type == "inductionPort", "induction matrix", "SPS")
|
||||
local dev = tri(type == "inductionPort", "induction matrix", "SPS")
|
||||
tool_ctl.p_idx.hide(true)
|
||||
tool_ctl.p_unit.hide(true)
|
||||
tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.")
|
||||
@@ -923,7 +929,7 @@ local function config_view(display)
|
||||
tool_ctl.ppm_devs.remove_all()
|
||||
for name, entry in pairs(mounts) do
|
||||
if util.table_contains(RTU_DEV_TYPES, entry.type) then
|
||||
local bkg = util.trinary(alternate, colors.white, colors.lightGray)
|
||||
local bkg = tri(alternate, colors.white, colors.lightGray)
|
||||
|
||||
---@cast entry ppm_entry
|
||||
local line = Div{parent=tool_ctl.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)}
|
||||
@@ -1085,8 +1091,9 @@ local function config_view(display)
|
||||
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
|
||||
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
|
||||
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
|
||||
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
|
||||
|
||||
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6}}
|
||||
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}}
|
||||
|
||||
TextBox{parent=rs_cfg,x=1,y=2,height=1,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
|
||||
|
||||
@@ -1143,9 +1150,23 @@ local function config_view(display)
|
||||
text = "You selected the ALL_WASTE shortcut."
|
||||
else
|
||||
tool_ctl.rs_cfg_shortcut.hide(true)
|
||||
tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
|
||||
tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
|
||||
tool_ctl.rs_cfg_color.show()
|
||||
text = "You selected " .. rsio.to_string(port) .. " (for "
|
||||
|
||||
local io_type = "analog input "
|
||||
local io_mode = rsio.get_io_mode(port)
|
||||
local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "")
|
||||
|
||||
if io_mode == IO_MODE.DIGITAL_IN then
|
||||
io_type = inv .. "digital input "
|
||||
elseif io_mode == IO_MODE.DIGITAL_OUT then
|
||||
io_type = inv .. "digital output "
|
||||
elseif io_mode == IO_MODE.ANALOG_OUT then
|
||||
io_type = "analog output "
|
||||
end
|
||||
|
||||
text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for "
|
||||
|
||||
if PORT_DSGN[port] == 1 then
|
||||
text = text .. "a unit)."
|
||||
tool_ctl.rs_cfg_unit_l.show()
|
||||
@@ -1167,25 +1188,35 @@ local function config_view(display)
|
||||
PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
TextBox{parent=all_w_macro,x=16,y=1,width=5,height=1,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=all_w_macro,x=22,y=1,height=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)}
|
||||
|
||||
for i = 1, rsio.NUM_PORTS do
|
||||
local name = rsio.to_string(i)
|
||||
local io_dir = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, "[in]", "[out]")
|
||||
local btn_color = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue)
|
||||
local p = PORT_DESC_MAP[i][1]
|
||||
local name = rsio.to_string(p)
|
||||
local io_dir = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]")
|
||||
local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue)
|
||||
|
||||
local entry = Div{parent=rs_ports,height=1}
|
||||
PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(i)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
|
||||
TextBox{parent=entry,x=16,y=1,width=5,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
|
||||
TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC[i],fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
|
||||
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=1,text=""}
|
||||
tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""}
|
||||
|
||||
tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=27,y=3,width=7,height=1,text="Unit ID"}
|
||||
tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=27,y=4,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=3,width=11,height=1,text="Output Side"}
|
||||
local side = Radio2D{parent=rs_c_3,x=1,y=4,rows=2,columns=3,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
|
||||
TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
|
||||
TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
|
||||
TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
|
||||
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,height=1,text="Output Side"}
|
||||
local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
|
||||
|
||||
tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,height=1,text="Unit ID"}
|
||||
tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
|
||||
|
||||
local function set_bundled(bundled)
|
||||
if bundled then tool_ctl.rs_cfg_color.enable() else tool_ctl.rs_cfg_color.disable() end
|
||||
@@ -1216,10 +1247,10 @@ local function config_view(display)
|
||||
if port >= 0 then
|
||||
---@type rtu_rs_definition
|
||||
local def = {
|
||||
unit = util.trinary(PORT_DSGN[port] == 1, u, nil),
|
||||
unit = tri(PORT_DSGN[port] == 1, u, nil),
|
||||
port = port,
|
||||
side = side_options_map[side.get_value()],
|
||||
color = util.trinary(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil)
|
||||
color = tri(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil)
|
||||
}
|
||||
|
||||
if tool_ctl.rs_cfg_editing == false then
|
||||
@@ -1233,10 +1264,10 @@ local function config_view(display)
|
||||
local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime }
|
||||
for i = 0, 3 do
|
||||
table.insert(tmp_cfg.Redstone, {
|
||||
unit = util.trinary(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
|
||||
unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
|
||||
port = IO.WASTE_PU + i,
|
||||
side = util.trinary(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
|
||||
color = util.trinary(bundled.get_value(), default_colors[i + 1], nil)
|
||||
side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
|
||||
color = tri(bundled.get_value(), default_colors[i + 1], nil)
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -1289,7 +1320,7 @@ local function config_view(display)
|
||||
peri_import_list.remove_all()
|
||||
for _, entry in ipairs(config.RTU_DEVICES) do
|
||||
local for_facility = entry.for_reactor == 0
|
||||
local ini_unit = util.trinary(for_facility, nil, entry.for_reactor)
|
||||
local ini_unit = tri(for_facility, nil, entry.for_reactor)
|
||||
|
||||
local def = { name = entry.name, unit = ini_unit, index = entry.index }
|
||||
local mount = mounts[def.name] ---@type ppm_entry|nil
|
||||
@@ -1368,7 +1399,7 @@ local function config_view(display)
|
||||
table.insert(tmp_cfg.Redstone, def)
|
||||
|
||||
local name = rsio.to_string(def.port)
|
||||
local io_dir = util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
|
||||
local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
|
||||
local conn = def.side
|
||||
local unit = "facility"
|
||||
|
||||
@@ -1431,7 +1462,7 @@ local function config_view(display)
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
|
||||
elseif f[1] == "FrontPanelTheme" then
|
||||
val = util.strval(themes.fp_theme_name(raw))
|
||||
elseif f[1] == "ColorMode" then
|
||||
@@ -1440,7 +1471,7 @@ local function config_view(display)
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
@@ -1554,7 +1585,7 @@ local function config_view(display)
|
||||
end
|
||||
|
||||
tool_ctl.rs_cfg_selection.set_value(text)
|
||||
tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
|
||||
tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
|
||||
side.set_value(side_to_idx(def.side))
|
||||
bundled.set_value(def.color ~= nil)
|
||||
tool_ctl.rs_cfg_color.set_value(value)
|
||||
@@ -1575,7 +1606,7 @@ local function config_view(display)
|
||||
local def = cfg.Redstone[i] ---@type rtu_rs_definition
|
||||
|
||||
local name = rsio.to_string(def.port)
|
||||
local io_dir = util.trinary(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
|
||||
local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
|
||||
local conn = def.side
|
||||
local unit = util.strval(def.unit or "F")
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ function rtu.load_config()
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
|
||||
@@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
|
||||
local sps_rtu = require("rtu.dev.sps_rtu")
|
||||
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
|
||||
|
||||
local RTU_VERSION = "v1.9.3"
|
||||
local RTU_VERSION = "v1.9.5"
|
||||
|
||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||
|
||||
@@ -16,8 +16,9 @@ local max_distance = nil
|
||||
---@class comms
|
||||
local comms = {}
|
||||
|
||||
-- protocol/data version (protocol/data independent changes tracked by util.lua version)
|
||||
comms.version = "2.4.5"
|
||||
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
|
||||
comms.version = "2.5.1"
|
||||
comms.api_version = "0.0.1"
|
||||
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
@@ -64,7 +65,9 @@ local CRDN_TYPE = {
|
||||
FAC_CMD = 3, -- faility command
|
||||
UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
|
||||
UNIT_STATUSES = 5, -- state of each of the reactor units
|
||||
UNIT_CMD = 6 -- command a reactor unit
|
||||
UNIT_CMD = 6, -- command a reactor unit
|
||||
API_GET_FAC = 7, -- API: get all the facility data
|
||||
API_GET_UNITS = 8 -- API: get all the reactor unit data
|
||||
}
|
||||
|
||||
---@enum ESTABLISH_ACK
|
||||
@@ -72,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
|
||||
@@ -93,7 +97,8 @@ local FAC_COMMAND = {
|
||||
START = 2, -- start automatic process control
|
||||
ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
|
||||
SET_WASTE_MODE = 4, -- set automatic waste processing mode
|
||||
SET_PU_FB = 5 -- set plutonium fallback mode
|
||||
SET_PU_FB = 5, -- set plutonium fallback mode
|
||||
SET_SPS_LP = 6 -- set SPS at low power mode
|
||||
}
|
||||
|
||||
---@enum UNIT_COMMAND
|
||||
|
||||
@@ -29,7 +29,7 @@ local annunc = {}
|
||||
annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s
|
||||
annunc.RCSFlowLow_NA = -2.0 -- flow < -2.0 mB/s
|
||||
annunc.CoolantLevelLow = 0.4 -- fill < 40%
|
||||
annunc.ReactorTempHigh = 1000 -- temp > 1000K
|
||||
annunc.OpTempTolerance = 5 -- high temp if >= operational temp + X
|
||||
annunc.ReactorHighDeltaT = 50 -- rate > 50K/s
|
||||
annunc.FuelLevelLow = 0.05 -- fill <= 5%
|
||||
annunc.WasteLevelHigh = 0.80 -- fill >= 80%
|
||||
@@ -66,21 +66,48 @@ constants.ALARM_LIMITS = alarms
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Supervisor Redstone Activation Thresholds
|
||||
|
||||
---@class _rs_threshold_constants
|
||||
local rs = {}
|
||||
|
||||
rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW
|
||||
rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH
|
||||
|
||||
constants.RS_THRESHOLDS = rs
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Supervisor Constants
|
||||
|
||||
-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks
|
||||
constants.FLOW_STABILITY_DELAY_MS = 15000
|
||||
-- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks
|
||||
constants.FLOW_STABILITY_DELAY_MS = 10000
|
||||
|
||||
-- Notes on Radiation
|
||||
-- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
|
||||
-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
|
||||
-- - damaging radiation 0.00006 Sv/h (60 uSv/h)
|
||||
constants.LOW_RADIATION = 0.00001
|
||||
constants.HAZARD_RADIATION = 0.00006
|
||||
constants.HIGH_RADIATION = 0.001
|
||||
constants.LOW_RADIATION = 0.00001
|
||||
constants.HAZARD_RADIATION = 0.00006
|
||||
constants.HIGH_RADIATION = 0.001
|
||||
constants.VERY_HIGH_RADIATION = 0.1
|
||||
constants.SEVERE_RADIATION = 8.0
|
||||
constants.EXTREME_RADIATION = 100.0
|
||||
constants.SEVERE_RADIATION = 8.0
|
||||
constants.EXTREME_RADIATION = 100.0
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Mekanism Configuration Constants
|
||||
|
||||
---@class _mek_constants
|
||||
local mek = {}
|
||||
|
||||
mek.BASE_BOIL_TEMP = 373.15 -- mekanism: HeatUtils.BASE_BOIL_TEMP
|
||||
mek.JOULES_PER_MB = 1000000 -- mekanism: energyPerFissionFuel
|
||||
mek.TURBINE_GAS_PER_TANK = 64000 -- mekanism: turbineGasPerTank
|
||||
mek.TURBINE_DISPERSER_FLOW = 1280 -- mekanism: turbineDisperserGasFlow
|
||||
mek.TURBINE_VENT_FLOW = 32000 -- mekanism: turbineVentGasFlow
|
||||
|
||||
constants.mek = mek
|
||||
|
||||
--#endregion
|
||||
|
||||
|
||||
@@ -141,7 +141,9 @@ local function peri_init(iface)
|
||||
local funcs = peripheral.wrap(iface)
|
||||
if (type(funcs) == "table") and (type(funcs[key]) == "function") then
|
||||
-- add this function then return it
|
||||
self.fault_counts[key] = 0
|
||||
self.device[key] = protect_peri_function(key, funcs[key])
|
||||
|
||||
log.info(util.c("PPM: [@", iface, "] initialized previously undefined field ", key, "()"))
|
||||
|
||||
return self.device[key]
|
||||
|
||||
@@ -52,6 +52,8 @@ local IO_PORT = {
|
||||
-- facility
|
||||
F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm)
|
||||
F_ALARM_ANY = 8, -- active high, any alarm regardless of priority
|
||||
F_MATRIX_LOW = 27, -- active high, induction matrix charge low
|
||||
F_MATRIX_HIGH = 28, -- active high, induction matrix charge high
|
||||
|
||||
-- waste
|
||||
WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route
|
||||
@@ -75,17 +77,27 @@ local IO_PORT = {
|
||||
|
||||
-- unit outputs
|
||||
U_ALARM = 25, -- active high, unit alarm
|
||||
U_EMER_COOL = 26 -- active low, emergency coolant control
|
||||
U_EMER_COOL = 26, -- active low, emergency coolant control
|
||||
|
||||
-- analog outputs --
|
||||
|
||||
-- facility
|
||||
F_MATRIX_CHG = 29 -- analog charge level of the induction matrix
|
||||
}
|
||||
|
||||
rsio.IO_LVL = IO_LVL
|
||||
rsio.IO_DIR = IO_DIR
|
||||
rsio.IO_MODE = IO_MODE
|
||||
rsio.IO = IO_PORT
|
||||
rsio.NUM_PORTS = IO_PORT.U_EMER_COOL
|
||||
|
||||
rsio.NUM_PORTS = 29
|
||||
rsio.NUM_DIG_PORTS = 28
|
||||
rsio.NUM_ANA_PORTS = 1
|
||||
|
||||
-- self checks
|
||||
|
||||
assert(rsio.NUM_PORTS == (rsio.NUM_DIG_PORTS + rsio.NUM_ANA_PORTS), "port counts inconsistent")
|
||||
|
||||
local dup_chk = {}
|
||||
for _, v in pairs(IO_PORT) do
|
||||
assert(dup_chk[v] ~= true, "duplicate in port list")
|
||||
@@ -96,64 +108,45 @@ assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Utility Functions
|
||||
--#region Utility Functions and Attribute Tables
|
||||
|
||||
local PORT_NAMES = {
|
||||
"F_SCRAM",
|
||||
"F_ACK",
|
||||
"R_SCRAM",
|
||||
"R_RESET",
|
||||
"R_ENABLE",
|
||||
"U_ACK",
|
||||
"F_ALARM",
|
||||
"F_ALARM_ANY",
|
||||
"WASTE_PU",
|
||||
"WASTE_PO",
|
||||
"WASTE_POPL",
|
||||
"WASTE_AM",
|
||||
"R_ACTIVE",
|
||||
"R_AUTO_CTRL",
|
||||
"R_SCRAMMED",
|
||||
"R_AUTO_SCRAM",
|
||||
"R_HIGH_DMG",
|
||||
"R_HIGH_TEMP",
|
||||
"R_LOW_COOLANT",
|
||||
"R_EXCESS_HC",
|
||||
"R_EXCESS_WS",
|
||||
"R_INSUFF_FUEL",
|
||||
"R_PLC_FAULT",
|
||||
"R_PLC_TIMEOUT",
|
||||
"U_ALARM",
|
||||
"U_EMER_COOL"
|
||||
}
|
||||
local IO = IO_PORT
|
||||
|
||||
-- list of all port names
|
||||
local PORT_NAMES = {}
|
||||
for k, v in pairs(IO) do PORT_NAMES[v] = k end
|
||||
|
||||
-- list of all port I/O modes
|
||||
local MODES = {
|
||||
IO_MODE.DIGITAL_IN, -- F_SCRAM
|
||||
IO_MODE.DIGITAL_IN, -- F_ACK
|
||||
IO_MODE.DIGITAL_IN, -- R_SCRAM
|
||||
IO_MODE.DIGITAL_IN, -- R_RESET
|
||||
IO_MODE.DIGITAL_IN, -- R_ENABLE
|
||||
IO_MODE.DIGITAL_IN, -- U_ACK
|
||||
IO_MODE.DIGITAL_OUT, -- F_ALARM
|
||||
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_PU
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_PO
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
|
||||
IO_MODE.DIGITAL_OUT, -- WASTE_AM
|
||||
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
|
||||
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
|
||||
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
|
||||
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
|
||||
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
|
||||
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
|
||||
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
|
||||
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
|
||||
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
|
||||
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
|
||||
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
|
||||
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
|
||||
IO_MODE.DIGITAL_OUT, -- U_ALARM
|
||||
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
|
||||
[IO.F_SCRAM] = IO_MODE.DIGITAL_IN,
|
||||
[IO.F_ACK] = IO_MODE.DIGITAL_IN,
|
||||
[IO.R_SCRAM] = IO_MODE.DIGITAL_IN,
|
||||
[IO.R_RESET] = IO_MODE.DIGITAL_IN,
|
||||
[IO.R_ENABLE] = IO_MODE.DIGITAL_IN,
|
||||
[IO.U_ACK] = IO_MODE.DIGITAL_IN,
|
||||
[IO.F_ALARM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_ALARM_ANY] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_MATRIX_LOW] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_MATRIX_HIGH] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_PU] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_PO] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_POPL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.WASTE_AM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_ACTIVE] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_AUTO_CTRL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_SCRAMMED] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_AUTO_SCRAM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_HIGH_DMG] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_HIGH_TEMP] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_LOW_COOLANT] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_EXCESS_HC] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_EXCESS_WS] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_INSUFF_FUEL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_PLC_FAULT] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
|
||||
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
|
||||
}
|
||||
|
||||
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
|
||||
@@ -179,74 +172,51 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur
|
||||
|
||||
-- I/O mappings to I/O function and I/O mode
|
||||
local RS_DIO_MAP = {
|
||||
-- F_SCRAM
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
-- F_ACK
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.F_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
[IO.F_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
|
||||
-- R_SCRAM
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
-- R_RESET
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
-- R_ENABLE
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.R_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
|
||||
[IO.R_RESET] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.R_ENABLE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
|
||||
-- U_ACK
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
[IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
|
||||
|
||||
-- F_ALARM
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- F_ALARM_ANY
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_ALARM_ANY] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_MATRIX_LOW] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.F_MATRIX_HIGH] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
|
||||
-- WASTE_PU
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
-- WASTE_PO
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
-- WASTE_POPL
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
-- WASTE_AM
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_PU] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_PO] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_POPL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
[IO.WASTE_AM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
|
||||
|
||||
-- R_ACTIVE
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_AUTO_CTRL
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_SCRAMMED
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_AUTO_SCRAM
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_HIGH_DMG
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_HIGH_TEMP
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_LOW_COOLANT
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_EXCESS_HC
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_EXCESS_WS
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_INSUFF_FUEL
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_PLC_FAULT
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- R_PLC_TIMEOUT
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_ACTIVE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_AUTO_CTRL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_SCRAMMED] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_AUTO_SCRAM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_HIGH_DMG] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_HIGH_TEMP] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_LOW_COOLANT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_EXCESS_HC] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_EXCESS_WS] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_INSUFF_FUEL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_PLC_FAULT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
|
||||
-- U_ALARM
|
||||
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
-- U_EMER_COOL
|
||||
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
|
||||
[IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
|
||||
[IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
|
||||
}
|
||||
|
||||
assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
|
||||
assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
|
||||
|
||||
-- get the I/O direction of a port
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
---@return IO_DIR
|
||||
function rsio.get_io_dir(port)
|
||||
if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode
|
||||
if rsio.is_valid_port(port) then
|
||||
return util.trinary(MODES[port] == IO_MODE.DIGITAL_OUT or MODES[port] == IO_MODE.ANALOG_OUT, IO_DIR.OUT, IO_DIR.IN)
|
||||
else return IO_DIR.IN end
|
||||
end
|
||||
|
||||
@@ -310,6 +280,13 @@ end
|
||||
|
||||
--#region Digital I/O
|
||||
|
||||
-- check if a port is digital
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
function rsio.is_digital(port)
|
||||
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.DIGITAL_IN or MODES[port] == IO_MODE.DIGITAL_OUT)
|
||||
end
|
||||
|
||||
-- get digital I/O level reading from a redstone boolean input value
|
||||
---@nodiscard
|
||||
---@param rs_value boolean raw value from redstone
|
||||
@@ -330,7 +307,7 @@ function rsio.digital_write(level) return level == IO_LVL.HIGH end
|
||||
---@param active boolean state to convert to logic level
|
||||
---@return IO_LVL|false
|
||||
function rsio.digital_write_active(port, active)
|
||||
if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then
|
||||
if not rsio.is_digital(port) then
|
||||
return false
|
||||
else
|
||||
return RS_DIO_MAP[port]._out(active)
|
||||
@@ -343,9 +320,7 @@ end
|
||||
---@param level IO_LVL logic level
|
||||
---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided
|
||||
function rsio.digital_is_active(port, level)
|
||||
if not util.is_int(port) then
|
||||
return nil
|
||||
elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
|
||||
if (not rsio.is_digital(port)) or level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
|
||||
return nil
|
||||
else
|
||||
return RS_DIO_MAP[port]._in(level)
|
||||
@@ -356,6 +331,13 @@ end
|
||||
|
||||
--#region Analog I/O
|
||||
|
||||
-- check if a port is analog
|
||||
---@nodiscard
|
||||
---@param port IO_PORT
|
||||
function rsio.is_analog(port)
|
||||
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.ANALOG_IN or MODES[port] == IO_MODE.ANALOG_OUT)
|
||||
end
|
||||
|
||||
-- read an analog value scaled from min to max
|
||||
---@nodiscard
|
||||
---@param rs_value number redstone reading (0 to 15)
|
||||
@@ -372,7 +354,7 @@ end
|
||||
---@param value number value to write (from min to max range)
|
||||
---@param min number minimum of range
|
||||
---@param max number maximum of range
|
||||
---@return number rs_value scaled redstone reading (0 to 15)
|
||||
---@return integer rs_value scaled redstone reading (0 to 15)
|
||||
function rsio.analog_write(value, min, max)
|
||||
local scaled_value = (value - min) / (max - min)
|
||||
return math.floor(scaled_value * 15)
|
||||
|
||||
@@ -22,7 +22,7 @@ local t_pack = table.pack
|
||||
local util = {}
|
||||
|
||||
-- scada-common version
|
||||
util.version = "1.2.0"
|
||||
util.version = "1.3.0"
|
||||
|
||||
util.TICK_TIME_S = 0.05
|
||||
util.TICK_TIME_MS = 50
|
||||
@@ -181,8 +181,7 @@ function util.round(x) return math.floor(x + 0.5) end
|
||||
-- get a new moving average object
|
||||
---@nodiscard
|
||||
---@param length integer history length
|
||||
---@param default number value to fill history with for first call to compute()
|
||||
function util.mov_avg(length, default)
|
||||
function util.mov_avg(length)
|
||||
local data = {}
|
||||
local index = 1
|
||||
local last_t = 0 ---@type number|nil
|
||||
@@ -190,11 +189,15 @@ function util.mov_avg(length, default)
|
||||
---@class moving_average
|
||||
local public = {}
|
||||
|
||||
-- reset all to a given value
|
||||
---@param x number value
|
||||
-- reset all to a given value, or clear all data if no value is given
|
||||
---@param x number? value
|
||||
function public.reset(x)
|
||||
index = 1
|
||||
data = {}
|
||||
for _ = 1, length do t_insert(data, x) end
|
||||
|
||||
if x then
|
||||
for _ = 1, length do t_insert(data, x) end
|
||||
end
|
||||
end
|
||||
|
||||
-- record a new value
|
||||
@@ -214,12 +217,15 @@ function util.mov_avg(length, default)
|
||||
---@nodiscard
|
||||
---@return number average
|
||||
function public.compute()
|
||||
local sum = 0
|
||||
for i = 1, length do sum = sum + data[i] end
|
||||
return sum / length
|
||||
end
|
||||
if #data == 0 then return 0 end
|
||||
|
||||
public.reset(default)
|
||||
local sum = 0
|
||||
for i = 1, #data do
|
||||
sum = sum + data[i]
|
||||
end
|
||||
|
||||
return sum / #data
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
@@ -91,6 +91,7 @@ local tmp_cfg = {
|
||||
CoolingConfig = {},
|
||||
FacilityTankMode = 0,
|
||||
FacilityTankDefs = {},
|
||||
ExtChargeIdling = false,
|
||||
SVR_Channel = nil, ---@type integer
|
||||
PLC_Channel = nil, ---@type integer
|
||||
RTU_Channel = nil, ---@type integer
|
||||
@@ -120,6 +121,7 @@ local fields = {
|
||||
{ "CoolingConfig", "Cooling Configuration", {} },
|
||||
{ "FacilityTankMode", "Facility Tank Mode", 0 },
|
||||
{ "FacilityTankDefs", "Facility Tank Definitions", {} },
|
||||
{ "ExtChargeIdling", "Extended Charge Idling", false },
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "PLC_Channel", "PLC Channel", 16241 },
|
||||
{ "RTU_Channel", "RTU Channel", 16242 },
|
||||
@@ -222,8 +224,9 @@ local function config_view(display)
|
||||
local svr_c_4 = Div{parent=svr_cfg,x=2,y=4,width=49}
|
||||
local svr_c_5 = Div{parent=svr_cfg,x=2,y=4,width=49}
|
||||
local svr_c_6 = Div{parent=svr_cfg,x=2,y=4,width=49}
|
||||
local svr_c_7 = Div{parent=svr_cfg,x=2,y=4,width=49}
|
||||
|
||||
local svr_pane = MultiPane{parent=svr_cfg,x=1,y=4,panes={svr_c_1,svr_c_2,svr_c_3,svr_c_4,svr_c_5,svr_c_6}}
|
||||
local svr_pane = MultiPane{parent=svr_cfg,x=1,y=4,panes={svr_c_1,svr_c_2,svr_c_3,svr_c_4,svr_c_5,svr_c_6,svr_c_7}}
|
||||
|
||||
TextBox{parent=svr_cfg,x=1,y=2,height=1,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
|
||||
|
||||
@@ -329,7 +332,7 @@ local function config_view(display)
|
||||
else
|
||||
tmp_cfg.FacilityTankMode = 0
|
||||
tmp_cfg.FacilityTankDefs = {}
|
||||
main_pane.set_value(3)
|
||||
svr_pane.set_value(7)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -563,7 +566,7 @@ local function config_view(display)
|
||||
|
||||
local function submit_mode()
|
||||
tmp_cfg.FacilityTankMode = tank_mode.get_value()
|
||||
main_pane.set_value(3)
|
||||
svr_pane.set_value(7)
|
||||
end
|
||||
|
||||
PushButton{parent=svr_c_5,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
@@ -577,6 +580,23 @@ local function config_view(display)
|
||||
|
||||
PushButton{parent=svr_c_6,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=svr_c_7,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."}
|
||||
TextBox{parent=svr_c_7,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."}
|
||||
|
||||
local ext_idling = CheckBox{parent=svr_c_7,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
|
||||
|
||||
local function back_from_idling()
|
||||
svr_pane.set_value(util.trinary(tmp_cfg.FacilityTankMode == 0, 3, 5))
|
||||
end
|
||||
|
||||
local function submit_idling()
|
||||
tmp_cfg.ExtChargeIdling = ext_idling.get_value()
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
|
||||
PushButton{parent=svr_c_7,x=1,y=14,text="\x1b Back",callback=back_from_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=svr_c_7,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Network
|
||||
|
||||
@@ -23,7 +23,7 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
|
||||
local IO = rsio.IO
|
||||
local IO = rsio.IO
|
||||
|
||||
local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA
|
||||
|
||||
@@ -50,9 +50,9 @@ local START_STATUS = {
|
||||
BLADE_MISMATCH = 2
|
||||
}
|
||||
|
||||
local charge_Kp = 0.275
|
||||
local charge_Kp = 0.15
|
||||
local charge_Ki = 0.0
|
||||
local charge_Kd = 4.5
|
||||
local charge_Kd = 0.6
|
||||
|
||||
local rate_Kp = 2.45
|
||||
local rate_Ki = 0.4825
|
||||
@@ -63,9 +63,9 @@ local facility = {}
|
||||
|
||||
-- create a new facility management object
|
||||
---@nodiscard
|
||||
---@param num_reactors integer number of reactor units
|
||||
---@param config svr_config supervisor configuration
|
||||
---@param cooling_conf sv_cooling_conf cooling configurations of reactor units
|
||||
function facility.new(num_reactors, cooling_conf)
|
||||
function facility.new(config, cooling_conf)
|
||||
local self = {
|
||||
units = {},
|
||||
status_text = { "START UP", "initializing..." },
|
||||
@@ -120,6 +120,8 @@ function facility.new(num_reactors, cooling_conf)
|
||||
waste_product = WASTE.PLUTONIUM,
|
||||
current_waste_product = WASTE.PLUTONIUM,
|
||||
pu_fallback = false,
|
||||
sps_low_power = false,
|
||||
disabled_sps = false,
|
||||
-- alarm tones
|
||||
tone_states = {},
|
||||
test_tone_set = false,
|
||||
@@ -128,14 +130,21 @@ function facility.new(num_reactors, cooling_conf)
|
||||
test_alarm_states = {},
|
||||
-- statistics
|
||||
im_stat_init = false,
|
||||
avg_charge = util.mov_avg(3, 0.0),
|
||||
avg_inflow = util.mov_avg(6, 0.0),
|
||||
avg_outflow = util.mov_avg(6, 0.0)
|
||||
avg_charge = util.mov_avg(3), -- 3 seconds
|
||||
avg_inflow = util.mov_avg(6), -- 3 seconds
|
||||
avg_outflow = util.mov_avg(6), -- 3 seconds
|
||||
-- induction matrix charge delta stats
|
||||
avg_net = util.mov_avg(60), -- 60 seconds
|
||||
imtx_last_capacity = 0,
|
||||
imtx_last_charge = 0,
|
||||
imtx_last_charge_t = 0,
|
||||
-- track faulted induction matrix update times to reject
|
||||
imtx_faulted_times = { 0, 0, 0 }
|
||||
}
|
||||
|
||||
-- create units
|
||||
for i = 1, num_reactors do
|
||||
table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount))
|
||||
for i = 1, config.UnitCount do
|
||||
table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling))
|
||||
table.insert(self.group_map, 0)
|
||||
end
|
||||
|
||||
@@ -225,6 +234,14 @@ function facility.new(num_reactors, cooling_conf)
|
||||
return unallocated, false
|
||||
end
|
||||
|
||||
-- set idle state of all assigned reactors
|
||||
---@param idle boolean idle state
|
||||
local function _set_idling(idle)
|
||||
for i = 1, #self.prio_defs do
|
||||
for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end
|
||||
end
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
---@class facility
|
||||
@@ -292,23 +309,68 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
-- calculate moving averages for induction matrix
|
||||
if self.induction[1] ~= nil then
|
||||
local matrix = self.induction[1] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
local matrix = self.induction[1] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
|
||||
charge_update = db.tanks.last_update
|
||||
local build_update = db.build.last_update
|
||||
rate_update = db.state.last_update
|
||||
charge_update = db.tanks.last_update
|
||||
|
||||
local has_data = build_update > 0 and rate_update > 0 and charge_update > 0
|
||||
|
||||
if matrix.is_faulted() then
|
||||
-- a fault occured, cannot reliably update stats
|
||||
has_data = false
|
||||
self.im_stat_init = false
|
||||
self.imtx_faulted_times = { build_update, rate_update, charge_update }
|
||||
elseif not self.im_stat_init then
|
||||
-- prevent operation with partially invalid data
|
||||
-- all fields must have updated since the last fault
|
||||
has_data = self.imtx_faulted_times[1] < build_update and
|
||||
self.imtx_faulted_times[2] < rate_update and
|
||||
self.imtx_faulted_times[3] < charge_update
|
||||
end
|
||||
|
||||
if has_data then
|
||||
local energy = util.joules_to_fe(db.tanks.energy)
|
||||
local input = util.joules_to_fe(db.state.last_input)
|
||||
local output = util.joules_to_fe(db.state.last_output)
|
||||
|
||||
if (charge_update > 0) and (rate_update > 0) then
|
||||
if self.im_stat_init then
|
||||
self.avg_charge.record(util.joules_to_fe(db.tanks.energy), charge_update)
|
||||
self.avg_inflow.record(util.joules_to_fe(db.state.last_input), rate_update)
|
||||
self.avg_outflow.record(util.joules_to_fe(db.state.last_output), rate_update)
|
||||
self.avg_charge.record(energy, charge_update)
|
||||
self.avg_inflow.record(input, rate_update)
|
||||
self.avg_outflow.record(output, rate_update)
|
||||
|
||||
if charge_update ~= self.imtx_last_charge_t then
|
||||
local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t)
|
||||
|
||||
self.imtx_last_charge = energy
|
||||
self.imtx_last_charge_t = charge_update
|
||||
|
||||
-- if the capacity changed, toss out existing data
|
||||
if db.build.max_energy ~= self.imtx_last_capacity then
|
||||
self.imtx_last_capacity = db.build.max_energy
|
||||
self.avg_net.reset()
|
||||
else
|
||||
self.avg_net.record(delta, charge_update)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.im_stat_init = true
|
||||
self.avg_charge.reset(util.joules_to_fe(db.tanks.energy))
|
||||
self.avg_inflow.reset(util.joules_to_fe(db.state.last_input))
|
||||
self.avg_outflow.reset(util.joules_to_fe(db.state.last_output))
|
||||
|
||||
self.avg_charge.reset(energy)
|
||||
self.avg_inflow.reset(input)
|
||||
self.avg_outflow.reset(output)
|
||||
self.avg_net.reset()
|
||||
|
||||
self.imtx_last_capacity = db.build.max_energy
|
||||
self.imtx_last_charge = energy
|
||||
self.imtx_last_charge_t = charge_update
|
||||
end
|
||||
else
|
||||
-- prevent use by control systems
|
||||
rate_update = 0
|
||||
charge_update = 0
|
||||
end
|
||||
else
|
||||
self.im_stat_init = false
|
||||
@@ -325,10 +387,11 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
--#region
|
||||
|
||||
local avg_charge = self.avg_charge.compute()
|
||||
local avg_inflow = self.avg_inflow.compute()
|
||||
local avg_charge = self.avg_charge.compute()
|
||||
local avg_inflow = self.avg_inflow.compute()
|
||||
local avg_outflow = self.avg_outflow.compute()
|
||||
|
||||
local now = util.time_s()
|
||||
local now = os.clock()
|
||||
|
||||
local state_changed = self.mode ~= self.last_mode
|
||||
local next_mode = self.mode
|
||||
@@ -390,6 +453,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
-- disable reactors and disengage auto control
|
||||
for _, u in pairs(self.prio_defs[i]) do
|
||||
u.disable()
|
||||
u.auto_set_idle(false)
|
||||
u.auto_disengage()
|
||||
end
|
||||
end
|
||||
@@ -460,9 +524,12 @@ function facility.new(num_reactors, cooling_conf)
|
||||
self.last_error = 0
|
||||
self.accumulator = 0
|
||||
|
||||
-- enabling idling on all assigned units
|
||||
_set_idling(true)
|
||||
|
||||
self.status_text = { "CHARGE MODE", "running control loop" }
|
||||
log.info("FAC: CHARGE mode starting PID control")
|
||||
elseif self.last_update ~= charge_update then
|
||||
elseif self.last_update < charge_update then
|
||||
-- convert to kFE to make constants not microscopic
|
||||
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
|
||||
|
||||
@@ -475,9 +542,9 @@ function facility.new(num_reactors, cooling_conf)
|
||||
local integral = self.accumulator
|
||||
local derivative = (error - self.last_error) / (now - self.last_time)
|
||||
|
||||
local P = (charge_Kp * error)
|
||||
local I = (charge_Ki * integral)
|
||||
local D = (charge_Kd * derivative)
|
||||
local P = charge_Kp * error
|
||||
local I = charge_Ki * integral
|
||||
local D = charge_Kd * derivative
|
||||
|
||||
local output = P + I + D
|
||||
|
||||
@@ -486,7 +553,12 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
self.saturated = output ~= out_c
|
||||
|
||||
-- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }",
|
||||
if not config.ExtChargeIdling then
|
||||
-- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge
|
||||
_set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0)))
|
||||
end
|
||||
|
||||
-- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
|
||||
-- runtime, avg_charge, error, integral, output, out_c, P, I, D))
|
||||
|
||||
_allocate_burn_rate(out_c, true)
|
||||
@@ -531,7 +603,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
self.status_text = { "GENERATION MODE", "running control loop" }
|
||||
log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control")
|
||||
end
|
||||
elseif self.last_update ~= rate_update then
|
||||
elseif self.last_update < rate_update then
|
||||
-- convert to MFE (in rounded kFE) to make constants not microscopic
|
||||
local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000
|
||||
|
||||
@@ -544,9 +616,9 @@ function facility.new(num_reactors, cooling_conf)
|
||||
local integral = self.accumulator
|
||||
local derivative = (error - self.last_error) / (now - self.last_time)
|
||||
|
||||
local P = (rate_Kp * error)
|
||||
local I = (rate_Ki * integral)
|
||||
local D = (rate_Kd * derivative)
|
||||
local P = rate_Kp * error
|
||||
local I = rate_Ki * integral
|
||||
local D = rate_Kd * derivative
|
||||
|
||||
-- velocity (rate) (derivative of charge level => rate) feed forward
|
||||
local FF = self.gen_rate_setpoint / self.charge_conversion
|
||||
@@ -602,8 +674,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
local astatus = self.ascram_status
|
||||
|
||||
if self.induction[1] ~= nil then
|
||||
local matrix = self.induction[1] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
local db = self.induction[1].get_db() ---@type imatrix_session_db
|
||||
|
||||
-- clear matrix disconnected
|
||||
if astatus.matrix_dc then
|
||||
@@ -756,6 +827,15 @@ function facility.new(num_reactors, cooling_conf)
|
||||
|
||||
self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm)
|
||||
self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
|
||||
|
||||
-- update induction matrix related outputs
|
||||
if self.induction[1] ~= nil then
|
||||
local db = self.induction[1].get_db() ---@type imatrix_session_db
|
||||
|
||||
self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW)
|
||||
self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH)
|
||||
self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1)
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
@@ -786,9 +866,25 @@ function facility.new(num_reactors, cooling_conf)
|
||||
end
|
||||
|
||||
-- update waste product
|
||||
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
|
||||
|
||||
self.current_waste_product = self.waste_product
|
||||
|
||||
if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then
|
||||
local db = self.induction[1].get_db() ---@type imatrix_session_db
|
||||
|
||||
if db.tanks.energy_fill >= 0.15 then
|
||||
self.disabled_sps = false
|
||||
elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then
|
||||
self.disabled_sps = true
|
||||
self.current_waste_product = WASTE.POLONIUM
|
||||
end
|
||||
else
|
||||
self.disabled_sps = false
|
||||
end
|
||||
|
||||
if self.pu_fallback and insufficent_po_rate then
|
||||
self.current_waste_product = WASTE.PLUTONIUM
|
||||
else self.current_waste_product = self.waste_product end
|
||||
end
|
||||
|
||||
-- make sure dynamic tanks are allowing outflow if required
|
||||
-- set all, rather than trying to determine which is for which (simpler & safer)
|
||||
@@ -936,41 +1032,41 @@ function facility.new(num_reactors, cooling_conf)
|
||||
function public.auto_stop() self.mode = PROCESS.INACTIVE end
|
||||
|
||||
-- set automatic control configuration and start the process
|
||||
---@param config coord_auto_config configuration
|
||||
---@param auto_cfg coord_auto_config configuration
|
||||
---@return table response ready state (successfully started) and current configuration (after updating)
|
||||
function public.auto_start(config)
|
||||
function public.auto_start(auto_cfg)
|
||||
local charge_scaler = 1000000 -- convert MFE to FE
|
||||
local gen_scaler = 1000 -- convert kFE to FE
|
||||
local ready = false
|
||||
|
||||
-- load up current limits
|
||||
local limits = {}
|
||||
for i = 1, num_reactors do
|
||||
for i = 1, config.UnitCount do
|
||||
local u = self.units[i] ---@type reactor_unit
|
||||
limits[i] = u.get_control_inf().lim_br100 * 100
|
||||
end
|
||||
|
||||
-- only allow changes if not running
|
||||
if self.mode == PROCESS.INACTIVE then
|
||||
if (type(config.mode) == "number") and (config.mode > PROCESS.INACTIVE) and (config.mode <= PROCESS.GEN_RATE) then
|
||||
self.mode_set = config.mode
|
||||
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then
|
||||
self.mode_set = auto_cfg.mode
|
||||
end
|
||||
|
||||
if (type(config.burn_target) == "number") and config.burn_target >= 0.1 then
|
||||
self.burn_target = config.burn_target
|
||||
if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
|
||||
self.burn_target = auto_cfg.burn_target
|
||||
end
|
||||
|
||||
if (type(config.charge_target) == "number") and config.charge_target >= 0 then
|
||||
self.charge_setpoint = config.charge_target * charge_scaler
|
||||
if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then
|
||||
self.charge_setpoint = auto_cfg.charge_target * charge_scaler
|
||||
end
|
||||
|
||||
if (type(config.gen_target) == "number") and config.gen_target >= 0 then
|
||||
self.gen_rate_setpoint = config.gen_target * gen_scaler
|
||||
if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then
|
||||
self.gen_rate_setpoint = auto_cfg.gen_target * gen_scaler
|
||||
end
|
||||
|
||||
if (type(config.limits) == "table") and (#config.limits == num_reactors) then
|
||||
for i = 1, num_reactors do
|
||||
local limit = config.limits[i]
|
||||
if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then
|
||||
for i = 1, config.UnitCount do
|
||||
local limit = auto_cfg.limits[i]
|
||||
|
||||
if (type(limit) == "number") and (limit >= 0.1) then
|
||||
limits[i] = limit
|
||||
@@ -1010,7 +1106,7 @@ function facility.new(num_reactors, cooling_conf)
|
||||
---@param unit_id integer unit ID
|
||||
---@param group integer group ID or 0 for independent
|
||||
function public.set_group(unit_id, group)
|
||||
if (group >= 0 and group <= 4) and (unit_id > 0 and unit_id <= num_reactors) and self.mode == PROCESS.INACTIVE then
|
||||
if (group >= 0 and group <= 4) and (unit_id > 0 and unit_id <= config.UnitCount) and self.mode == PROCESS.INACTIVE then
|
||||
-- remove from old group if previously assigned
|
||||
local old_group = self.group_map[unit_id]
|
||||
if old_group ~= 0 then
|
||||
@@ -1045,6 +1141,14 @@ function facility.new(num_reactors, cooling_conf)
|
||||
return self.pu_fallback
|
||||
end
|
||||
|
||||
-- enable/disable SPS at low power
|
||||
---@param enabled boolean requested state
|
||||
---@return boolean enabled newly set value
|
||||
function public.set_sps_low_power(enabled)
|
||||
self.sps_low_power = enabled == true
|
||||
return self.sps_low_power
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Diagnostic Testing
|
||||
@@ -1149,7 +1253,8 @@ function facility.new(num_reactors, cooling_conf)
|
||||
self.status_text[2],
|
||||
self.group_map,
|
||||
self.current_waste_product,
|
||||
(self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM)
|
||||
self.pu_fallback and (self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM),
|
||||
self.disabled_sps
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1165,15 +1270,21 @@ function facility.new(num_reactors, cooling_conf)
|
||||
status.power = {
|
||||
self.avg_charge.compute(),
|
||||
self.avg_inflow.compute(),
|
||||
self.avg_outflow.compute()
|
||||
self.avg_outflow.compute(),
|
||||
0
|
||||
}
|
||||
|
||||
-- status of induction matricies (including tanks)
|
||||
status.induction = {}
|
||||
for i = 1, #self.induction do
|
||||
local matrix = self.induction[i] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
local matrix = self.induction[i] ---@type unit_session
|
||||
local db = matrix.get_db() ---@type imatrix_session_db
|
||||
|
||||
status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
|
||||
|
||||
local fe_per_ms = self.avg_net.compute()
|
||||
local remaining = util.joules_to_fe(util.trinary(fe_per_ms >= 0, db.tanks.energy_need, db.tanks.energy))
|
||||
status.power[4] = remaining / fe_per_ms
|
||||
end
|
||||
|
||||
-- status of sps
|
||||
|
||||
@@ -17,9 +17,6 @@ local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local SV_Q_DATA = svqtypes.SV_Q_DATA
|
||||
|
||||
-- grace period in seconds for coordinator to finish UI draw to prevent timeout
|
||||
local WATCHDOG_GRACE = 20.0
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
local RETRY_PERIOD = 1000
|
||||
@@ -273,6 +270,12 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
else
|
||||
log.debug(log_header .. "CRDN set pu fallback packet length mismatch")
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.SET_SPS_LP then
|
||||
if pkt.length == 2 then
|
||||
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
|
||||
else
|
||||
log.debug(log_header .. "CRDN set sps low power packet length mismatch")
|
||||
end
|
||||
else
|
||||
log.debug(log_header .. "CRDN facility command unknown")
|
||||
end
|
||||
@@ -360,15 +363,7 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
||||
-- check if a timer matches this session's watchdog
|
||||
---@nodiscard
|
||||
function public.check_wd(timer)
|
||||
local is_wd = self.conn_watchdog.is_timer(timer) and self.connected
|
||||
|
||||
-- if we are waiting for initial coordinator UI draw, don't close yet
|
||||
if is_wd and (util.time_s() - self.establish_time) <= WATCHDOG_GRACE then
|
||||
self.conn_watchdog.feed()
|
||||
is_wd = false
|
||||
end
|
||||
|
||||
return is_wd
|
||||
return self.conn_watchdog.is_timer(timer) and self.connected
|
||||
end
|
||||
|
||||
-- close the connection
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local const = require("scada-common.constants")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local types = require("scada-common.types")
|
||||
@@ -105,6 +106,8 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
|
||||
formed = false,
|
||||
rps_tripped = false,
|
||||
rps_trip_cause = "ok", ---@type rps_trip_cause
|
||||
max_op_temp_H2O = 1200,
|
||||
max_op_temp_Na = 1200,
|
||||
---@class rps_status
|
||||
rps_status = {
|
||||
high_dmg = false,
|
||||
@@ -138,11 +141,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
|
||||
waste = 0,
|
||||
waste_need = 0,
|
||||
waste_fill = 0.0,
|
||||
ccool_type = "?",
|
||||
ccool_type = types.FLUID.EMPTY_GAS, ---@type fluid
|
||||
ccool_amnt = 0,
|
||||
ccool_need = 0,
|
||||
ccool_fill = 0.0,
|
||||
hcool_type = "?",
|
||||
hcool_type = types.FLUID.EMPTY_GAS, ---@type fluid
|
||||
hcool_amnt = 0,
|
||||
hcool_need = 0,
|
||||
hcool_fill = 0.0
|
||||
@@ -169,6 +172,21 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
|
||||
---@class plc_session
|
||||
local public = {}
|
||||
|
||||
-- compute maximum expected operational temperatures for high temp warnings
|
||||
local function _compute_op_temps()
|
||||
local JOULES_PER_MB = const.mek.JOULES_PER_MB
|
||||
local BASE_BOIL_TEMP = const.mek.BASE_BOIL_TEMP
|
||||
|
||||
local heat_cap = self.sDB.mek_struct.heat_cap
|
||||
local max_burn = self.sDB.mek_struct.max_burn
|
||||
|
||||
self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
|
||||
self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
|
||||
|
||||
log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
|
||||
self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na))
|
||||
end
|
||||
|
||||
-- copy in the RPS status
|
||||
---@param rps_status table
|
||||
local function _copy_rps_status(rps_status)
|
||||
@@ -351,6 +369,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
|
||||
local status = pcall(_copy_struct, pkt.data)
|
||||
if status then
|
||||
-- copied in structure data OK
|
||||
_compute_op_temps()
|
||||
self.received_struct = true
|
||||
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
|
||||
else
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
-- Redstone RTU Session I/O Controller
|
||||
--
|
||||
|
||||
local rsio = require("scada-common.rsio")
|
||||
|
||||
local rsctl = {}
|
||||
|
||||
-- create a new redstone RTU I/O controller
|
||||
@@ -16,7 +18,7 @@ function rsctl.new(redstone_rtus)
|
||||
---@return boolean
|
||||
function public.is_connected(port)
|
||||
for i = 1, #redstone_rtus do
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
if db.io[port] ~= nil then return true end
|
||||
end
|
||||
|
||||
@@ -28,8 +30,8 @@ function rsctl.new(redstone_rtus)
|
||||
---@param value boolean
|
||||
function public.digital_write(port, value)
|
||||
for i = 1, #redstone_rtus do
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
local io = db.io[port] ---@type rs_db_dig_io|nil
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
local io = db.io[port] ---@type rs_db_dig_io|nil
|
||||
if io ~= nil then io.write(value) end
|
||||
end
|
||||
end
|
||||
@@ -40,12 +42,25 @@ function rsctl.new(redstone_rtus)
|
||||
---@return boolean|nil
|
||||
function public.digital_read(port)
|
||||
for i = 1, #redstone_rtus do
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
local io = db.io[port] ---@type rs_db_dig_io|nil
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
local io = db.io[port] ---@type rs_db_dig_io|nil
|
||||
if io ~= nil then return io.read() end
|
||||
end
|
||||
end
|
||||
|
||||
-- write to an analog redstone port (applies to all RTUs)
|
||||
---@param port IO_PORT
|
||||
---@param value number value
|
||||
---@param min number minimum value for scaling 0 to 15
|
||||
---@param max number maximum value for scaling 0 to 15
|
||||
function public.analog_write(port, value, min, max)
|
||||
for i = 1, #redstone_rtus do
|
||||
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
|
||||
local io = db.io[port] ---@type rs_db_ana_io|nil
|
||||
if io ~= nil then io.write(rsio.analog_write(value, min, max)) end
|
||||
end
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ function svsessions.init(nic, fp_ok, config, cooling_conf)
|
||||
self.nic = nic
|
||||
self.fp_ok = fp_ok
|
||||
self.config = config
|
||||
self.facility = facility.new(config.UnitCount, cooling_conf)
|
||||
self.facility = facility.new(config, cooling_conf)
|
||||
end
|
||||
|
||||
-- find an RTU session by the computer ID
|
||||
|
||||
@@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
|
||||
|
||||
local svsessions = require("supervisor.session.svsessions")
|
||||
|
||||
local SUPERVISOR_VERSION = "v1.3.4"
|
||||
local SUPERVISOR_VERSION = "v1.3.10"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
@@ -26,6 +26,7 @@ function supervisor.load_config()
|
||||
config.CoolingConfig = settings.get("CoolingConfig")
|
||||
config.FacilityTankMode = settings.get("FacilityTankMode")
|
||||
config.FacilityTankDefs = settings.get("FacilityTankDefs")
|
||||
config.ExtChargeIdling = settings.get("ExtChargeIdling")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.PLC_Channel = settings.get("PLC_Channel")
|
||||
@@ -58,6 +59,8 @@ function supervisor.load_config()
|
||||
cfv.assert_type_int(config.FacilityTankMode)
|
||||
cfv.assert_range(config.FacilityTankMode, 0, 8)
|
||||
|
||||
cfv.assert_type_bool(config.ExtChargeIdling)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.PLC_Channel)
|
||||
cfv.assert_channel(config.RTU_Channel)
|
||||
@@ -78,7 +81,7 @@ function supervisor.load_config()
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||
cfv.assert(len == 0 or len >= 8)
|
||||
end
|
||||
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
|
||||
@@ -8,9 +8,6 @@ local logic = require("supervisor.unitlogic")
|
||||
local plc = require("supervisor.session.plc")
|
||||
local rsctl = require("supervisor.session.rsctl")
|
||||
|
||||
---@class reactor_control_unit
|
||||
local unit = {}
|
||||
|
||||
local WASTE_MODE = types.WASTE_MODE
|
||||
local WASTE = types.WASTE_PRODUCT
|
||||
local ALARM = types.ALARM
|
||||
@@ -55,17 +52,27 @@ local AISTATE = {
|
||||
---@field id ALARM alarm ID
|
||||
---@field tier integer alarm urgency tier (0 = highest)
|
||||
|
||||
-- burn rate to idle at
|
||||
local IDLE_RATE = 0.01
|
||||
|
||||
---@class reactor_control_unit
|
||||
local unit = {}
|
||||
|
||||
-- create a new reactor unit
|
||||
---@nodiscard
|
||||
---@param reactor_id integer reactor unit number
|
||||
---@param num_boilers integer number of boilers expected
|
||||
---@param num_turbines integer number of turbines expected
|
||||
function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
---@param ext_idle boolean extended idling mode
|
||||
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
|
||||
-- time (ms) to idle for auto idling
|
||||
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
|
||||
|
||||
---@class _unit_self
|
||||
local self = {
|
||||
r_id = reactor_id,
|
||||
plc_s = nil, ---@class plc_session_struct
|
||||
plc_i = nil, ---@class plc_session
|
||||
plc_s = nil, ---@type plc_session_struct
|
||||
plc_i = nil, ---@type plc_session
|
||||
num_boilers = num_boilers,
|
||||
num_turbines = num_turbines,
|
||||
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
|
||||
@@ -83,6 +90,9 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
emcool_opened = false,
|
||||
-- auto control
|
||||
auto_engaged = false,
|
||||
auto_idle = false,
|
||||
auto_idling = false,
|
||||
auto_idle_start = 0,
|
||||
auto_was_alarmed = false,
|
||||
ramp_target_br100 = 0,
|
||||
-- state tracking
|
||||
@@ -98,6 +108,8 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
status_text = { "UNKNOWN", "awaiting connection..." },
|
||||
-- logic for alarms
|
||||
had_reactor = false,
|
||||
turbine_flow_stable = false,
|
||||
turbine_stability_data = {},
|
||||
last_rate_change_ms = 0,
|
||||
---@type rps_status
|
||||
last_rps_trips = {
|
||||
@@ -135,7 +147,8 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
},
|
||||
damage = 0,
|
||||
temp = 0,
|
||||
waste = 0
|
||||
waste = 0,
|
||||
high_temp_lim = 1150
|
||||
},
|
||||
---@class alarm_monitors
|
||||
alarms = {
|
||||
@@ -251,6 +264,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
table.insert(self.db.annunciator.TurbineOverSpeed, false)
|
||||
table.insert(self.db.annunciator.GeneratorTrip, false)
|
||||
table.insert(self.db.annunciator.TurbineTrip, false)
|
||||
table.insert(self.turbine_stability_data, { time_state = 0, time_tanks = 0, rotation = 1 })
|
||||
end
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
@@ -530,6 +544,13 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
|
||||
-- re-engage auto lock if it reconnected without it
|
||||
if self.auto_engaged and not self.plc_i.is_auto_locked() then self.plc_i.auto_lock(true) end
|
||||
|
||||
-- stop idling when completed
|
||||
if self.auto_idling and (((util.time_ms() - self.auto_idle_start) > IDLE_TIME) or not self.auto_idle) then
|
||||
log.info(util.c("UNIT ", self.r_id, ": completed idling period"))
|
||||
self.auto_idling = false
|
||||
self.plc_i.auto_set_burn(0, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- update deltas
|
||||
@@ -578,6 +599,23 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
end
|
||||
end
|
||||
|
||||
-- set automatic control idling mode to change behavior when given a burn rate command of zero<br>
|
||||
-- - enabling it will hold the reactor at 0.01 mB/t for a period when commanded zero before disabling
|
||||
-- - disabling it will stop the reactor when commanded zero
|
||||
---@param idle boolean true to enable, false to disable (and stop)
|
||||
function public.auto_set_idle(idle)
|
||||
if idle and not self.auto_idle then
|
||||
self.auto_idling = false
|
||||
self.auto_idle_start = 0
|
||||
end
|
||||
|
||||
if idle ~= self.auto_idle then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": idling mode changed to ", idle))
|
||||
end
|
||||
|
||||
self.auto_idle = idle
|
||||
end
|
||||
|
||||
-- get the actual limit of this unit<br>
|
||||
-- if it is degraded or not ready, the limit will be 0
|
||||
---@nodiscard
|
||||
@@ -597,7 +635,35 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
||||
if self.auto_engaged then
|
||||
if self.plc_i ~= nil then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp))
|
||||
self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
|
||||
|
||||
local rate = self.db.control.br100 / 100
|
||||
|
||||
if self.auto_idle then
|
||||
if rate <= IDLE_RATE then
|
||||
if self.auto_idle_start == 0 then
|
||||
self.auto_idling = true
|
||||
self.auto_idle_start = util.time_ms()
|
||||
log.info(util.c("UNIT ", self.r_id, ": started idling at ", IDLE_RATE, " mB/t"))
|
||||
|
||||
rate = IDLE_RATE
|
||||
elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then
|
||||
if self.auto_idling then
|
||||
self.auto_idling = false
|
||||
log.info(util.c("UNIT ", self.r_id, ": completed idling period"))
|
||||
end
|
||||
else
|
||||
log.debug(util.c("UNIT ", self.r_id, ": continuing idle at ", IDLE_RATE, " mB/t"))
|
||||
|
||||
rate = IDLE_RATE
|
||||
end
|
||||
else
|
||||
self.auto_idling = false
|
||||
self.auto_idle_start = 0
|
||||
end
|
||||
end
|
||||
|
||||
self.plc_i.auto_set_burn(rate, ramp)
|
||||
|
||||
if ramp then self.ramp_target_br100 = self.db.control.br100 end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,6 +39,21 @@ local ALARM_LIMS = const.ALARM_LIMITS
|
||||
---@class unit_logic_extension
|
||||
local logic = {}
|
||||
|
||||
-- compute Mekanism's rotation rate for a turbine
|
||||
---@param turbine turbinev_session_db
|
||||
local function turbine_rotation(turbine)
|
||||
local build = turbine.build
|
||||
|
||||
local inner_vol = build.steam_cap / const.mek.TURBINE_GAS_PER_TANK
|
||||
local disp_rate = (build.dispersers * const.mek.TURBINE_DISPERSER_FLOW) * inner_vol
|
||||
local vent_rate = build.vents * const.mek.TURBINE_VENT_FLOW
|
||||
|
||||
local max_rate = math.min(disp_rate, vent_rate)
|
||||
local flow = math.min(max_rate, turbine.tanks.steam.amount)
|
||||
|
||||
return (flow * (turbine.tanks.steam.amount / build.steam_cap)) / max_rate
|
||||
end
|
||||
|
||||
-- update the annunciator
|
||||
---@param self _unit_self
|
||||
function logic.update_annunciator(self)
|
||||
@@ -81,6 +96,11 @@ function logic.update_annunciator(self)
|
||||
-- some alarms wait until the burn rate has stabilized, so keep track of that
|
||||
if math.abs(_get_dt(DT_KEYS.ReactorBurnR)) > 0 then
|
||||
self.last_rate_change_ms = util.time_ms()
|
||||
self.turbine_flow_stable = false
|
||||
|
||||
for t = 1, self.num_turbines do
|
||||
self.turbine_stability_data[t] = { time_state = 0, time_tanks = 0, rotation = 1 }
|
||||
end
|
||||
end
|
||||
|
||||
-- record reactor stats
|
||||
@@ -113,7 +133,15 @@ function logic.update_annunciator(self)
|
||||
self.last_heartbeat = plc_db.last_status_update
|
||||
end
|
||||
|
||||
local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O)
|
||||
local flow_low = ANNUNC_LIMS.RCSFlowLow_H2O
|
||||
local high_temp = plc_db.max_op_temp_H2O
|
||||
|
||||
if plc_db.mek_status.ccool_type == types.FLUID.SODIUM then
|
||||
flow_low = ANNUNC_LIMS.RCSFlowLow_NA
|
||||
high_temp = plc_db.max_op_temp_Na
|
||||
end
|
||||
|
||||
self.plc_cache.high_temp_lim = math.min(high_temp + ANNUNC_LIMS.OpTempTolerance, 1200)
|
||||
|
||||
-- update other annunciator fields
|
||||
annunc.ReactorSCRAM = plc_db.rps_tripped
|
||||
@@ -122,7 +150,7 @@ function logic.update_annunciator(self)
|
||||
annunc.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool)
|
||||
annunc.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low
|
||||
annunc.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
|
||||
annunc.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
|
||||
annunc.ReactorTempHigh = plc_db.mek_status.temp >= self.plc_cache.high_temp_lim
|
||||
annunc.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
|
||||
annunc.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
|
||||
annunc.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
|
||||
@@ -274,6 +302,7 @@ function logic.update_annunciator(self)
|
||||
local total_flow_rate = 0
|
||||
local total_input_rate = 0
|
||||
local max_water_return_rate = 0
|
||||
local turbines_stable = true
|
||||
|
||||
-- recompute blade count on the chance that it may have changed
|
||||
self.db.control.blade_count = 0
|
||||
@@ -282,12 +311,14 @@ function logic.update_annunciator(self)
|
||||
for i = 1, #self.turbines do
|
||||
local session = self.turbines[i] ---@type unit_session
|
||||
local turbine = session.get_db() ---@type turbinev_session_db
|
||||
local idx = session.get_device_idx()
|
||||
|
||||
annunc.RCSFault = annunc.RCSFault or (not turbine.formed) or session.is_faulted()
|
||||
annunc.TurbineOnline[idx] = true
|
||||
|
||||
-- update ready state
|
||||
-- - must be formed
|
||||
-- - must have received build, state, and tanks at least once
|
||||
-- - must be formed
|
||||
-- - must have received build, state, and tanks at least once
|
||||
turbines_ready = turbines_ready and turbine.formed and
|
||||
(turbine.build.last_update > 0) and
|
||||
(turbine.state.last_update > 0) and
|
||||
@@ -296,11 +327,56 @@ function logic.update_annunciator(self)
|
||||
total_flow_rate = total_flow_rate + turbine.state.flow_rate
|
||||
total_input_rate = total_input_rate + turbine.state.steam_input_rate
|
||||
max_water_return_rate = max_water_return_rate + turbine.build.max_water_output
|
||||
|
||||
self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades
|
||||
|
||||
annunc.TurbineOnline[session.get_device_idx()] = true
|
||||
local last = self.turbine_stability_data[i]
|
||||
|
||||
if (not self.turbine_flow_stable) and (turbine.state.steam_input_rate > 0) then
|
||||
local rotation = turbine_rotation(turbine)
|
||||
local rotation_stable = false
|
||||
|
||||
-- see if data updated, and if so, check rotation speed change
|
||||
-- minimal change indicates the turbine is converging on a flow rate
|
||||
if last.time_tanks < turbine.tanks.last_update then
|
||||
if last.time_tanks > 0 then
|
||||
rotation_stable = math.abs(rotation - last.rotation) < 0.00000003
|
||||
end
|
||||
|
||||
last.time_tanks = turbine.tanks.last_update
|
||||
last.rotation = rotation
|
||||
end
|
||||
|
||||
-- flow is stable if the flow rate is at the input rate or at the max (±1 mB/t)
|
||||
local flow_stable = false
|
||||
if last.time_state < turbine.state.last_update then
|
||||
if (last.time_state > 0) and (turbine.state.flow_rate > 0) then
|
||||
flow_stable = math.abs(turbine.state.flow_rate - math.min(turbine.state.steam_input_rate, turbine.build.max_flow_rate)) < 2
|
||||
end
|
||||
|
||||
last.time_state = turbine.state.last_update
|
||||
end
|
||||
|
||||
if rotation_stable then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached rotational stability (", rotation, ")"))
|
||||
end
|
||||
|
||||
if flow_stable then
|
||||
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached flow stability (", turbine.state.flow_rate, " mB/t)"))
|
||||
end
|
||||
|
||||
turbines_stable = turbines_stable and (rotation_stable or flow_stable)
|
||||
else
|
||||
last.time_state = 0
|
||||
last.time_tanks = 0
|
||||
last.rotation = 1
|
||||
|
||||
turbines_stable = false
|
||||
end
|
||||
end
|
||||
|
||||
self.turbine_flow_stable = self.turbine_flow_stable or turbines_stable
|
||||
|
||||
-- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine
|
||||
annunc.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
|
||||
|
||||
@@ -474,7 +550,8 @@ function logic.update_alarms(self)
|
||||
end
|
||||
|
||||
-- High Temperature
|
||||
_update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp)
|
||||
local high_temp = math.min(math.max(self.plc_cache.high_temp_lim, 1100), 1199.995)
|
||||
_update_alarm_state(self, plc_cache.temp >= high_temp, self.alarms.ReactorHighTemp)
|
||||
|
||||
-- Waste Leak
|
||||
_update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak)
|
||||
@@ -508,11 +585,25 @@ function logic.update_alarms(self)
|
||||
|
||||
local rcs_trans = any_low or any_over or gen_trip or annunc.RCPTrip or annunc.MaxWaterReturnFeed
|
||||
|
||||
-- annunciator indicators for these states may not indicate a real issue when:
|
||||
-- > flow is ramping up right after reactor start
|
||||
-- > flow is ramping down after reactor shutdown
|
||||
if ((util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS) and plc_cache.active then
|
||||
rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch
|
||||
if plc_cache.active then
|
||||
-- these conditions may not indicate an issue when flow is changing after a burn rate change
|
||||
if self.num_boilers == 0 then
|
||||
if (util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS then
|
||||
rcs_trans = rcs_trans or annunc.BoilRateMismatch
|
||||
end
|
||||
|
||||
if self.turbine_flow_stable then
|
||||
rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch
|
||||
end
|
||||
else
|
||||
if (util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS then
|
||||
rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch
|
||||
end
|
||||
|
||||
if self.turbine_flow_stable then
|
||||
rcs_trans = rcs_trans or annunc.SteamFeedMismatch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if _update_alarm_state(self, rcs_trans, self.alarms.RCSTransient) then
|
||||
@@ -636,11 +727,11 @@ function logic.update_status_text(self)
|
||||
self.status_text[2] = "elevated level of radiation"
|
||||
end
|
||||
elseif is_active(self.alarms.ReactorOverTemp) then
|
||||
self.status_text = { "CORE OVER TEMP", "reactor core temperature >=1200K" }
|
||||
self.status_text = { "CORE OVER TEMP", "reactor core temp damaging" }
|
||||
elseif is_active(self.alarms.ReactorWasteLeak) then
|
||||
self.status_text = { "WASTE LEAK", "radioactive waste leak detected" }
|
||||
elseif is_active(self.alarms.ReactorHighTemp) then
|
||||
self.status_text = { "CORE TEMP HIGH", "reactor core temperature >1150K" }
|
||||
self.status_text = { "CORE TEMP HIGH", "reactor core temperature high" }
|
||||
elseif is_active(self.alarms.ReactorHighWaste) then
|
||||
self.status_text = { "WASTE LEVEL HIGH", "waste accumulating in reactor" }
|
||||
elseif is_active(self.alarms.TurbineTrip) then
|
||||
@@ -666,7 +757,9 @@ function logic.update_status_text(self)
|
||||
elseif annunc.WasteLineOcclusion then
|
||||
self.status_text[2] = "insufficient waste output rate"
|
||||
elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
|
||||
self.status_text[2] = "awaiting flow stability"
|
||||
self.status_text[2] = "awaiting coolant flow stability"
|
||||
elseif not self.turbine_flow_stable then
|
||||
self.status_text[2] = "awaiting turbine flow stability"
|
||||
else
|
||||
self.status_text[2] = "system nominal"
|
||||
end
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
require("/initenv").init_env()
|
||||
|
||||
local rsio = require("scada-common.rsio")
|
||||
local util = require("scada-common.util")
|
||||
local rsio = require("scada-common.rsio")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local testutils = require("test.testutils")
|
||||
|
||||
local IO = rsio.IO
|
||||
local IO_LVL = rsio.IO_LVL
|
||||
local IO_MODE = rsio.IO_MODE
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
|
||||
local IO = rsio.IO
|
||||
local IO_LVL = rsio.IO_LVL
|
||||
local IO_MODE = rsio.IO_MODE
|
||||
-- list of inverted digital signals<br>
|
||||
-- only using the key for a quick lookup, value just can't be nil
|
||||
local DIG_INV = {
|
||||
[IO.F_SCRAM] = 0,
|
||||
[IO.R_SCRAM] = 0,
|
||||
[IO.WASTE_PU] = 0,
|
||||
[IO.WASTE_PO] = 0,
|
||||
[IO.WASTE_POPL] = 0,
|
||||
[IO.WASTE_AM] = 0,
|
||||
[IO.U_EMER_COOL] = 0
|
||||
}
|
||||
|
||||
println("starting RSIO tester")
|
||||
println("")
|
||||
@@ -50,8 +62,8 @@ testutils.pause()
|
||||
|
||||
println(">>> checking invalid ports:")
|
||||
|
||||
testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "")
|
||||
testutils.test_func_nil("rsio.to_string", rsio.to_string, "")
|
||||
testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "UNKNOWN")
|
||||
testutils.test_func_nil("rsio.to_string", rsio.to_string, "UNKNOWN")
|
||||
testutils.test_func("rsio.get_io_mode", rsio.get_io_mode, { -1, 100, false }, IO_MODE.ANALOG_IN)
|
||||
testutils.test_func_nil("rsio.get_io_mode", rsio.get_io_mode, IO_MODE.ANALOG_IN)
|
||||
|
||||
@@ -100,46 +112,35 @@ println(">>> checking port I/O:")
|
||||
|
||||
print("rsio.digital_is_active(...): ")
|
||||
|
||||
-- check input ports
|
||||
assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.LOW) == true, "IO_F_SCRAM_HIGH")
|
||||
assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.HIGH) == false, "IO_F_SCRAM_LOW")
|
||||
assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.LOW) == true, "IO_R_SCRAM_HIGH")
|
||||
assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.HIGH) == false, "IO_R_SCRAM_LOW")
|
||||
assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.LOW) == false, "IO_R_ENABLE_HIGH")
|
||||
assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.HIGH) == true, "IO_R_ENABLE_LOW")
|
||||
-- check all digital ports
|
||||
for i = 1, rsio.NUM_PORTS do
|
||||
if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then
|
||||
local high = DIG_INV[i] == nil
|
||||
assert(rsio.digital_is_active(i, IO_LVL.LOW) == not high, "IO_" .. rsio.to_string(i) .. "_LOW")
|
||||
assert(rsio.digital_is_active(i, IO_LVL.HIGH) == high, "IO_" .. rsio.to_string(i) .. "_HIGH")
|
||||
end
|
||||
end
|
||||
|
||||
-- non-inputs should always return LOW
|
||||
assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.LOW) == false, "IO_OUT_READ_LOW")
|
||||
assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.HIGH) == false, "IO_OUT_READ_HIGH")
|
||||
assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.LOW) == nil, "ANA_DIG_READ_LOW")
|
||||
assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.HIGH) == nil, "ANA_DIG_READ_HIGH")
|
||||
|
||||
println("PASS")
|
||||
|
||||
-- check output ports
|
||||
-- check digital write
|
||||
|
||||
print("rsio.digital_write(...): ")
|
||||
print("rsio.digital_write_active(...): ")
|
||||
|
||||
-- check output ports
|
||||
assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.LOW, "IO_F_ALARM_LOW")
|
||||
assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_HIGH")
|
||||
assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.HIGH, "IO_WASTE_PU_HIGH")
|
||||
assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_LOW")
|
||||
assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.HIGH, "IO_WASTE_PO_HIGH")
|
||||
assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_LOW")
|
||||
assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.HIGH, "IO_WASTE_POPL_HIGH")
|
||||
assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.LOW, "IO_WASTE_POPL_LOW")
|
||||
assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.HIGH, "IO_WASTE_AM_HIGH")
|
||||
assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_LOW")
|
||||
|
||||
-- check all reactor output ports (all are active high)
|
||||
for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do
|
||||
assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_PORT")
|
||||
assert(rsio.digital_write_active(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW")
|
||||
assert(rsio.digital_write_active(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH")
|
||||
-- check all digital ports
|
||||
for i = 1, rsio.NUM_PORTS do
|
||||
if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then
|
||||
local high = DIG_INV[i] == nil
|
||||
assert(rsio.digital_write_active(i, not high) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW")
|
||||
assert(rsio.digital_write_active(i, high) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH")
|
||||
end
|
||||
end
|
||||
|
||||
-- non-outputs should always return false
|
||||
assert(rsio.digital_write_active(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_FALSE")
|
||||
assert(rsio.digital_write_active(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_TRUE")
|
||||
assert(rsio.digital_write_active(IO.F_MATRIX_CHG, true) == false, "ANA_DIG_WRITE_TRUE")
|
||||
assert(rsio.digital_write_active(IO.F_MATRIX_CHG, false) == false, "ANA_DIG_WRITE_FALSE")
|
||||
|
||||
println("PASS")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user