Compare commits

...

89 Commits

Author SHA1 Message Date
Mikayla
afc38e7e7a Merge pull request #490 from MikaylaFischler/devel
2024.05.15 Release
2024-05-15 17:22:18 -04:00
Mikayla Fischler
41b7a68f3e #487 stop retrying failed disable when needing to enable 2024-05-14 20:10:21 -04:00
Mikayla Fischler
8968ebede3 #486 fixed pcall messages for newer CC versions 2024-05-14 20:00:34 -04:00
Mikayla
50a9168b06 Merge pull request #489 from MikaylaFischler/pocket-alpha-dev
Pocket Update
2024-05-12 18:27:03 -04:00
Mikayla Fischler
76e85da9d5 version increments and small fix 2024-05-12 18:19:21 -04:00
Mikayla Fischler
be560cd532 luacheck fixes 2024-05-12 15:19:01 -04:00
Mikayla Fischler
2b0a536292 #200 pocket RCS overview 2024-05-12 15:14:58 -04:00
Mikayla Fischler
afed6f514d removed stray space in annunciator Coolant Level Low 2024-05-12 15:05:36 -04:00
Mikayla Fischler
3e6a0a8869 #200 updated RPS indicator text 2024-05-12 14:20:48 -04:00
Mikayla Fischler
b99cf19be0 #200 placeholder for alarm page, start of RCS page 2024-05-12 13:36:46 -04:00
Mikayla Fischler
7f009f9c86 #200 RPS view on pocket 2024-05-12 13:28:34 -04:00
Mikayla Fischler
6a8ed311f3 #200 functioning pocket unit overview 2024-05-11 19:19:52 -04:00
Mikayla Fischler
c181142f75 added network details to about app 2024-05-11 15:03:14 -04:00
Mikayla Fischler
0cb964a177 diagnostic disables 2024-05-10 19:18:21 -04:00
Mikayla Fischler
3cd832ca20 sidebar updates 2024-05-10 19:17:52 -04:00
Mikayla Fischler
0c6c7bf9a5 #200 work in progress unit views and sidebar/app updates 2024-05-09 23:05:55 -04:00
Mikayla Fischler
3bfcb1d83c #485 fixed assertion with height auto incrementing y when inheriting height 2024-05-04 13:50:53 -04:00
Mikayla
4fe6792804 Merge pull request #483 from MikaylaFischler/devel
2024.04.30 Release
2024-04-30 20:35:24 -04:00
Mikayla Fischler
25dc47d520 fixed recording bad stats on induction matrix faults 2024-04-30 20:28:07 -04:00
Mikayla Fischler
f958b0e3b7 fixed at max i/o indicator 2024-04-30 20:27:04 -04:00
Mikayla Fischler
f621ff2482 added some value inits and unit labels 2024-04-30 18:54:01 -04:00
Mikayla Fischler
eb45ff899b #455 calculate reactor temp high limit 2024-04-29 22:03:54 -04:00
Mikayla
e91fd2fcaa Merge pull request #482 from MikaylaFischler/412-additional-matrix-integrations
412 additional matrix integrations
2024-04-28 13:08:51 -04:00
Mikayla Fischler
d35b824458 luacheck fix and cleanup 2024-04-28 13:08:16 -04:00
Mikayla Fischler
165d1497f8 reverted test change that got committed 2024-04-28 02:01:40 -04:00
Mikayla Fischler
50bf057ca6 #412 optionally disable SPS at low power 2024-04-28 02:01:21 -04:00
Mikayla Fischler
6f768ef6b3 #469 made ETA tolerant to induction matrix capacity changes 2024-04-28 01:26:44 -04:00
Mikayla Fischler
826086951e return zero on mov_avg compute if no samples 2024-04-27 19:50:35 -04:00
Mikayla Fischler
35bf56663f #469 induction matrix charge ETAs and misc cleanup/updates 2024-04-27 16:27:01 -04:00
Mikayla Fischler
7b8cea4a5c Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 13:57:47 -04:00
Mikayla Fischler
51d4a22532 #478 simplified reactor PLC reactor formed handling 2024-04-21 13:55:39 -04:00
Mikayla Fischler
fb85c2f05b RTU configurator updates for redstone I/O clarity 2024-04-21 13:54:14 -04:00
Mikayla Fischler
712d018806 Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 12:09:53 -04:00
Mikayla Fischler
00a8d64a88 fixed coordinator not showing FAIL on unit count mismatch when connecting 2024-04-20 20:38:55 -04:00
Mikayla Fischler
d9efd5b8d2 #412 updates to RSIO for induction matrix low, high, and analog charge level 2024-04-20 16:32:18 -04:00
Mikayla Fischler
a786404092 #476 fixed PPM not initing fault counter in __index handler when a function is found 2024-04-16 15:55:40 -04:00
Mikayla
1bd03c0b1a Merge pull request #473 from MikaylaFischler/devel
2024.04.14 Release
2024-04-15 11:59:40 -04:00
Mikayla Fischler
0d8d7aeb15 readme updates 2024-04-15 10:20:48 -04:00
Mikayla Fischler
0b60dc9fa4 #474 run main reactor-plc loop clock when not networked 2024-04-14 19:24:12 -04:00
Mikayla Fischler
48b91ac808 fixed reactor-plc configurator auth key displaying *** when not networked 2024-04-14 19:08:19 -04:00
Mikayla Fischler
4c9efc78b1 bumped up scada-common version 2024-04-14 16:30:50 -04:00
Mikayla
3c5857cd68 Merge pull request #472 from MikaylaFischler/pocket-alpha-dev
Work on Pocket Alpha App
2024-04-14 16:17:14 -04:00
Mikayla Fischler
473b0dd10d fixes and comments 2024-04-14 16:16:18 -04:00
Mikayla Fischler
d2df7db18b luacheck fixes 2024-04-14 15:30:13 -04:00
Mikayla Fischler
0ca002316f work on pocket, bumped comms version for PR 2024-04-14 15:28:38 -04:00
Mikayla Fischler
9fe34648c2 added system information/about app 2024-04-13 16:18:27 -04:00
Mikayla Fischler
57dc20692b need to use a compact signal bar due to how edge extensions work 2024-04-13 16:15:20 -04:00
Mikayla Fischler
f22d699baf flipped app pane scroll direction 2024-04-13 16:14:38 -04:00
Mikayla Fischler
23b31e0049 #410 pocket nav overhaul 2024-04-13 14:47:20 -04:00
Mikayla Fischler
2b4309afa7 pocket coordinator linking fixes 2024-04-13 11:02:41 -04:00
Mikayla
89cd5a07f2 Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-04-13 00:42:01 +00:00
Mikayla
99213da760 #200 work on pocket comms for unit data 2024-04-13 00:41:47 +00:00
Mikayla Fischler
f23f69d441 Merge branch 'devel' into pocket-alpha-dev 2024-04-12 20:27:21 -04:00
Mikayla
a99b7912f8 Merge pull request #471 from MikaylaFischler/366-charge-control-mode-idling
366 charge control mode idling
2024-04-12 20:07:21 -04:00
Mikayla Fischler
23ca5fb69e clear flow monitor on coordinator ui close 2024-04-12 20:06:39 -04:00
Mikayla Fischler
c243d064ef fixed incorrect render behavior on quick supervisor reconnects 2024-04-12 00:18:35 -04:00
Mikayla Fischler
878c3b92e1 #366 added idling to config and adjusted/fixed some behaviors 2024-04-12 00:13:05 -04:00
Mikayla Fischler
2aa52d2e2c simplified a config validation assert 2024-04-11 20:30:54 -04:00
Mikayla Fischler
dfc1ee6497 cleanup and constants 2024-04-10 22:16:41 -04:00
Mikayla Fischler
a6a1a61954 #470 reworked flow stability logic 2024-04-10 21:30:51 -04:00
Mikayla Fischler
d0b50c834c fixed coordinator not exiting on failed connection 2024-04-10 20:49:14 -04:00
Mikayla Fischler
612a06ba98 use os.clock rather than unix time for control 2024-04-09 22:24:03 -04:00
Mikayla Fischler
65d43d55c7 #366 idling and charge level PD gain changes 2024-04-09 22:23:43 -04:00
Mikayla Fischler
4fcd375ee2 show average charge rather than current charge below charge target 2024-04-09 22:20:46 -04:00
Mikayla Fischler
a22f5562cf only ramp up on reactor plc 2024-04-09 22:19:53 -04:00
Mikayla
0365ea5e8a Merge branch 'pocket-alpha-dev' of https://github.com/MikaylaFischler/cc-mek-scada into pocket-alpha-dev 2024-04-09 14:27:50 +00:00
Mikayla
da300607f1 Merge pull request #468 from MikaylaFischler/460-create-coordinator-ui-thread
460 create coordinator UI thread
2024-04-09 00:10:44 -04:00
Mikayla Fischler
cc3174ee76 comments and whitespace 2024-04-09 00:08:47 -04:00
Mikayla Fischler
98c37caecd #460 cleanup 2024-04-08 23:59:16 -04:00
Mikayla Fischler
cc50e4a020 #460 removed coordinator connecting grace period 2024-04-07 22:33:48 -04:00
Mikayla Fischler
f734c4307b #460 exit on UI crash 2024-04-07 20:55:07 -04:00
Mikayla Fischler
45573be8c7 #460 renderer logging 2024-04-07 20:47:31 -04:00
Mikayla Fischler
eab1ffbe03 #460 cleanup and moved date and time update to be behind UI ready rather than linked 2024-04-07 20:24:27 -04:00
Mikayla Fischler
1504247b33 #460 added yields throughout main UI rendering 2024-04-07 20:21:57 -04:00
Mikayla Fischler
92a4277f73 #460 moved connect/disconnect/resize to render thread 2024-04-07 19:54:22 -04:00
Mikayla Fischler
31a663e86b #460 threaded coordinator and moved UI start to render thread 2024-04-07 19:37:06 -04:00
Mikayla Fischler
5c0e2c32ee #461 prevent log spam in standalone mode when RPS trips continuously 2024-04-07 18:04:11 -04:00
Mikayla Fischler
1d7104ae74 Merge branch 'devel' into pocket-alpha-dev 2024-03-31 13:35:23 -04:00
Mikayla
598f6f08af Merge branch 'devel' into pocket-alpha-dev 2024-03-13 13:55:51 +00:00
Mikayla
f0f2aadf53 #200 work on pocket comms 2024-03-07 17:27:25 +00:00
Mikayla
ce37a672a3 Merge branch 'devel' into pocket-alpha-dev 2024-03-07 16:21:37 +00:00
Mikayla Fischler
526a54903e #410 moved diagnostic apps to main app list and added app page nav 2024-01-15 17:40:43 -05:00
Mikayla Fischler
e1a632dcc7 Merge branch 'devel' into pocket-alpha-dev 2024-01-14 14:21:09 -05:00
Mikayla Fischler
d5d818e625 pocket signal bars 2024-01-14 14:20:59 -05:00
Mikayla Fischler
03de90c3d8 fixes to page navigation nav_up 2023-12-31 12:58:24 -05:00
Mikayla Fischler
99096e0fc9 Merge branch 'devel' into pocket-alpha-dev 2023-12-30 22:58:53 -05:00
Mikayla Fischler
0395aa95b6 indicate pocket app is a work in progress 2023-12-22 22:30:22 -05:00
Mikayla Fischler
c624baed2c #395 fixes to new pocket navigation 2023-12-22 12:46:36 -05:00
Mikayla
1a40321c0f #395 pocket navigation system 2023-12-22 16:12:47 +00:00
58 changed files with 3167 additions and 1043 deletions

View File

@@ -6,9 +6,9 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=main&label=main)
![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/MikaylaFischler/cc-mek-scada/check.yml?branch=devel&label=devel)
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
### Join [the Discord](https://discord.gg/R9NSCkhcwt)!
![Discord](https://img.shields.io/discord/1129075839288496259)
![Discord](https://img.shields.io/discord/1129075839288496259?logo=Discord&logoColor=white&label=discord)
## 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.

View File

@@ -1449,9 +1449,11 @@ function configurator.configure(start_code, message)
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_mon_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_mon_list()
elseif event == "monitor_resize" then

View File

@@ -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)

View File

@@ -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(),
@@ -226,6 +228,8 @@ function iocontrol.init(conf, comms, temp_scale)
---@class ioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = { boilers = {}, turbines = {} },
num_boilers = 0,
num_turbines = 0,
@@ -279,18 +283,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
@@ -317,12 +321,14 @@ function iocontrol.init(conf, comms, temp_scale)
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
end
-- create tank tables
@@ -376,6 +382,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 +598,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 +649,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 +670,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
@@ -869,6 +901,7 @@ function iocontrol.update_unit_statuses(statuses)
end
if #reactor_status == 0 then
unit.connected = false
unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
@@ -928,6 +961,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish(key, val)
end
end
unit.connected = true
else
log.debug(log_header .. "reactor status length mismatch")
valid = false
@@ -942,7 +977,10 @@ function iocontrol.update_unit_statuses(statuses)
local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[id] == nil then
local connected = rtu_statuses.boilers[id] ~= nil
unit.rtu_hw.boilers[id].connected = connected
if not connected then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
@@ -954,6 +992,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.rtu_hw.boilers[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
@@ -985,7 +1024,10 @@ function iocontrol.update_unit_statuses(statuses)
local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[id] == nil then
local connected = rtu_statuses.turbines[id] ~= nil
unit.rtu_hw.turbines[id].connected = connected
if not connected then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
@@ -997,6 +1039,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.rtu_hw.turbines[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted

View File

@@ -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

View File

@@ -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

View File

@@ -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,44 @@ 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_UNIT then
if pkt.length == 1 and type(pkt.data[1]) == "number" then
local u = db.units[pkt.data[1]] ---@type ioctl_unit
if u then
local data = {
u.unit_id,
u.connected,
u.rtu_hw,
u.alarms,
u.annunciator,
u.reactor_data,
u.boiler_data_tbl,
u.turbine_data_tbl,
u.tank_data_tbl
}
_send(CRDN_TYPE.API_GET_UNIT, data)
end
end
else
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
end

View File

@@ -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.6"
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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -183,7 +183,7 @@ local function init(parent, id)
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
@@ -352,6 +352,8 @@ local function init(parent, id)
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end
util.nop()
----------------------
-- reactor controls --
----------------------

View File

@@ -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 --
---------

View File

@@ -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}

View File

@@ -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

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "2.2.2"
core.version = "2.2.4"
core.flasher = flasher
core.events = events

View File

@@ -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
@@ -196,6 +198,9 @@ function element.new(args, child_offset_x, child_offset_y)
---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil, 1, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
self.offset_y = offset_y

View 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

View File

@@ -30,7 +30,7 @@ local function app_button(args)
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4
args.width = 5
args.width = 7
-- create new graphics element base object
local e = element.new(args)
@@ -46,7 +46,7 @@ local function app_button(args)
end
-- draw icon
e.w_set_cur(1, 1)
e.w_set_cur(2, 1)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write("\x9f\x83\x83\x83")
@@ -55,16 +55,16 @@ local function app_button(args)
e.w_write("\x90")
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_set_cur(1, 2)
e.w_set_cur(2, 2)
e.w_write("\x95 ")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x95")
e.w_set_cur(1, 3)
e.w_set_cur(2, 3)
e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text
e.w_set_cur(3, 2)
e.w_set_cur(4, 2)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write(args.text)

View File

@@ -8,13 +8,7 @@ local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_tab
---@field char string character identifier
---@field color cpair tab colors (fg/bg)
---@class sidebar_args
---@field tabs table sidebar tab options
---@field callback function function to call on tab change
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
@@ -27,21 +21,16 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@param args sidebar_args
---@return graphics_element element, element_id id
local function sidebar(args)
element.assert(type(args.tabs) == "table", "tabs is a required field")
element.assert(#args.tabs > 0, "at least one tab is required")
element.assert(type(args.callback) == "function", "callback is a required field")
args.width = 3
-- create new graphics element base object
local e = element.new(args)
element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
-- default to 1st tab
e.value = 1
local was_pressed = false
local tabs = {}
-- show the button state
---@param pressed? boolean if the currently selected tab should appear as actively pressed
@@ -51,10 +40,18 @@ local function sidebar(args)
was_pressed = pressed
pressed_idx = pressed_idx or e.value
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type sidebar_tab
-- clear
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
for y = 1, e.frame.h do
e.w_set_cur(1, y)
e.w_write(" ")
end
local y = ((i - 1) * 3) + 1
-- draw tabs
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
local y = tab.y_start
e.w_set_cur(1, y)
@@ -66,13 +63,29 @@ local function sidebar(args)
e.w_set_bkg(tab.color.bkg)
end
e.w_write(" ")
e.w_set_cur(1, y + 1)
if e.value == i then
e.w_write(" " .. tab.char .. "\x10")
else e.w_write(" " .. tab.char .. " ") end
e.w_set_cur(1, y + 2)
e.w_write(" ")
if tab.tall then
e.w_write(" ")
e.w_set_cur(1, y + 1)
end
e.w_write(tab.label)
if tab.tall then
e.w_set_cur(1, y + 2)
e.w_write(" ")
end
end
end
-- determine which tab was pressed
---@param y integer y coordinate
local function find_tab(y)
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
if y >= tab.y_start and y <= tab.y_end then
return i
end
end
end
@@ -81,23 +94,25 @@ local function sidebar(args)
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
local cur_idx = math.ceil(event.current.y / 3)
local ini_idx = math.ceil(event.initial.y / 3)
local cur_idx = find_tab(event.current.y)
local ini_idx = find_tab(event.initial.y)
local tab = tabs[cur_idx]
if args.tabs[cur_idx] ~= nil then
-- handle press if a callback was provided
if tab ~= nil and type(tab.callback) == "function" then
if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx
draw(true)
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value)
tab.callback()
elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx)
elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx
draw(false)
args.callback(e.value)
tab.callback()
else draw(false) end
end
elseif event.type == MOUSE_CLICK.UP then
@@ -113,6 +128,35 @@ local function sidebar(args)
draw(false)
end
-- update the sidebar navigation options
---@param items table sidebar entries
function e.on_update(items)
local next_y = 1
tabs = {}
for i = 1, #items do
local item = items[i]
local height = util.trinary(item.tall, 3, 1)
---@class sidebar_tab
local entry = {
y_start = next_y, ---@type integer
y_end = next_y + height - 1, ---@type integer
tall = item.tall, ---@type boolean
label = item.label, ---@type string
color = item.color, ---@type cpair
callback = item.callback ---@type function|nil
}
next_y = next_y + height
tabs[i] = entry
end
draw()
end
-- element redraw
e.redraw = draw

View File

@@ -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

View File

@@ -9,7 +9,7 @@ local element = require("graphics.element")
---@class icon_indicator_args
---@field label string indicator label
---@field states table state color and symbol table
---@field value? integer default state, defaults to 1
---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1)
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
@@ -33,6 +33,7 @@ local function icon(args)
local e = element.new(args)
e.value = args.value or 1
if e.value == true then e.value = 2 end
-- state blit strings
local state_blit_cmds = {}
@@ -47,8 +48,11 @@ local function icon(args)
end
-- on state change
---@param new_state integer indicator state
---@param new_state integer|boolean indicator state
function e.on_update(new_state)
new_state = new_state or 1
if new_state == true then new_state = 2 end
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.w_set_cur(1, 1)
@@ -56,7 +60,7 @@ local function icon(args)
end
-- set indicator state
---@param val integer indicator state
---@param val integer|boolean indicator state
function e.set_value(val) e.on_update(val) end
-- element redraw

View 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

View File

@@ -2,19 +2,20 @@
-- 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 util = require("scada-common.util")
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
---@todo nominal trip time is ping (0ms to 10ms usually)
local WARN_TT = 40
local HIGH_TT = 80
local iocontrol = {}
---@class pocket_ioctl
local io = {
ps = psil.create()
}
---@enum POCKET_LINK_STATE
local LINK_STATE = {
UNLINKED = 0,
@@ -23,23 +24,203 @@ 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
function io.nav.set_sidebar(sidebar)
self.sidebar = sidebar
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 = {
loaded = false,
load = nil,
cur_page = nil, ---@type nav_tree_page
pane = pane,
paned_pages = {},
sidebar_items = {}
}
app.load = function () app.loaded = true end
-- 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
function app.set_sidebar(items)
app.sidebar_items = items
if self.sidebar then self.sidebar.update(items) end
end
-- function to run on initial load into memory
---@param on_load function callback
function app.set_on_load(on_load)
app.load = function ()
on_load()
app.loaded = true
end
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 and app.cur_page == nil then
app.cur_page = page
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)
local app = self.apps[app_id] ---@type pocket_app
if app then
if not app.loaded then app.load() end
self.cur_app = app_id
self.pane.set_value(app_id)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
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 = {}
@@ -77,28 +258,514 @@ function iocontrol.init_core(comms)
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 = {}
-- API access
---@class pocket_ioctl_api
io.api = {
get_unit = function (unit) comms.api__get_unit(unit) end
}
-- 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,
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_dc = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
gen_fault = 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 = {}
}
-- create induction and SPS tables (currently only 1 of each is supported)
table.insert(io.facility.induction_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, {})
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
table.insert(io.facility.tank_ps_tbl, psil.create())
table.insert(io.facility.tank_data_tbl, {})
end
end
-- create unit data structures
io.units = {}
for i = 1, conf.num_units do
---@class pioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = {},
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
control_state = false,
burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(),
sna_peak_rate = 0.0,
sna_max_rate = 0.0,
sna_out_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
-- auto control group
a_group = 0,
---@type alarms
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
}
-- on other facility modes, overwrite unit TANK option with facility tank defs
if io.facility.tank_mode ~= 0 then
entry.has_tank = conf.cooling.fac_tank_defs[i] > 0
end
-- create boiler tables
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
end
-- create tank tables
if io.facility.tank_defs[i] == 1 then
table.insert(entry.tank_ps_tbl, psil.create())
table.insert(entry.tank_data_tbl, {})
end
entry.num_boilers = #entry.boiler_data_tbl
entry.num_turbines = #entry.turbine_data_tbl
table.insert(io.units, entry)
end
end
-- set network link state
---@param state POCKET_LINK_STATE
function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end
---@param sv_addr integer? supervisor address if linked
---@param api_addr integer? coordinator address if linked
function iocontrol.report_link_state(state, sv_addr, api_addr)
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
if state == LINK_STATE.LINKED then
io.ps.publish("sv_addr", sv_addr)
io.ps.publish("api_addr", api_addr)
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
-- update unit status data from API_GET_UNIT
---@param data table
function iocontrol.record_unit_data(data)
if type(data[1]) == "number" and io.units[data[1]] then
local unit = io.units[data[1]] ---@type pioctl_unit
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.alarms = data[4]
--#region Annunciator
unit.annunciator = data[5]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
-- split up online arrays
local every = true
for id = 1, #val do
every = every and val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch or anc.MaxWaterReturnFeed
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[6]
local control_status = 1
local reactor_status = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
else rps_status = 4 end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then
reactor_status = 3
else
reactor_status = 4
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
unit.boiler_data_tbl = data[7]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local boiler_status = 1
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
elseif boiler.formed then
boiler_status = 4
else
boiler_status = 2
end
end
ps.publish("BoilerStatus", boiler_status)
end
unit.turbine_data_tbl = data[8]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local turbine_status = 1
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
elseif turbine.formed then
turbine_status = 4
else
turbine_status = 2
end
end
ps.publish("TurbineStatus", turbine_status)
end
unit.tank_data_tbl = data[9]
end
end
-- get the IO controller database
function iocontrol.get_db() return io end

View File

@@ -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
@@ -118,6 +119,20 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.api.seq_num = self.api.seq_num + 1
end
-- send an API packet to the coordinator
---@param msg_type CRDN_TYPE
---@param msg table
local function _send_api(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.crdn_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable())
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1
end
-- attempt supervisor connection establishment
local function _send_sv_establish()
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
@@ -125,7 +140,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
@@ -191,7 +206,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
end
else
-- linked, all good!
iocontrol.report_link_state(LINK_STATE.LINKED)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
end
end
@@ -214,6 +229,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end
-- coordinator get unit data
function public.api__get_unit(unit)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
end
-- parse a packet
---@param side string
---@param sender integer
@@ -246,6 +266,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 +307,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 +316,27 @@ 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_UNIT then
if _check_length(packet, 9) then
iocontrol.record_unit_data(packet.data)
end
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 +344,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 +357,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, self.sv.addr, self.api.addr)
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 +402,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 +442,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 +450,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 +464,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 +506,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
@@ -453,7 +519,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.sv.addr = src_addr
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
end
@@ -474,15 +540,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 +562,4 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
return public
end
return pocket

View File

@@ -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.9.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

View File

@@ -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

View File

@@ -0,0 +1,26 @@
--
-- 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}
TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)}
end
return create_pages

145
pocket/ui/apps/sys_apps.lua Normal file
View File

@@ -0,0 +1,145 @@
--
-- System Apps
--
local comms = require("scada-common.comms")
local util = require("scada-common.util")
local lockbox = require("lockbox")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
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 nt_page = about_app.new_page(about_page, 2)
local fw_page = about_app.new_page(about_page, 3)
local hw_page = about_app.new_page(about_page, 4)
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="Network >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=nt_page.nav_to}
PushButton{parent=about,x=2,y=4,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to}
PushButton{parent=about,x=2,y=5,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to}
--#region Network Details
local config = pocket.config
local nt_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",height=1,alignment=ALIGN.CENTER}
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
---@diagnostic disable-next-line: undefined-field
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel),height=1,alignment=ALIGN.LEFT}
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Supervisor Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local sv = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Coordinator Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local coord = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
sv.register(db.ps, "sv_addr", function (addr) sv.set_value(util.c(addr, ":", config.SVR_Channel)) end)
coord.register(db.ps, "api_addr", function (addr) coord.set_value(util.c(addr, ":", config.CRD_Channel)) end)
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Message Authentication",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None")
TextBox{parent=nt_div,x=2,text=auth,height=1,alignment=ALIGN.LEFT}
--#endregion
--#region Firmware Versions
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}
--#endregion
--#region Host Versions
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}
--#endregion
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
about_app.set_root_pane(root_pane)
end
return create_pages

View File

@@ -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="WIP 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,29 @@ 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)
}
}
home_page(page_div)
unit_page(page_div)
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) }
diag_apps(page_div)
sys_apps(page_div)
dummy_app(page_div)
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
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
db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()})
db.nav.set_sidebar(Sidebar{parent=main_pane,x=1,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)})
page_pane.set_value(page)
end
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}
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar}
db.nav.open_app(iocontrol.APP_ID.ROOT)
--#endregion
end
return init

View File

@@ -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

View File

@@ -1,21 +1,64 @@
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
app.set_sidebar({
{ label = " #\x10", tall = true, color = core.cpair(colors.black, colors.green), callback = function () open(APP_ID.ROOT) end }
})
local active_fg_bg = cpair(colors.white,colors.gray)
App{parent=apps_1,x=2,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=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",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=2,y=7,text="\x17",title="Process",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=9,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=16,y=7,text="\x08",title="Devices",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=2,y=12,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=9,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=2,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=9,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=16,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

View File

@@ -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

View File

@@ -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

View File

@@ -1,20 +1,319 @@
-- local style = require("pocket.ui.style")
--
-- Unit Overview Page
--
local core = require("graphics.core")
local util = require("scada-common.util")
-- local log = require("scada-common.log")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
local iocontrol = require("pocket.iocontrol")
-- local cpair = core.cpair
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
-- local RadIndicator = require("graphics.elements.indicators.rad")
-- local VerticalBar = require("graphics.elements.indicators.vbar")
local PushButton = require("graphics.elements.controls.push_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local basic_states = {
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
{ color = cpair(colors.black, colors.red), symbol = "-" },
{ color = cpair(colors.black, colors.yellow), symbol = "\x1e" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
local mode_states = {
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
{ color = cpair(colors.black, colors.red), symbol = "-" },
{ color = cpair(colors.black, colors.green), symbol = "+" },
{ color = cpair(colors.black, colors.purple), symbol = "A" }
}
local emc_ind_s = {
{ color = cpair(colors.black, colors.gray), symbol = "-" },
{ color = cpair(colors.black, colors.white), symbol = "\x07" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
local red_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.red), symbol = "-" }
}
local yel_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.yellow), symbol = "-" }
}
-- 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)
TextBox{parent=main,y=2,text="Units App",height=1,alignment=ALIGN.CENTER}
TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER}
local btn_fg_bg = cpair(colors.yellow, colors.black)
local btn_active = cpair(colors.white, colors.black)
-- local label = cpair(colors.lightGray, colors.black)
local nav_links = {}
local function set_sidebar(id)
-- local unit = db.units[id] ---@type pioctl_unit
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(iocontrol.APP_ID.ROOT) end },
{ label = "U-" .. id, color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(id) end },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = nav_links[id].alarm },
{ label = "RPS", tall = true, color = core.cpair(colors.black, colors.cyan), callback = nav_links[id].rps },
-- { label = " R ", color = core.cpair(colors.black, colors.lightGray), callback = function () end },
{ label = "RCS", tall = true, color = core.cpair(colors.black, colors.blue), callback = nav_links[id].rcs },
}
-- for i = 1, unit.num_boilers do
-- table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightBlue), callback = function () end })
-- end
-- for i = 1, unit.num_turbines do
-- table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.white), callback = function () end })
-- end
app.set_sidebar(list)
end
local function load()
local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2}
local panes = {}
local active_unit = 1
-- create all page divs
for _ = 1, db.facility.num_units do
local div = Div{parent=page_div}
table.insert(panes, div)
table.insert(nav_links, {})
end
-- previous unit
local function prev(x)
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
app.switcher(active_unit)
set_sidebar(active_unit)
end
-- next unit
local function next(x)
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
app.switcher(active_unit)
set_sidebar(active_unit)
end
for i = 1, db.facility.num_units do
local u_div = panes[i] ---@type graphics_element
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- refresh data callback, every 500ms it will re-send the query
local last_update = 0
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_unit(i)
last_update = util.time_ms()
end
end
--#region Main Unit Overview
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,height=1,alignment=ALIGN.CENTER}
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
local type = util.trinary(unit.num_boilers > 0, "Sodium Cooled Reactor", "Boiling Water Reactor")
TextBox{parent=u_div,y=3,text=type,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
local lu_col = cpair(colors.lightGray, colors.lightGray)
local text_fg = cpair(colors.white, colors._INHERIT)
local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Rate",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit="K",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states}
rate.register(u_ps, "act_burn_rate", rate.update)
temp.register(u_ps, "temp", temp.update)
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
u_div.line_break()
local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states}
local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states}
rct.register(u_ps, "U_ReactorStatus", rct.update)
rps.register(u_ps, "U_RPS", rps.update)
u_div.line_break()
local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states}
rcs.register(u_ps, "U_RCS", rcs.update)
for b = 1, unit.num_boilers do
local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states}
blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update)
end
for t = 1, unit.num_turbines do
local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states}
tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update)
end
--#endregion
--#region Alarms Tab
local alm_div = Div{parent=page_div}
table.insert(panes, alm_div)
local alm_page = app.new_page(u_page, #panes)
alm_page.tasks = { update }
nav_links[i].alarm = alm_page.nav_to
TextBox{parent=alm_div,y=1,text="Unit Alarms",height=1,alignment=ALIGN.CENTER}
TextBox{parent=alm_div,y=3,text="work in progress",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
--#endregion
--#region RPS Tab
local rps_div = Div{parent=page_div}
table.insert(panes, rps_div)
local rps_page = app.new_page(u_page, #panes)
rps_page.tasks = { update }
nav_links[i].rps = rps_page.nav_to
TextBox{parent=rps_div,y=1,text="Protection System",height=1,alignment=ALIGN.CENTER}
local r_trip = IconIndicator{parent=rps_div,y=3,label="RPS Trip",states=basic_states}
r_trip.register(u_ps, "U_RPS", r_trip.update)
local r_mscrm = IconIndicator{parent=rps_div,y=5,label="Manual SCRAM",states=red_ind_s}
local r_ascrm = IconIndicator{parent=rps_div,label="Automatic SCRAM",states=red_ind_s}
local rps_tmo = IconIndicator{parent=rps_div,label="Timeout",states=yel_ind_s}
local rps_flt = IconIndicator{parent=rps_div,label="PPM Fault",states=yel_ind_s}
local rps_sfl = IconIndicator{parent=rps_div,label="Not Formed",states=red_ind_s}
r_mscrm.register(u_ps, "manual", r_mscrm.update)
r_ascrm.register(u_ps, "automatic", r_ascrm.update)
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
rps_flt.register(u_ps, "fault", rps_flt.update)
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
rps_div.line_break()
local rps_dmg = IconIndicator{parent=rps_div,label="Reactor Damage Hi",states=red_ind_s}
local rps_tmp = IconIndicator{parent=rps_div,label="Temp. Critical",states=red_ind_s}
local rps_nof = IconIndicator{parent=rps_div,label="Fuel Level Lo",states=yel_ind_s}
local rps_exw = IconIndicator{parent=rps_div,label="Waste Level Hi",states=yel_ind_s}
local rps_loc = IconIndicator{parent=rps_div,label="Coolant Lo Lo",states=yel_ind_s}
local rps_exh = IconIndicator{parent=rps_div,label="Heated Coolant Hi",states=yel_ind_s}
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
rps_loc.register(u_ps, "low_cool", rps_loc.update)
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
--#endregion
--#region RCS Tab
local rcs_div = Div{parent=page_div}
table.insert(panes, rcs_div)
local rcs_page = app.new_page(u_page, #panes)
rcs_page.tasks = { update }
nav_links[i].rcs = rcs_page.nav_to
TextBox{parent=rcs_div,y=1,text="Coolant System",height=1,alignment=ALIGN.CENTER}
local r_rtrip = IconIndicator{parent=rcs_div,y=3,label="RCP Trip",states=red_ind_s}
local r_cflow = IconIndicator{parent=rcs_div,label="RCS Flow Lo",states=yel_ind_s}
local r_clow = IconIndicator{parent=rcs_div,label="Coolant Level Lo",states=yel_ind_s}
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
local c_flt = IconIndicator{parent=rcs_div,label="RCS HW Fault",states=yel_ind_s}
local c_emg = IconIndicator{parent=rcs_div,label="Emergency Coolant",states=emc_ind_s}
local c_mwrf = IconIndicator{parent=rcs_div,label="Max Water Return",states=yel_ind_s}
c_flt.register(u_ps, "RCSFault", c_flt.update)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
-- rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Mismatches",height=1,alignment=ALIGN.CENTER,fg_bg=label}
local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s}
local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s}
local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s}
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Aggregate Checks",height=1,alignment=ALIGN.CENTER,fg_bg=label}
if unit.num_boilers > 0 then
local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s}
local hrl = IconIndicator{parent=rcs_div,label="Heating Rate Lo",states=yel_ind_s}
wll.register(u_ps, "U_WaterLevelLow", wll.update)
hrl.register(u_ps, "U_HeatingRateLow", hrl.update)
end
local tospd = IconIndicator{parent=rcs_div,label="TRB Over Speed",states=red_ind_s}
local gtrip = IconIndicator{parent=rcs_div,label="Generator Trip",states=yel_ind_s}
local ttrip = IconIndicator{parent=rcs_div,label="Turbine Trip",states=red_ind_s}
tospd.register(u_ps, "U_TurbineOverSpeed", tospd.update)
gtrip.register(u_ps, "U_GeneratorTrip", gtrip.update)
ttrip.register(u_ps, "U_TurbineTrip", ttrip.update)
--#endregion
end
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
set_sidebar(active_unit)
end
app.set_on_load(load)
return main
end

View File

@@ -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

View File

@@ -25,8 +25,8 @@ local RPS_LIMITS = const.RPS_LIMITS
-- I sure hope the devs don't change this error message, not that it would have safety implications
-- I wish they didn't change it to be like this
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active."
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "Reactor is already active."
---@type plc_config
local config = {}
@@ -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
@@ -307,7 +307,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor SCRAM")
reactor.scram()
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_SCRAM_MSG) then
log.error("RPS: failed reactor SCRAM")
return false
else
@@ -325,7 +325,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor start")
reactor.activate()
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_START_MSG) then
log.error("RPS: failed reactor start")
else
self.reactor_enabled = true

View File

@@ -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.11"
local println = util.println
local println_ts = util.println_ts

View File

@@ -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"))

View File

@@ -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")
@@ -1638,9 +1669,11 @@ function configurator.configure(ask_config)
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.update_peri_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.update_peri_list()
end

View File

@@ -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)

View File

@@ -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.6"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE

View File

@@ -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.2"
---@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_UNIT = 8 -- API: get 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

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -639,6 +658,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
local cmd = message.message
if cmd == PLC_S_CMDS.ENABLE then
-- enable reactor
self.acks.disable = true
if not self.auto_lock then
_send(RPLC_TYPE.RPS_ENABLE, {})
end
@@ -695,6 +715,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.auto_cmd_token = 0
self.ramping_rate = true
self.acks.burn_rate = false
self.acks.disable = true
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
_send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
@@ -702,13 +723,14 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then
-- set automatic burn rate
if self.auto_lock then
cmd.val = math.floor(cmd.val * 100) / 100 -- round to 100ths place
cmd.val = math.floor(cmd.val * 100) / 100 -- round to 100ths place
if cmd.val >= 0 and cmd.val <= self.sDB.mek_struct.max_burn then
self.auto_cmd_token = util.time_ms()
self.commanded_burn_rate = cmd.val
-- this is only for manual control, only retry auto ramps
self.acks.burn_rate = not self.ramping_rate
self.acks.disable = true
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT
_send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })

View File

@@ -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

View File

@@ -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

View File

@@ -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.11"
local println = util.println
local println_ts = util.println_ts

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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")