Compare commits

...

82 Commits

Author SHA1 Message Date
Mikayla Fischler
264edc0030 #592 fixed bug with pocket help page linking navigation 2025-05-10 17:30:54 -04:00
Mikayla Fischler
fcb17ae5e7 show "new!" next to new config fields 2025-05-10 11:26:19 -04:00
Mikayla Fischler
35f82af2e2 fixed CONFIG button coloring 2025-05-10 11:25:52 -04:00
Mikayla
1a2ecd0599 Merge pull request #619 from MikaylaFischler/rtu-redstone-enhancements
604 redstone relay integration
2025-05-09 11:41:53 -04:00
Mikayla
5f8c947105 cleanup and fixes 2025-05-09 15:41:14 +00:00
Mikayla Fischler
41e6d89a4b incremented comms version for RTU advertisement changes 2025-05-07 20:06:04 -04:00
Mikayla Fischler
f01fb62863 #604 updated emergency coolant annunciator logic 2025-05-07 20:05:03 -04:00
Mikayla Fischler
8f6425b814 #604 fixed supervisor bugs with new redstone 2025-05-07 20:04:39 -04:00
Mikayla Fischler
069a7ce0ad #604 front panel updates and hw state tracking fixes 2025-05-07 20:03:48 -04:00
Mikayla Fischler
8eff1c0d76 #604 refresh connections count on saving an interface 2025-05-07 20:03:20 -04:00
Mikayla Fischler
7404e6da31 #604 updated self check for relays and added duplicate input detection 2025-05-07 11:48:32 -04:00
Mikayla Fischler
12ead136a3 #604 configuration of redstone RTUs 2025-05-07 11:27:53 -04:00
Mikayla Fischler
e3dbda3c54 fixed logic for duplicate input detection 2025-05-07 10:42:52 -04:00
Mikayla
41b6a558d5 init RTU gateway UI after checking for modem to prevent that failure making a UI mess 2025-05-05 16:35:44 +00:00
Mikayla Fischler
07bb0f13e3 Merge branch 'devel' into rtu-redstone-enhancements 2025-05-03 09:54:29 -04:00
Mikayla
f1014ce941 Merge pull request #618 from MikaylaFischler/572-audible-alarms-on-facility-radiation
572 audible alarms on facility radiation
2025-04-30 10:27:08 -04:00
Mikayla Fischler
0debbdc167 fixed facility not scram'ing on radiation 2025-04-30 10:25:30 -04:00
Mikayla Fischler
0a26629e20 fixed no_ring_back going to RING_BACK if leaving TRIPPED not ACKED 2025-04-30 10:17:28 -04:00
Mikayla Fischler
9393b1830d restored use of self for unit logic function calls 2025-04-30 10:17:09 -04:00
Mikayla
0df1e48780 reorganized unit logic inclusion to work like facility update 2025-04-29 20:11:12 +00:00
Mikayla
b8c30ba8a4 cleanup 2025-04-29 19:47:29 +00:00
Mikayla
eafd39fa35 #572 added facility radiation alarm 2025-04-29 19:41:52 +00:00
Mikayla
e6f5ab8ef4 #604 reworked supervisor redstone RTU interface 2025-04-29 02:38:42 +00:00
Mikayla
be462db50b #604 new redstone initialization logic 2025-04-29 01:44:52 +00:00
Mikayla Fischler
1dc3d82e59 #604 work on redstone RTU rework 2025-04-27 22:40:42 -04:00
Mikayla Fischler
fa2a6d7786 Merge branch 'devel' into rtu-redstone-enhancements 2025-04-26 15:30:31 -04:00
Mikayla Fischler
04c53c7074 #616 pocket pellet color options 2025-04-26 15:24:50 -04:00
Mikayla Fischler
1af2cdba8d #616 fixes to coordinator pellet color option 2025-04-26 15:21:09 -04:00
Mikayla Fischler
0d7302dc8e #604 start of total rework of redstone RTUs for relay functionalitiy 2025-04-21 22:18:09 -04:00
Mikayla Fischler
48ec973695 #616 added pellet color option to the coordinator 2025-04-21 22:13:58 -04:00
Mikayla Fischler
ee868eb607 #616 updated flow view to match fluid colors not pellet colors 2025-04-20 21:36:40 -04:00
Mikayla
e4da9a62d9 Merge pull request #617 from MikaylaFischler/rtu-redstone-enhancements
redstone invert
2025-04-20 17:41:26 -04:00
Mikayla Fischler
c8910bfc40 clear inverted on analog creation 2025-04-20 17:40:40 -04:00
Mikayla Fischler
d6e3a67562 #484 redstone inversion support 2025-04-20 17:36:16 -04:00
Mikayla Fischler
f7c0a1d97d #484 work on redstone inversion 2025-04-20 11:28:02 -04:00
Mikayla Fischler
bfab2d6af2 #583 fixes for pocket crash display 2025-04-19 18:09:43 -04:00
Mikayla Fischler
ae055a7d99 luacheck fixes and version increment 2025-04-19 00:21:15 -04:00
Mikayla Fischler
592f1110ed define pocket global in craftos-pc environment pocket application 2025-04-19 00:16:43 -04:00
Mikayla Fischler
97875f4e52 #583 graphical crash screens 2025-04-19 00:15:41 -04:00
Mikayla
657261642c Merge pull request #615 from MikaylaFischler/364-rtu-configurator-self-check
364 rtu configurator self check
2025-04-05 21:18:15 -04:00
Mikayla Fischler
0da944c3ea #364 updated scroll height max and reduced duplicate text 2025-04-05 21:17:19 -04:00
Mikayla Fischler
1b692b5b9a #364 fail self check if using a side for both bundled and unbundled redstone 2025-04-05 21:14:33 -04:00
Mikayla Fischler
b4a9366f73 #364 fixes to redstone and peripheral checks 2025-04-05 21:00:16 -04:00
Mikayla Fischler
2b3099ac59 #364 check validity of redstone and peripheral entries and check redstone side/color combos are not repeated 2025-04-04 00:17:40 -04:00
Mikayla Fischler
cd654fb9b8 shorter variable for self.settings in PLC self check 2025-04-03 23:21:31 -04:00
Mikayla Fischler
ad834218c2 #364 check all configured RTU peripherals in self check 2025-04-03 23:21:06 -04:00
Mikayla Fischler
c6a7de2669 #364 base RTU gateway self checks 2025-04-03 23:06:45 -04:00
Mikayla Fischler
d374967cb7 #614 fixed reactor PLC self check when configured as not networked 2025-04-03 22:56:54 -04:00
Mikayla Fischler
1971153dae configurator summary enhancements 2025-02-26 18:38:21 -05:00
Mikayla Fischler
5fc8912590 #480 fixed aux coolant connection to boilers with emergency coolant 2025-02-26 13:08:58 -05:00
Mikayla
122fa1a7a7 Merge pull request #609 from MikaylaFischler/480-auxiliarybackup-water-control
480 auxiliary backup water control
2025-02-25 16:44:23 -05:00
Mikayla Fischler
2b73196130 #480 updated aux coolant logic 2025-02-25 16:43:03 -05:00
Mikayla Fischler
d45f19c8a6 refactor 2025-02-25 15:32:07 -05:00
Mikayla Fischler
a9f68ce3ea Merge branch 'devel' into 480-auxiliarybackup-water-control 2025-02-25 14:53:44 -05:00
Mikayla Fischler
7ab5ea710f additional supervisor config validations 2025-02-25 14:52:05 -05:00
Mikayla Fischler
de41ee56aa #480 auxiliary water coolant 2025-02-25 14:33:25 -05:00
Mikayla Fischler
99ea59a86b #526 coordinator front panel scale to term size 2025-02-16 13:32:08 -05:00
Mikayla Fischler
234652b886 #526 cleanup 2025-02-16 13:21:00 -05:00
Mikayla Fischler
e37e3ba696 #526 supervisor front panel scale to term size 2025-02-16 13:20:23 -05:00
Mikayla Fischler
20b71bead1 #526 RTU gateway front panel scale to term size 2025-02-16 12:51:10 -05:00
Mikayla Fischler
18d093e72d #526 reactor PLC front panel scale to term size 2025-02-16 12:34:06 -05:00
Mikayla Fischler
21eae4932f #607 updated deny message 2025-02-16 11:54:45 -05:00
Mikayla Fischler
9163fb14c4 RTU gateway version increment 2025-02-16 11:45:26 -05:00
Mikayla Fischler
02db01524c Merge branch 'devel' of github.com:MikaylaFischler/cc-mek-scada into devel 2025-02-16 11:44:44 -05:00
Mikayla Fischler
e0d1eb3445 #608 fixed front panel network lights 2025-02-16 11:44:30 -05:00
Mikayla Fischler
7c22c172d5 #607 deny reactor PLC with index out of range 2025-02-16 11:43:32 -05:00
Mikayla
7b29702000 #480 auxiliary coolant control logic 2025-02-11 22:42:52 +00:00
Mikayla
425a6c8775 #480 added auxiliary coolant redstone output 2025-02-11 22:42:07 +00:00
Mikayla
eafcd89aba updated SNA RTU note after #564's changes 2025-02-11 22:40:17 +00:00
Mikayla Fischler
016cd988e1 #564 improved SNA statistic clarity 2025-02-09 16:17:37 -05:00
Mikayla
06a8e3d9ca Merge pull request #603 from MikaylaFischler/589-reboot-recovery
589 reboot recovery
2025-02-09 15:25:48 -05:00
Mikayla Fischler
5f22069ce1 #589 cleanup and fixes 2025-02-09 14:19:06 -05:00
Mikayla Fischler
ecdaf78ed0 #589 moved boot recovery to facility update file 2025-02-09 13:48:20 -05:00
Mikayla Fischler
3b2fb00285 cleanup 2025-02-09 13:37:22 -05:00
Mikayla Fischler
54167e2113 #589 only scram reactor on plc boot if networked 2025-02-09 13:13:18 -05:00
Mikayla Fischler
22cdbc8638 #589 supervisor control reboot recovery 2025-02-09 13:07:36 -05:00
Mikayla Fischler
556331f75b better unit ready check 2025-02-09 13:07:01 -05:00
Mikayla Fischler
40cb9f599a #602 only auto reset units that should be 2025-02-09 13:06:44 -05:00
Mikayla Fischler
cab3427c70 #601 only reset on timeout once per unit per supervisor boot 2025-02-09 12:10:13 -05:00
Mikayla Fischler
4e31b33b09 #601 reset RPS if the triggering condition is a timeout on PLC session establish 2025-02-09 11:59:03 -05:00
Mikayla Fischler
f32855084e #589 WIP reboot recovery 2025-02-08 22:20:00 -05:00
Mikayla
b3cf40a01a #589 initial attempt at reboot recovery 2025-02-08 20:35:04 +00:00
63 changed files with 2232 additions and 882 deletions

View File

@@ -234,19 +234,24 @@ function hmi.create(tool_ctl, main_pane, cfg_sys, divs, style)
TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)} TextBox{parent=crd_cfg,x=1,y=2,text=" Coordinator UI Configuration",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=crd_c_1,x=1,y=1,height=3,text="Configure the UI interface options below if you wish to customize formats."} TextBox{parent=crd_c_1,x=1,y=1,height=2,text="You can customize the UI with the interface options below."}
TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"} TextBox{parent=crd_c_1,x=1,y=4,text="Clock Time Format"}
tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} tool_ctl.clock_fmt = RadioButton{parent=crd_c_1,x=1,y=5,default=util.trinary(ini_cfg.Time24Hour,1,2),options={"24-Hour","12-Hour"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=20,y=4,text="Po/Pu Pellet Color"}
TextBox{parent=crd_c_1,x=39,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
tool_ctl.pellet_color = RadioButton{parent=crd_c_1,x=20,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po (Mek 10.4+)"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"} TextBox{parent=crd_c_1,x=1,y=8,text="Temperature Scale"}
tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} tool_ctl.temp_scale = RadioButton{parent=crd_c_1,x=1,y=9,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=crd_c_1,x=24,y=8,text="Energy Scale"} TextBox{parent=crd_c_1,x=20,y=8,text="Energy Scale"}
tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=24,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} tool_ctl.energy_scale = RadioButton{parent=crd_c_1,x=20,y=9,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts() local function submit_ui_opts()
tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1 tmp_cfg.Time24Hour = tool_ctl.clock_fmt.get_value() == 1
tmp_cfg.GreenPuPellet = tool_ctl.pellet_color.get_value() == 1
tmp_cfg.TempScale = tool_ctl.temp_scale.get_value() tmp_cfg.TempScale = tool_ctl.temp_scale.get_value()
tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value() tmp_cfg.EnergyScale = tool_ctl.energy_scale.get_value()
main_pane.set_value(7) main_pane.set_value(7)

View File

@@ -380,6 +380,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
try_set(tool_ctl.num_units, ini_cfg.UnitCount) try_set(tool_ctl.num_units, ini_cfg.UnitCount)
try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView) try_set(tool_ctl.dis_flow_view, ini_cfg.DisableFlowView)
try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume) try_set(tool_ctl.s_vol, ini_cfg.SpeakerVolume)
try_set(tool_ctl.pellet_color, ini_cfg.GreenPuPellet)
try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2)) try_set(tool_ctl.clock_fmt, tri(ini_cfg.Time24Hour, 1, 2))
try_set(tool_ctl.temp_scale, ini_cfg.TempScale) try_set(tool_ctl.temp_scale, ini_cfg.TempScale)
try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale) try_set(tool_ctl.energy_scale, ini_cfg.EnergyScale)
@@ -528,6 +529,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val)) 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 = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw]) val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then elseif f[1] == "EnergyScale" then
@@ -550,7 +553,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate alternate = not alternate
if string.len(val) > val_max_w then if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width) local lines = util.strwrap(val, inner_width)
height = #lines + 1 height = #lines + 1
end end

View File

@@ -35,7 +35,8 @@ local changes = {
{ "v1.2.4", { "Added temperature scale options" } }, { "v1.2.4", { "Added temperature scale options" } },
{ "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.2.12", { "Added main UI theme", "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } }, { "v1.3.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.5.1", { "Added energy scale options" } } { "v1.5.1", { "Added energy scale options" } },
{ "v1.6.13", { "Added option for Po/Pu pellet green/cyan pairing" } }
} }
---@class crd_configurator ---@class crd_configurator
@@ -77,6 +78,7 @@ local tool_ctl = {
-- settings elements from hmi -- settings elements from hmi
dis_flow_view = nil, ---@type Checkbox dis_flow_view = nil, ---@type Checkbox
s_vol = nil, ---@type NumberField s_vol = nil, ---@type NumberField
pellet_color = nil, ---@type RadioButton
clock_fmt = nil, ---@type RadioButton clock_fmt = nil, ---@type RadioButton
temp_scale = nil, ---@type RadioButton temp_scale = nil, ---@type RadioButton
energy_scale = nil, ---@type RadioButton energy_scale = nil, ---@type RadioButton
@@ -95,6 +97,7 @@ local tmp_cfg = {
UnitCount = 1, UnitCount = 1,
SpeakerVolume = 1.0, SpeakerVolume = 1.0,
Time24Hour = true, Time24Hour = true,
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE EnergyScale = 1, ---@type ENERGY_SCALE
DisableFlowView = false, DisableFlowView = false,
@@ -129,6 +132,7 @@ local fields = {
{ "UnitDisplays", "Unit Monitors", {} }, { "UnitDisplays", "Unit Monitors", {} },
{ "SpeakerVolume", "Speaker Volume", 1.0 }, { "SpeakerVolume", "Speaker Volume", 1.0 },
{ "Time24Hour", "Use 24-hour Time Format", true }, { "Time24Hour", "Use 24-hour Time Format", true },
{ "GreenPuPellet", "Pellet Colors", false },
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN }, { "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE }, { "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false }, { "DisableFlowView", "Disable Flow Monitor (legacy, discouraged)", false },

View File

@@ -38,6 +38,7 @@ function coordinator.load_config()
config.UnitCount = settings.get("UnitCount") config.UnitCount = settings.get("UnitCount")
config.SpeakerVolume = settings.get("SpeakerVolume") config.SpeakerVolume = settings.get("SpeakerVolume")
config.Time24Hour = settings.get("Time24Hour") config.Time24Hour = settings.get("Time24Hour")
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale") config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale") config.EnergyScale = settings.get("EnergyScale")
@@ -67,6 +68,7 @@ function coordinator.load_config()
cfv.assert_type_int(config.UnitCount) cfv.assert_type_int(config.UnitCount)
cfv.assert_range(config.UnitCount, 1, 4) cfv.assert_range(config.UnitCount, 1, 4)
cfv.assert_type_bool(config.Time24Hour) cfv.assert_type_bool(config.Time24Hour)
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale) cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4) cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale) cfv.assert_type_int(config.EnergyScale)
@@ -380,6 +382,18 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end end
-- send the resume ready state to the supervisor
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
mode, burn_target, charge_target, gen_target, limits
})
end
-- send a facility command -- send a facility command
---@param cmd FAC_COMMAND command ---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode) ---@param option any? optional option options for the optional options (like waste mode)

View File

@@ -164,6 +164,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
num_turbines = 0, num_turbines = 0,
num_snas = 0, num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection, has_tank = conf.cooling.r_cool[i].TankConnection,
aux_coolant = conf.cooling.aux_coolant[i],
status_lines = { "", "" }, status_lines = { "", "" },
@@ -1214,7 +1215,7 @@ function iocontrol.update_unit_statuses(statuses)
local valve_states = status[6] local valve_states = status[6]
if type(valve_states) == "table" then if type(valve_states) == "table" then
if #valve_states == 5 then if #valve_states == 6 then
unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0) unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0)
unit.unit_ps.publish("V_pu_state", valve_states[1] == 2) unit.unit_ps.publish("V_pu_state", valve_states[1] == 2)
unit.unit_ps.publish("V_po_conn", valve_states[2] > 0) unit.unit_ps.publish("V_po_conn", valve_states[2] > 0)
@@ -1225,6 +1226,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("V_am_state", valve_states[4] == 2) unit.unit_ps.publish("V_am_state", valve_states[4] == 2)
unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0) unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0)
unit.unit_ps.publish("V_emc_state", valve_states[5] == 2) unit.unit_ps.publish("V_emc_state", valve_states[5] == 2)
unit.unit_ps.publish("V_aux_conn", valve_states[6] > 0)
unit.unit_ps.publish("V_aux_state", valve_states[6] == 2)
else else
log.debug(log_header .. "valve states length mismatch") log.debug(log_header .. "valve states length mismatch")
valid = false valid = false

View File

@@ -139,6 +139,11 @@ function process.init(iocontrol, coord_comms)
log.info("PROCESS: loaded priority groups settings") log.info("PROCESS: loaded priority groups settings")
end end
-- report to the supervisor all initial configuration data has been sent
-- startup resume can occur if needed
local p = ctl_proc
pctl.comms.send_ready(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
end end
-- create a handle to process control for usage of commands that get acknowledgements -- create a handle to process control for usage of commands that get acknowledgements

View File

@@ -137,7 +137,7 @@ function renderer.try_start_fp()
if not engine.fp_ready then if not engine.fp_ready then
-- show front panel view on terminal -- show front panel view on terminal
status, msg = pcall(function () status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root} engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays) panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end) end)

View File

@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads") local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.6.4" local COORDINATOR_VERSION = "v1.6.15"
local CHUNK_LOAD_DELAY_S = 30.0 local CHUNK_LOAD_DELAY_S = 30.0

View File

@@ -28,6 +28,8 @@ local function init(parent, id)
local ps = iocontrol.get_db().fp.ps local ps = iocontrol.get_db().fp.ps
local term_w, _ = term.getSize()
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright}
@@ -43,9 +45,9 @@ local function init(parent, id)
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg} local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value) pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4} TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg} local pkt_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg} TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update) pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor) pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)

View File

@@ -325,7 +325,7 @@ local function new_view(root, x, y)
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8} TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8}
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht} local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6} local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.get_waste().states_abbrv,value=1,min_width=6}
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update) a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update) waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
@@ -339,11 +339,11 @@ local function new_view(root, x, y)
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg} TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,x=1,y=2,fg_bg=cutout_fg_bg}
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3} local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17} local status = StateIndicator{parent=rect,x=2,y=1,states=style.get_waste().states,value=1,min_width=17}
status.register(facility.ps, "current_waste_product", status.update) 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 waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value) waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)

View File

@@ -398,7 +398,7 @@ local function init(parent, id)
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49} local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1} local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6} local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.get_waste().unit_opts,callback=unit.set_waste,min_width=6}
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)

View File

@@ -59,7 +59,7 @@ local function make(parent, x, y, wide, unit_id)
local tank_conns = facility.tank_conns local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 5) local v_start = 1 + ((unit.unit_id - 1) * 6)
local prv_start = 1 + ((unit.unit_id - 1) * 3) local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" } local v_fields = { "pu", "po", "pl", "am" }
local v_names = { local v_names = {
@@ -94,11 +94,21 @@ local function make(parent, x, y, wide, unit_id)
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true)) table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true)) table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true)) table.insert(rc_pipes, pipe(_wide(46, 39), 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true)) table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
end
else else
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true)) table.insert(rc_pipes, pipe(0, 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true)) table.insert(rc_pipes, pipe(0, 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
end
end end
if unit.has_tank then if unit.has_tank then
@@ -169,12 +179,12 @@ local function make(parent, x, y, wide, unit_id)
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true), pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true), pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true), pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.cyan, true),
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true), pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.green, true),
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true), pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.green, true),
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true), pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.green, true),
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true), pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.green, true),
pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true), pipe(_wide(108, 94), 1, _wide(132, 110), 6, waste_c, true, true),
pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true), pipe(_wide(108, 94), 4, _wide(111, 95), 1, waste_c, true, true),
@@ -222,17 +232,21 @@ local function make(parent, x, y, wide, unit_id)
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b") _machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray} TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright} local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=8,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn} local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7} local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17} TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17} TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17} local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end) sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update) sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update) sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update) sna_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update) sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root return root

View File

@@ -268,7 +268,7 @@ local function init(main)
for i = 1, facility.num_units do for i = 1, facility.num_units do
local y_offset = y_ofs(i) local y_offset = y_ofs(i)
unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i) unit_flow(main, flow_x, 5 + y_offset, #emcool_pipes == 0, i)
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true)) table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.green, true, true))
util.nop() util.nop()
end end
@@ -286,7 +286,7 @@ local function init(main)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2} TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn} local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht} local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_emc_conn", conn.update) conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
@@ -294,6 +294,35 @@ local function init(main)
end end
end end
------------------------------
-- auxiliary coolant valves --
------------------------------
for i = 1, facility.num_units do
if units[i].aux_coolant then
local vx
local vy = 3 + y_ofs(i)
if #emcool_pipes == 0 then
vx = util.trinary(units[i].num_boilers == 0, 36, 79)
else
local em_water = tank_types[tank_conns[i]] == COOLANT_TYPE.WATER
vx = util.trinary(units[i].num_boilers == 0, 58, util.trinary(units[i].has_tank and em_water, 94, 91))
end
PipeNetwork{parent=main,x=vx-6,y=vy,pipes={pipe(0,1,9,0,colors.blue,true)},bg=style.theme.bg}
TextBox{parent=main,x=vx,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
TextBox{parent=main,x=vx+5,y=vy,text="\x1b",fg_bg=cpair(colors.blue,text_col.bkg),width=1}
local conn = IndicatorLight{parent=main,x=vx-3,y=vy+1,label=util.sprintf("PV%02d-AUX", i * 6),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=vx-3,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_aux_conn", conn.update)
open.register(units[i].unit_ps, "V_aux_state", open.update)
end
end
------------------- -------------------
-- dynamic tanks -- -- dynamic tanks --
------------------- -------------------

View File

@@ -39,6 +39,8 @@ local led_grn = style.led_grn
local function init(panel, num_units) local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps local ps = iocontrol.get_db().fp.ps
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header} TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
local page_div = Div{parent=panel,x=1,y=3} local page_div = Div{parent=panel,x=1,y=3}
@@ -61,7 +63,7 @@ local function init(panel, num_units)
local modem = LED{parent=system,label="MODEM",colors=led_grn} local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update) network.register(ps, "link_state", network.update)
else else
@@ -131,9 +133,9 @@ local function init(panel, num_units)
-- about footer -- about footer
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg} local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"} local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"} local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -145,7 +147,7 @@ local function init(panel, num_units)
-- API page -- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true} local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1} -- padding local _ = Div{parent=api_list,height=1} -- padding
-- assemble page panes -- assemble page panes

View File

@@ -2,16 +2,20 @@
-- Graphics Style Options -- Graphics Style Options
-- --
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
local themes = require("graphics.themes") local themes = require("graphics.themes")
local coordinator = require("coordinator.coordinator")
---@class crd_style ---@class crd_style
local style = {} local style = {}
local cpair = core.cpair local cpair = core.cpair
local config = coordinator.config
-- front panel styling -- front panel styling
style.fp_theme = themes.sandstone style.fp_theme = themes.sandstone
@@ -223,27 +227,34 @@ style.sps = {
} }
} }
style.waste = { -- get waste styling, which depends on the configuration
-- auto waste processing states ---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: { text: string, fg_bg: cpair, active_fg_bg:cpair } }
states = { function style.get_waste()
{ color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
{ color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
}, return {
states_abbrv = { -- auto waste processing states
{ color = cpair(colors.black, colors.green), text = "Pu" }, states = {
{ color = cpair(colors.black, colors.cyan), text = "Po" }, { color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
{ color = cpair(colors.black, colors.purple), text = "AM" } { color = cpair(colors.black, po_color), text = "POLONIUM" },
}, { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
-- process radio button options },
options = { "Plutonium", "Polonium", "Antimatter" }, states_abbrv = {
-- unit waste selection { color = cpair(colors.black, pu_color), text = "Pu" },
unit_opts = { { color = cpair(colors.black, po_color), text = "Po" },
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) }, { color = cpair(colors.black, colors.purple), text = "AM" }
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.green) }, },
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.cyan) }, -- process radio button options
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) } options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = {
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) },
{ text = "Pu", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, pu_color) },
{ text = "Po", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, po_color) },
{ text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
}
} }
} end
return style return style

View File

@@ -53,25 +53,44 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
--#region Pocket UI --#region Pocket UI
local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24} local ui_c_1 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_c_2 = Div{parent=ui_cfg,x=2,y=4,width=24}
local ui_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={ui_c_1,ui_c_2}}
TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)} TextBox{parent=ui_cfg,x=1,y=2,text=" Pocket UI",fg_bg=cpair(colors.black,colors.lime)}
TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize units below."} TextBox{parent=ui_c_1,x=1,y=1,height=3,text="You may customize UI options below."}
TextBox{parent=ui_c_1,x=1,y=4,text="Temperature Scale"} TextBox{parent=ui_c_1,y=4,text="Po/Pu Pellet Color"}
local temp_scale = RadioButton{parent=ui_c_1,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime} TextBox{parent=ui_c_1,x=20,y=4,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
local pellet_color = RadioButton{parent=ui_c_1,y=5,default=util.trinary(ini_cfg.GreenPuPellet,1,2),options={"Green Pu/Cyan Po","Cyan Pu/Green Po"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_1,x=1,y=10,text="Energy Scale"} TextBox{parent=ui_c_1,y=8,height=4,text="In Mekanism 10.4 and later, pellet colors now match gas colors (Cyan Pu/Green Po).",fg_bg=g_lg_fg_bg}
local energy_scale = RadioButton{parent=ui_c_1,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_opts() local function submit_ui_opts()
tmp_cfg.GreenPuPellet = pellet_color.get_value() == 1
ui_pane.set_value(2)
end
PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=ui_c_2,x=1,y=1,height=3,text="You may customize units below."}
TextBox{parent=ui_c_2,x=1,y=4,text="Temperature Scale"}
local temp_scale = RadioButton{parent=ui_c_2,x=1,y=5,default=ini_cfg.TempScale,options=types.TEMP_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
TextBox{parent=ui_c_2,x=1,y=10,text="Energy Scale"}
local energy_scale = RadioButton{parent=ui_c_2,x=1,y=11,default=ini_cfg.EnergyScale,options=types.ENERGY_SCALE_NAMES,callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.lime}
local function submit_ui_units()
tmp_cfg.TempScale = temp_scale.get_value() tmp_cfg.TempScale = temp_scale.get_value()
tmp_cfg.EnergyScale = energy_scale.get_value() tmp_cfg.EnergyScale = energy_scale.get_value()
main_pane.set_value(3) main_pane.set_value(3)
end end
PushButton{parent=ui_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=ui_c_2,x=1,y=15,text="\x1b Back",callback=function()ui_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=ui_c_1,x=19,y=15,text="Next \x1a",callback=submit_ui_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=ui_c_2,x=19,y=15,text="Next \x1a",callback=submit_ui_units,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
@@ -266,6 +285,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
try_set(pellet_color, ini_cfg.GreenPuPellet)
try_set(temp_scale, ini_cfg.TempScale) try_set(temp_scale, ini_cfg.TempScale)
try_set(energy_scale, ini_cfg.EnergyScale) try_set(energy_scale, ini_cfg.EnergyScale)
try_set(svr_chan, ini_cfg.SVR_Channel) try_set(svr_chan, ini_cfg.SVR_Channel)
@@ -374,6 +394,8 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
val = string.rep("*", string.len(val)) val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then elseif f[1] == "LogMode" then
val = tri(raw == log.MODE.APPEND, "append", "replace") val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "GreenPuPellet" then
val = tri(raw, "Green Pu/Cyan Po", "Cyan Pu/Green Po")
elseif f[1] == "TempScale" then elseif f[1] == "TempScale" then
val = util.strval(types.TEMP_SCALE_NAMES[raw]) val = util.strval(types.TEMP_SCALE_NAMES[raw])
elseif f[1] == "EnergyScale" then elseif f[1] == "EnergyScale" then
@@ -385,7 +407,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local c = tri(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 alternate = not alternate
if string.len(val) > val_max_w then if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width) local lines = util.strwrap(val, inner_width)
height = #lines + 1 height = #lines + 1
end end

View File

@@ -29,7 +29,8 @@ local CENTER = core.ALIGN.CENTER
-- changes to the config data/format to let the user know -- changes to the config data/format to let the user know
local changes = { local changes = {
{ "v0.9.2", { "Added temperature scale options" } }, { "v0.9.2", { "Added temperature scale options" } },
{ "v0.11.3", { "Added energy scale options" } } { "v0.11.3", { "Added energy scale options" } },
{ "v0.13.2", { "Added option for Po/Pu pellet green/cyan pairing" } }
} }
---@class pkt_configurator ---@class pkt_configurator
@@ -64,6 +65,7 @@ local tool_ctl = {
---@class pkt_config ---@class pkt_config
local tmp_cfg = { local tmp_cfg = {
GreenPuPellet = false,
TempScale = 1, ---@type TEMP_SCALE TempScale = 1, ---@type TEMP_SCALE
EnergyScale = 1, ---@type ENERGY_SCALE EnergyScale = 1, ---@type ENERGY_SCALE
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
@@ -84,6 +86,7 @@ local settings_cfg = {}
-- all settings fields, their nice names, and their default values -- all settings fields, their nice names, and their default values
local fields = { local fields = {
{ "GreenPuPellet", "Pellet Colors", false },
{ "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN }, { "TempScale", "Temperature Scale", types.TEMP_SCALE.KELVIN },
{ "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE }, { "EnergyScale", "Energy Scale", types.ENERGY_SCALE.FE },
{ "SVR_Channel", "SVR Channel", 16240 }, { "SVR_Channel", "SVR Channel", 16240 },

View File

@@ -314,7 +314,7 @@ function iorx.record_unit_data(data)
local function blue(text) return { text = text, color = colors.blue } end local function blue(text) return { text = text, color = colors.blue } end
-- if unit.reactor_data.rps_status then -- if unit.reactor_data.rps_status then
-- for k, v in pairs(unit.alarms) do -- for k, _ in pairs(unit.alarms) do
-- unit.alarms[k] = ALARM_STATE.TRIPPED -- unit.alarms[k] = ALARM_STATE.TRIPPED
-- end -- end
-- end -- end

View File

@@ -38,6 +38,7 @@ pocket.config = config
function pocket.load_config() function pocket.load_config()
if not settings.load("/pocket.settings") then return false end if not settings.load("/pocket.settings") then return false end
config.GreenPuPellet = settings.get("GreenPuPellet")
config.TempScale = settings.get("TempScale") config.TempScale = settings.get("TempScale")
config.EnergyScale = settings.get("EnergyScale") config.EnergyScale = settings.get("EnergyScale")
@@ -54,6 +55,7 @@ function pocket.load_config()
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_type_bool(config.GreenPuPellet)
cfv.assert_type_int(config.TempScale) cfv.assert_type_int(config.TempScale)
cfv.assert_range(config.TempScale, 1, 4) cfv.assert_range(config.TempScale, 1, 4)
cfv.assert_type_int(config.EnergyScale) cfv.assert_type_int(config.EnergyScale)
@@ -267,8 +269,8 @@ function pocket.init_nav(smem)
-- open an app -- open an app
---@param app_id POCKET_APP_ID ---@param app_id POCKET_APP_ID
---@param on_loaded? function ---@param on_ready? function
function nav.open_app(app_id, on_loaded) function nav.open_app(app_id, on_ready)
-- reset help return on navigating out of an app -- reset help return on navigating out of an app
if app_id == APP_ID.ROOT then self.help_return = nil end if app_id == APP_ID.ROOT then self.help_return = nil end
@@ -281,7 +283,7 @@ function pocket.init_nav(smem)
app = self.apps[app_id] app = self.apps[app_id]
else self.loader_return = nil end else self.loader_return = nil end
if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_loaded }) end if not app.loaded then smem.q.mq_render.push_data(MQ__RENDER_DATA.LOAD_APP, { app_id, on_ready }) end
self.cur_app = app_id self.cur_app = app_id
self.pane.set_value(app_id) self.pane.set_value(app_id)
@@ -289,6 +291,8 @@ function pocket.init_nav(smem)
if #app.sidebar_items > 0 then if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items) self.sidebar.update(app.sidebar_items)
end end
if app.loaded and on_ready then on_ready() end
else else
log.debug("tried to open unknown app") log.debug("tried to open unknown app")
end end

View File

@@ -2,8 +2,10 @@
-- SCADA System Access on a Pocket Computer -- SCADA System Access on a Pocket Computer
-- --
---@diagnostic disable-next-line: undefined-global ---@diagnostic disable-next-line: lowercase-global
local _is_pocket_env = pocket or periphemu -- luacheck: ignore pocket pocket = pocket or periphemu -- luacheck: ignore pocket
local _is_pocket_env = pocket -- luacheck: ignore pocket
require("/initenv").init_env() require("/initenv").init_env()
@@ -20,7 +22,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer") local renderer = require("pocket.renderer")
local threads = require("pocket.threads") local threads = require("pocket.threads")
local POCKET_VERSION = "v0.13.0-beta" local POCKET_VERSION = "v0.13.4-beta"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@@ -95,8 +95,8 @@ local function new_view(root)
local function set_waste(mode) process.set_unit_waste(i, mode) end local function set_waste(mode) process.set_unit_waste(i, mode) end
local waste_prod = StateIndicator{parent=u_div,x=16,y=3,states=style.waste.states_abbrv,value=1,min_width=6} local waste_prod = StateIndicator{parent=u_div,x=16,y=3,states=style.get_waste().states_abbrv,value=1,min_width=6}
local waste_mode = RadioButton{parent=u_div,y=3,options=style.waste.unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white} local waste_mode = RadioButton{parent=u_div,y=3,options=style.get_waste().unit_opts,callback=set_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
waste_prod.register(u_ps, "U_WasteProduct", waste_prod.update) waste_prod.register(u_ps, "U_WasteProduct", waste_prod.update)
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value) waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
@@ -159,8 +159,8 @@ local function new_view(root)
TextBox{parent=c_div,y=1,text="Waste Control",alignment=ALIGN.CENTER} TextBox{parent=c_div,y=1,text="Waste Control",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=c_div,x=3,y=3,states=style.waste.states,value=1,min_width=17} local status = StateIndicator{parent=c_div,x=3,y=3,states=style.get_waste().states,value=1,min_width=17}
local waste_prod = RadioButton{parent=c_div,y=5,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white} local waste_prod = RadioButton{parent=c_div,y=5,options=style.get_waste().options,callback=process.set_process_waste,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.white}
status.register(f_ps, "current_waste_product", status.update) status.register(f_ps, "current_waste_product", status.update)
waste_prod.register(f_ps, "process_waste_product", waste_prod.set_value) waste_prod.register(f_ps, "process_waste_product", waste_prod.set_value)

View File

@@ -2,12 +2,18 @@
-- Graphics Style Options -- Graphics Style Options
-- --
local core = require("graphics.core") local util = require("scada-common.util")
local core = require("graphics.core")
local pocket = require("pocket.pocket")
local style = {} local style = {}
local cpair = core.cpair local cpair = core.cpair
local config = pocket.config
-- GLOBAL -- -- GLOBAL --
style.root = cpair(colors.white, colors.black) style.root = cpair(colors.white, colors.black)
@@ -171,22 +177,29 @@ style.sps = {
} }
} }
style.waste = { -- get waste styling, which depends on the configuration
-- auto waste processing states ---@return { states: { color: color, text: string }, states_abbrv: { color: color, text: string }, options: string[], unit_opts: string[] }
states = { function style.get_waste()
{ color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
{ color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
}, return {
states_abbrv = { -- auto waste processing states
{ color = cpair(colors.black, colors.green), text = "Pu" }, states = {
{ color = cpair(colors.black, colors.cyan), text = "Po" }, { color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
{ color = cpair(colors.black, colors.purple), text = "AM" } { color = cpair(colors.black, po_color), text = "POLONIUM" },
}, { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
-- process radio button options },
options = { "Plutonium", "Polonium", "Antimatter" }, states_abbrv = {
-- unit waste selection { color = cpair(colors.black, pu_color), text = "Pu" },
unit_opts = { "Auto", "Plutonium", "Polonium", "Antimatter" } { color = cpair(colors.black, po_color), text = "Po" },
} { color = cpair(colors.black, colors.purple), text = "AM" }
},
-- process radio button options
options = { "Plutonium", "Polonium", "Antimatter" },
-- unit waste selection
unit_opts = { "Auto", "Plutonium", "Polonium", "Antimatter" }
}
end
return style return style

View File

@@ -84,7 +84,7 @@ local function handle_packet(packet)
elseif est_ack == ESTABLISH_ACK.COLLISION then elseif est_ack == ESTABLISH_ACK.COLLISION then
error_msg = "another reactor PLC is connected with this reactor unit ID" error_msg = "another reactor PLC is connected with this reactor unit ID"
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update ...)" error_msg = "reactor PLC comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update)"
else else
error_msg = "error: invalid reply from supervisor" error_msg = "error: invalid reply from supervisor"
end end
@@ -120,11 +120,15 @@ local function self_check()
self.self_check_pass = true self.self_check_pass = true
local cfg = self.settings
local modem = ppm.get_wireless_modem() local modem = ppm.get_wireless_modem()
local reactor = ppm.get_fission_reactor() local reactor = ppm.get_fission_reactor()
local valid_cfg = plc.validate_config(self.settings) local valid_cfg = plc.validate_config(cfg)
if cfg.Networked then
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
end
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the reactor PLC")
self.self_check_msg("> check fission reactor connected...", reactor ~= nil, "please connect the reactor PLC to the reactor's fission reactor logic adapter") self.self_check_msg("> check fission reactor connected...", reactor ~= nil, "please connect the reactor PLC to the reactor's fission reactor logic adapter")
self.self_check_msg("> check fission reactor formed...") self.self_check_msg("> check fission reactor formed...")
-- this consumes events, but that is fine here -- this consumes events, but that is fine here
@@ -132,12 +136,12 @@ local function self_check()
self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones") self.self_check_msg("> check configuration...", valid_cfg, "go through Configure System and apply settings to set any missing settings and repair any corrupted ones")
if valid_cfg and modem then if cfg.Networked and valid_cfg and modem then
self.self_check_msg("> check supervisor connection...") self.self_check_msg("> check supervisor connection...")
-- init mac as needed -- init mac as needed
if self.settings.AuthKey and string.len(self.settings.AuthKey) >= 8 then if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
network.init_mac(self.settings.AuthKey) network.init_mac(cfg.AuthKey)
else else
network.deinit_mac() network.deinit_mac()
end end
@@ -145,12 +149,12 @@ local function self_check()
self.nic = network.nic(modem) self.nic = network.nic(modem)
self.nic.closeAll() self.nic.closeAll()
self.nic.open(self.settings.PLC_Channel) self.nic.open(cfg.PLC_Channel)
self.sv_addr = comms.BROADCAST self.sv_addr = comms.BROADCAST
self.net_listen = true self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, self.settings.UnitID }) send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.PLC, cfg.UnitID })
tcd.dispatch_unique(8, handle_timeout) tcd.dispatch_unique(8, handle_timeout)
else else

View File

@@ -592,7 +592,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white)) local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate alternate = not alternate
if string.len(val) > val_max_w then if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width) local lines = util.strwrap(val, inner_width)
height = #lines + 1 height = #lines + 1
end end

View File

@@ -40,6 +40,8 @@ local function init(panel)
local disabled_fg = style.fp.disabled_fg local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,fg_bg=style.theme.header} local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end) header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
@@ -60,7 +62,7 @@ local function init(panel)
local modem = LED{parent=system,label="MODEM",colors=ind_grn} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update) network.register(databus.ps, "link_state", network.update)
else else
@@ -121,7 +123,7 @@ local function init(panel)
-- status & controls -- status & controls
-- --
local status = Div{parent=panel,width=19,height=18,x=17,y=3} local status = Div{parent=panel,width=term_w-32,height=18,x=17,y=3}
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=ind_grn} local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=ind_grn}
@@ -131,14 +133,15 @@ local function init(panel)
emer_cool.register(databus.ps, "emer_cool", emer_cool.update) emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
end end
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true} local status_trip_rct = Rectangle{parent=status,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=s_hi_box} local status_trip = Div{parent=status_trip_rct,height=1,fg_bg=s_hi_box}
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS} local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS}
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true} local controls_rct = Rectangle{parent=status,width=status.get_width()-2,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=s_hi_box} local controls = Div{parent=controls_rct,width=controls_rct.get_width()-2,height=1,fg_bg=s_hi_box}
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)} local button_padding = math.floor((controls.get_width() - 14) / 3)
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)} PushButton{parent=controls,x=button_padding+1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=(2*button_padding)+9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
active.register(databus.ps, "reactor_active", active.update) active.register(databus.ps, "reactor_active", active.update)
scram.register(databus.ps, "rps_scram", scram.update) scram.register(databus.ps, "rps_scram", scram.update)
@@ -147,9 +150,9 @@ local function init(panel)
-- about footer -- about footer
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg} local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"} local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"} local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -158,7 +161,7 @@ local function init(panel)
-- rps list -- rps list
-- --
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box} local rps = Rectangle{parent=panel,width=16,height=16,x=term_w-15,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box}
local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red} local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red}
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red} local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red}
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red} local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red}

View File

@@ -23,8 +23,7 @@ local AUTO_ACK = comms.PLC_AUTO_ACK
local RPS_LIMITS = const.RPS_LIMITS local RPS_LIMITS = const.RPS_LIMITS
-- I sure hope the devs don't change this error message, not that it would have safety implications -- specific errors thrown when scram/start is used that still count as success
-- I wish they didn't change it to be like this
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active." local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "Reactor is already active." local PCALL_START_MSG = "Reactor is already active."

View File

@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer") local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads") local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.8.14" local R_PLC_VERSION = "v1.8.20"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -169,12 +169,12 @@ local function main()
-- PLC init<br> -- PLC init<br>
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
local function init() local function init()
-- just booting up, no fission allowed (neutrons stay put thanks) -- scram on boot if networked, otherwise leave the reactor be
if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram() smem_dev.reactor.scram()
end end
-- front panel time! -- setup front panel
if not renderer.ui_ready() then if not renderer.ui_ready() then
local message local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode) plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)

318
rtu/config/check.lua Normal file
View File

@@ -0,0 +1,318 @@
local comms = require("scada-common.comms")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local rtu = require("rtu.rtu")
local redstone = require("rtu.config.redstone")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local tri = util.trinary
local cpair = core.cpair
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local self = {
nic = nil, ---@type nic
net_listen = false,
sv_addr = comms.BROADCAST,
sv_seq_num = util.time_ms() * 10,
self_check_pass = true,
settings = nil, ---@type rtu_config
run_test_btn = nil, ---@type PushButton
sc_log = nil, ---@type ListBox
self_check_msg = nil ---@type function
}
-- report successful completion of the check
local function check_complete()
TextBox{parent=self.sc_log,text="> all tests passed!",fg_bg=cpair(colors.blue,colors._INHERIT)}
TextBox{parent=self.sc_log,text=""}
local more = Div{parent=self.sc_log,height=3,fg_bg=cpair(colors.gray,colors._INHERIT)}
TextBox{parent=more,text="if you still have a problem:"}
TextBox{parent=more,text="- check the wiki on GitHub"}
TextBox{parent=more,text="- ask for help on GitHub discussions or Discord"}
end
-- send a management packet to the supervisor
---@param msg_type MGMT_TYPE
---@param msg table
local function send_sv(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.mgmt_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.sv_addr, self.sv_seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable())
self.nic.transmit(self.settings.SVR_Channel, self.settings.RTU_Channel, s_pkt)
self.sv_seq_num = self.sv_seq_num + 1
end
-- handle an establish message from the supervisor
---@param packet mgmt_frame
local function handle_packet(packet)
local error_msg = nil
if packet.scada_frame.local_channel() ~= self.settings.RTU_Channel then
error_msg = "error: unknown receive channel"
elseif packet.scada_frame.remote_channel() == self.settings.SVR_Channel and packet.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if packet.type == MGMT_TYPE.ESTABLISH then
if packet.length == 1 then
local est_ack = packet.data[1]
if est_ack== ESTABLISH_ACK.ALLOW then
self.self_check_msg(nil, true, "")
self.sv_addr = packet.scada_frame.src_addr()
send_sv(MGMT_TYPE.CLOSE, {})
if self.self_check_pass then check_complete() end
elseif est_ack == ESTABLISH_ACK.DENY then
error_msg = "error: supervisor connection denied"
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
error_msg = "RTU gateway comms version does not match supervisor comms version, make sure both devices are up-to-date (ccmsi update)"
else
error_msg = "error: invalid reply from supervisor"
end
else
error_msg = "error: invalid reply length from supervisor"
end
else
error_msg = "error: didn't get an establish reply from supervisor"
end
end
self.net_listen = false
self.run_test_btn.enable()
if error_msg then
self.self_check_msg(nil, false, error_msg)
end
end
-- handle supervisor connection failure
local function handle_timeout()
self.net_listen = false
self.run_test_btn.enable()
self.self_check_msg(nil, false, "make sure your supervisor is running, your channels are correct, trusted ranges are set properly (if enabled), facility keys match (if set), and if you are using wireless modems rather than ender modems, that your devices are close together in the same dimension")
end
-- check if a value is an integer within a range (inclusive)
---@param x any
---@param min integer
---@param max integer
local function is_int_min_max(x, min, max) return util.is_int(x) and x >= min and x <= max end
-- execute the self-check
local function self_check()
self.run_test_btn.disable()
self.sc_log.remove_all()
ppm.mount_all()
self.self_check_pass = true
local cfg = self.settings
local modem = ppm.get_wireless_modem()
local valid_cfg = rtu.validate_config(cfg)
self.self_check_msg("> check wireless/ender modem connected...", modem ~= nil, "you must connect an ender or wireless modem to the RTU gateway")
self.self_check_msg("> check gateway configuration...", valid_cfg, "go through Configure Gateway and apply settings to set any missing settings and repair any corrupted ones")
-- check redstone configurations
local phys = {} ---@type rtu_rs_definition[][]
local inputs = { [0] = {}, {}, {}, {}, {} }
for i = 1, #cfg.Redstone do
local entry = cfg.Redstone[i]
local name = entry.relay or "local"
if phys[name] == nil then phys[name] = {} end
table.insert(phys[entry.relay or "local"], entry)
end
for name, entries in pairs(phys) do
TextBox{parent=self.sc_log,text="> checking redstone @ "..name.."...",fg_bg=cpair(colors.blue,colors.white)}
local ifaces = {}
local bundled_sides = {}
for i = 1, #entries do
local entry = entries[i]
local ident = entry.side .. tri(entry.color, ":" .. rsio.color_name(entry.color), "")
local sc_dupe = util.table_contains(ifaces, ident)
local mixed = (bundled_sides[entry.side] and (entry.color == nil)) or (bundled_sides[entry.side] == false and (entry.color ~= nil))
local mixed_msg = util.trinary(bundled_sides[entry.side], "bundled entry(s) but this entry is not", "non-bundled entry(s) but this entry is")
self.self_check_msg("> check redstone " .. ident .. " unique...", not sc_dupe, "only one port should be set to a side/color combination")
self.self_check_msg("> check redstone " .. ident .. " bundle...", not mixed, "this side has " .. mixed_msg .. " bundled, which will not work")
self.self_check_msg("> check redstone " .. ident .. " valid...", redstone.validate(entry), "configuration invalid, please re-configure redstone entry")
if rsio.get_io_dir(entry.port) == rsio.IO_DIR.IN then
local in_dupe = util.table_contains(inputs[entry.unit or 0], entry.port)
self.self_check_msg("> check redstone " .. ident .. " input...", not in_dupe, "you cannot have multiple of the same input for a given unit or the facility ("..rsio.to_string(entry.port)..")")
end
bundled_sides[entry.side] = bundled_sides[entry.side] or entry.color ~= nil
table.insert(ifaces, ident)
end
end
-- check peripheral configurations
for i = 1, #cfg.Peripherals do
local entry = cfg.Peripherals[i]
local valid = false
if type(entry.name) == "string" then
self.self_check_msg("> check " .. entry.name .. " connected...", ppm.get_periph(entry.name), "please connect this device via a wired modem or direct contact and ensure the configuration matches what it connects as")
local p_type = ppm.get_type(entry.name)
if p_type == "boilerValve" then
valid = is_int_min_max(entry.index, 1, 2) and is_int_min_max(entry.unit, 1, 4)
elseif p_type == "turbineValve" then
valid = is_int_min_max(entry.index, 1, 3) and is_int_min_max(entry.unit, 1, 4)
elseif p_type == "solarNeutronActivator" then
valid = is_int_min_max(entry.unit, 1, 4)
elseif p_type == "dynamicValve" then
valid = (entry.unit == nil and is_int_min_max(entry.index, 1, 4)) or is_int_min_max(entry.unit, 1, 4)
elseif p_type == "environmentDetector" then
valid = (entry.unit == nil or is_int_min_max(entry.unit, 1, 4)) and util.is_int(entry.index)
else
valid = true
if p_type ~= nil and not (p_type == "inductionPort" or p_type == "spsPort") then
self.self_check_msg("> check " .. entry.name .. " valid...", false, "unrecognized device type")
end
end
end
if not valid then
self.self_check_msg("> check " .. entry.name .. " valid...", false, "configuration invalid, please re-configure peripheral entry")
end
end
if valid_cfg and modem then
self.self_check_msg("> check supervisor connection...")
-- init mac as needed
if cfg.AuthKey and string.len(cfg.AuthKey) >= 8 then
network.init_mac(cfg.AuthKey)
else
network.deinit_mac()
end
self.nic = network.nic(modem)
self.nic.closeAll()
self.nic.open(cfg.RTU_Channel)
self.sv_addr = comms.BROADCAST
self.net_listen = true
send_sv(MGMT_TYPE.ESTABLISH, { comms.version, "0.0.0", DEVICE_TYPE.RTU, {} })
tcd.dispatch_unique(8, handle_timeout)
else
if self.self_check_pass then check_complete() end
self.run_test_btn.enable()
end
end
-- exit self check back home
---@param main_pane MultiPane
local function exit_self_check(main_pane)
tcd.abort(handle_timeout)
self.net_listen = false
self.run_test_btn.enable()
self.sc_log.remove_all()
main_pane.set_value(1)
end
local check = {}
-- create the self-check view
---@param main_pane MultiPane
---@param settings_cfg rtu_config
---@param check_sys Div
---@param style { [string]: cpair }
function check.create(main_pane, settings_cfg, check_sys, style)
local bw_fg_bg = style.bw_fg_bg
local g_lg_fg_bg = style.g_lg_fg_bg
local nav_fg_bg = style.nav_fg_bg
local btn_act_fg_bg = style.btn_act_fg_bg
local btn_dis_fg_bg = style.btn_dis_fg_bg
self.settings = settings_cfg
local sc = Div{parent=check_sys,x=2,y=4,width=49}
TextBox{parent=check_sys,x=1,y=2,text=" RTU Gateway Self-Check",fg_bg=bw_fg_bg}
self.sc_log = ListBox{parent=sc,x=1,y=1,height=12,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local last_check = { nil, nil }
function self.self_check_msg(msg, success, fail_msg)
if type(msg) == "string" then
last_check[1] = Div{parent=self.sc_log,height=1}
local e = TextBox{parent=last_check[1],text=msg,fg_bg=bw_fg_bg}
last_check[2] = e.get_x()+string.len(msg)
end
if type(fail_msg) == "string" then
TextBox{parent=last_check[1],x=last_check[2],y=1,text=tri(success,"PASS","FAIL"),fg_bg=tri(success,cpair(colors.green,colors._INHERIT),cpair(colors.red,colors._INHERIT))}
if not success then
local fail = Div{parent=self.sc_log,height=#util.strwrap(fail_msg, 46)}
TextBox{parent=fail,x=3,text=fail_msg,fg_bg=cpair(colors.gray,colors.white)}
end
self.self_check_pass = self.self_check_pass and success
end
end
PushButton{parent=sc,x=1,y=14,text="\x1b Back",callback=function()exit_self_check(main_pane)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.run_test_btn = PushButton{parent=sc,x=40,y=14,min_width=10,text="Run Test",callback=function()self_check()end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
-- handle incoming modem messages
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
function check.receive_sv(side, sender, reply_to, message, distance)
if self.nic ~= nil and self.net_listen then
local s_pkt = self.nic.receive(side, sender, reply_to, message, distance)
if s_pkt and s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
tcd.abort(handle_timeout)
handle_packet(mgmt_pkt.get())
end
end
end
end
return check

View File

@@ -149,7 +149,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7) reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7)
self.p_idx.hide() self.p_idx.hide()
self.p_assign_btn.hide(true) self.p_assign_btn.hide(true)
self.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.") self.p_desc_ext.set_value("Warning: too many devices on one RTU Gateway can cause lag. Note that 10x the \"PEAK\x1a\" rate on the flow monitor gives you the mB/t of waste that the SNA(s) can process. Enough SNAs to provide 2x to 3x of that unit's max burn rate should be a good margin to catch up after night or cloudy weather.")
elseif type == "dynamicValve" then elseif type == "dynamicValve" then
reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8) reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8)
self.p_assign_btn.show() self.p_assign_btn.show()

View File

@@ -1,4 +1,5 @@
local constants = require("scada-common.constants") local constants = require("scada-common.constants")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local util = require("scada-common.util") local util = require("scada-common.util")
@@ -18,8 +19,10 @@ local NumberField = require("graphics.elements.form.NumberField")
---@class rtu_rs_definition ---@class rtu_rs_definition
---@field unit integer|nil ---@field unit integer|nil
---@field port IO_PORT ---@field port IO_PORT
---@field relay string|nil
---@field side side ---@field side side
---@field color color|nil ---@field color color|nil
---@field invert true|nil
local tri = util.trinary local tri = util.trinary
@@ -32,6 +35,7 @@ local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT local LEFT = core.ALIGN.LEFT
local self = { local self = {
rs_cfg_phy = false, ---@type string|nil|false
rs_cfg_port = 1, ---@type IO_PORT rs_cfg_port = 1, ---@type IO_PORT
rs_cfg_editing = false, ---@type integer|false rs_cfg_editing = false, ---@type integer|false
@@ -41,7 +45,9 @@ local self = {
rs_cfg_side_l = nil, ---@type TextBox rs_cfg_side_l = nil, ---@type TextBox
rs_cfg_bundled = nil, ---@type Checkbox rs_cfg_bundled = nil, ---@type Checkbox
rs_cfg_color = nil, ---@type Radio2D rs_cfg_color = nil, ---@type Radio2D
rs_cfg_shortcut = nil ---@type TextBox rs_cfg_inverted = nil, ---@type Checkbox
rs_cfg_shortcut = nil, ---@type TextBox
rs_cfg_advanced = nil ---@type PushButton
} }
-- rsio port descriptions -- rsio port descriptions
@@ -74,11 +80,12 @@ local PORT_DESC_MAP = {
{ IO.R_PLC_FAULT, "RPS PLC Fault" }, { IO.R_PLC_FAULT, "RPS PLC Fault" },
{ IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" }, { IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" },
{ IO.U_ALARM, "Unit Alarm" }, { IO.U_ALARM, "Unit Alarm" },
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" } { IO.U_EMER_COOL, "Unit Emergency Cool. Valve" },
{ IO.U_AUX_COOL, "Unit Auxiliary Cool. Valve" }
} }
-- designation (0 = facility, 1 = unit) -- 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, 0, 0, 0 } 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, 1 }
assert(#PORT_DESC_MAP == rsio.NUM_PORTS) assert(#PORT_DESC_MAP == rsio.NUM_PORTS)
assert(#PORT_DSGN == rsio.NUM_PORTS) assert(#PORT_DSGN == rsio.NUM_PORTS)
@@ -104,8 +111,34 @@ local function color_to_idx(color)
end end
end end
-- select the subset of redstone entries assigned to the given phy
---@param cfg rtu_rs_definition[] the full redstone entry list
---@param phy string|nil which phy to get redstone entries for
---@param invert boolean? true to get all except this phy
---@return rtu_rs_definition[]
local function redstone_subset(cfg, phy, invert)
local subset = {}
for i = 1, #cfg do
if ((not invert) and cfg[i].relay == phy) or (invert and cfg[i].relay ~= phy) then
table.insert(subset, cfg[i])
end
end
return subset
end
local redstone = {} local redstone = {}
-- validate a redstone entry
---@param def rtu_rs_definition
function redstone.validate(def)
return tri(PORT_DSGN[def.port] == 1, util.is_int(def.unit) and def.unit > 0 and def.unit <= 4, def.unit == nil) and
rsio.is_valid_port(def.port) and
rsio.is_valid_side(def.side) and
(def.color == nil or (rsio.is_digital(def.port) and rsio.is_color(def.color)))
end
-- create the redstone configuration view -- create the redstone configuration view
---@param tool_ctl _rtu_cfg_tool_ctl ---@param tool_ctl _rtu_cfg_tool_ctl
---@param main_pane MultiPane ---@param main_pane MultiPane
@@ -124,20 +157,89 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
--#region Redstone --#region Redstone
local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_1 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_2 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49} local rs_c_3 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49} 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_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_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_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_8 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_9 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_10 = 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,rs_c_7}} 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,rs_c_8,rs_c_9,rs_c_10}}
TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)} local header = TextBox{parent=rs_cfg,x=1,y=2,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
TextBox{parent=rs_c_1,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg} --#region Interface Selection
local rs_list = ListBox{parent=rs_c_1,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
TextBox{parent=rs_c_1,x=1,y=1,text="Configure this computer or a redstone relay."}
local iface_list = ListBox{parent=rs_c_1,x=1,y=3,height=10,width=49,scroll_height=1000,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
-- update relay interface list
function tool_ctl.update_relay_list()
local mounts = ppm.list_mounts()
iface_list.remove_all()
-- assemble list of configured relays
local relays = {}
for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i]
if def.relay and not util.table_contains(relays, def.relay) then
table.insert(relays, def.relay)
end
end
-- add unconfigured connected relays
for name, entry in pairs(mounts) do
if entry.type == "redstone_relay" and not util.table_contains(relays, name) then
table.insert(relays, name)
end
end
local function config_rs(name)
header.set_value(" Redstone Connections (" .. name .. ")")
self.rs_cfg_phy = tri(name == "local", nil, name)
tool_ctl.gen_rs_summary()
rs_pane.set_value(2)
end
local line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ local",fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="This Computer",fg_bg=cpair(colors.gray,colors.white)}
local count = #redstone_subset(ini_cfg.Redstone, nil)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs("local")end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
for i = 1, #relays do
local name = relays[i]
line = Div{parent=iface_list,height=2,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=1,y=1,text="@ "..name,fg_bg=cpair(colors.black,colors.white)}
TextBox{parent=line,x=3,y=2,text="Redstone Relay",fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=line,x=18,y=2,text=tri(mounts[name],"ONLINE","OFFLINE"),fg_bg=cpair(tri(mounts[name],colors.green,colors.red),colors.white)}
count = #redstone_subset(ini_cfg.Redstone, name)
TextBox{parent=line,x=33,y=2,width=16,alignment=core.ALIGN.RIGHT,text=count.." connections",fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=line,x=41,y=1,min_width=8,height=1,text="CONFIG",callback=function()config_rs(name)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
end
end
tool_ctl.update_relay_list()
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_1,x=27,y=14,min_width=23,text="I don't see my relay!",callback=function()rs_pane.set_value(10)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Configuration List
TextBox{parent=rs_c_2,x=1,y=1,text=" port side/color unit/facility",fg_bg=g_lg_fg_bg}
local rs_list = ListBox{parent=rs_c_2,x=1,y=2,height=11,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function rs_revert() local function rs_revert()
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
@@ -145,43 +247,47 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
end end
local function rs_apply() local function rs_apply()
settings.set("Redstone", tmp_cfg.Redstone) -- add the changed data to the existing saved data
local new_data = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local new_save = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy, true)
for i = 1, #new_data do table.insert(new_save, new_data[i]) end
settings.set("Redstone", new_save)
if settings.save("/rtu.settings") then if settings.save("/rtu.settings") then
load_settings(settings_cfg, true) load_settings(settings_cfg, true)
load_settings(ini_cfg) load_settings(ini_cfg)
rs_pane.set_value(4) rs_pane.set_value(5)
-- for return to list from saved screen -- for return to list from saved screen
-- this will delete unsaved changes for other phy's, which is acceptable
tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone) tmp_cfg.Redstone = tool_ctl.deep_copy_rs(ini_cfg.Redstone)
tool_ctl.gen_rs_summary() tool_ctl.gen_rs_summary()
tool_ctl.update_relay_list()
else else
rs_pane.set_value(5) rs_pane.set_value(6)
end end
end end
PushButton{parent=rs_c_1,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} local function rs_back()
local rs_revert_btn = PushButton{parent=rs_c_1,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} self.rs_cfg_phy = false
PushButton{parent=rs_c_1,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} rs_pane.set_value(1)
local rs_apply_btn = PushButton{parent=rs_c_1,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} header.set_value(" Redstone Connections")
end
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="You already configured this input. There can only be one entry for each input.\n\nPlease select a different port."} PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=rs_back,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} local rs_revert_btn = PushButton{parent=rs_c_2,x=8,y=14,min_width=16,text="Revert Changes",callback=rs_revert,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_2,x=35,y=14,min_width=7,text="New +",callback=function()rs_pane.set_value(3)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
local rs_apply_btn = PushButton{parent=rs_c_2,x=43,y=14,min_width=7,text="Apply",callback=rs_apply,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
TextBox{parent=rs_c_2,x=1,y=1,text="Select one of the below ports to use."} --#endregion
--#region Port Selection
local rs_ports = ListBox{parent=rs_c_2,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)} TextBox{parent=rs_c_3,x=1,y=1,text="Select one of the below ports to use."}
local rs_ports = ListBox{parent=rs_c_3,x=1,y=3,height=10,width=49,scroll_height=200,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
local function new_rs(port) local function new_rs(port)
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port then
rs_pane.set_value(6)
return
end
end
end
self.rs_cfg_editing = false self.rs_cfg_editing = false
local text local text
@@ -190,6 +296,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_color.hide(true) self.rs_cfg_color.hide(true)
self.rs_cfg_shortcut.show() self.rs_cfg_shortcut.show()
self.rs_cfg_side_l.set_value("Output Side") self.rs_cfg_side_l.set_value("Output Side")
self.rs_cfg_bundled.enable()
self.rs_cfg_advanced.disable()
text = "You selected the ALL_WASTE shortcut." text = "You selected the ALL_WASTE shortcut."
else else
self.rs_cfg_shortcut.hide(true) self.rs_cfg_shortcut.hide(true)
@@ -204,9 +312,13 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_bundled.set_value(false) self.rs_cfg_bundled.set_value(false)
self.rs_cfg_bundled.disable() self.rs_cfg_bundled.disable()
self.rs_cfg_color.disable() self.rs_cfg_color.disable()
self.rs_cfg_inverted.set_value(false)
self.rs_cfg_advanced.disable()
else else
self.rs_cfg_bundled.enable() self.rs_cfg_bundled.enable()
if self.rs_cfg_bundled.get_value() then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end if self.rs_cfg_bundled.get_value() then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
self.rs_cfg_inverted.set_value(false)
self.rs_cfg_advanced.enable()
end end
if io_mode == IO_MODE.DIGITAL_IN then if io_mode == IO_MODE.DIGITAL_IN then
@@ -232,7 +344,7 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_selection.set_value(text) self.rs_cfg_selection.set_value(text)
self.rs_cfg_port = port self.rs_cfg_port = port
rs_pane.set_value(3) rs_pane.set_value(4)
end end
-- add entries to redstone option list -- add entries to redstone option list
@@ -253,43 +365,43 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=22,y=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end 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} PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
self.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""} --#endregion
--#region Port Configuration
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} self.rs_cfg_selection = TextBox{parent=rs_c_4,x=1,y=1,height=2,text=""}
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"} PushButton{parent=rs_c_4,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
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}
self.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,text="Output Side"} self.rs_cfg_side_l = TextBox{parent=rs_c_4,x=1,y=4,width=11,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} local side = Radio2D{parent=rs_c_4,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
self.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,text="Unit ID"} self.rs_cfg_unit_l = TextBox{parent=rs_c_4,x=25,y=7,width=7,text="Unit ID"}
self.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} self.rs_cfg_unit = NumberField{parent=rs_c_4,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local function set_bundled(bundled) local function set_bundled(bundled)
if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end if bundled then self.rs_cfg_color.enable() else self.rs_cfg_color.disable() end
end end
self.rs_cfg_shortcut = TextBox{parent=rs_c_3,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."} self.rs_cfg_shortcut = TextBox{parent=rs_c_4,x=1,y=9,height=4,text="This shortcut will add entries for each of the 4 waste outputs. If you select bundled, 4 colors will be assigned to the selected side. Otherwise, 4 default sides will be used."}
self.rs_cfg_shortcut.hide(true) self.rs_cfg_shortcut.hide(true)
self.rs_cfg_bundled = Checkbox{parent=rs_c_3,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg} self.rs_cfg_bundled = Checkbox{parent=rs_c_4,x=1,y=7,label="Is Bundled?",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=set_bundled,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color = Radio2D{parent=rs_c_3,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg} self.rs_cfg_color = Radio2D{parent=rs_c_4,x=1,y=9,rows=4,columns=4,default=1,options=color_options,radio_colors=cpair(colors.lightGray,colors.black),color_map=color_options_map,disable_color=colors.gray,disable_fg_bg=g_lg_fg_bg}
self.rs_cfg_color.disable() self.rs_cfg_color.disable()
local rs_err = TextBox{parent=rs_c_3,x=8,y=14,width=30,text="Unit ID must be within 1 to 4.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local rs_err = TextBox{parent=rs_c_4,x=8,y=14,width=30,text="Unit ID invalid.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
rs_err.hide(true) rs_err.hide(true)
local function back_from_rs_opts() local function back_from_rs_opts()
rs_err.hide(true) rs_err.hide(true)
if self.rs_cfg_editing ~= false then rs_pane.set_value(1) else rs_pane.set_value(2) end if self.rs_cfg_editing ~= false then rs_pane.set_value(2) else rs_pane.set_value(3) end
end end
local function save_rs_entry() local function save_rs_entry()
assert(self.rs_cfg_phy ~= false, "tried to save a redstone entry without a phy")
local port = self.rs_cfg_port local port = self.rs_cfg_port
local u = tonumber(self.rs_cfg_unit.get_value()) local u = tonumber(self.rs_cfg_unit.get_value())
@@ -301,11 +413,23 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
local def = { local def = {
unit = tri(PORT_DSGN[port] == 1, u, nil), unit = tri(PORT_DSGN[port] == 1, u, nil),
port = port, port = port,
relay = self.rs_cfg_phy,
side = side_options_map[side.get_value()], side = side_options_map[side.get_value()],
color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil) color = tri(self.rs_cfg_bundled.get_value() and rsio.is_digital(port), color_options_map[self.rs_cfg_color.get_value()], nil),
invert = self.rs_cfg_inverted.get_value() or nil
} }
if self.rs_cfg_editing == false then if self.rs_cfg_editing == false then
-- check for duplicate inputs for this unit/facility
if (rsio.get_io_dir(port) == rsio.IO_DIR.IN) then
for i = 1, #tmp_cfg.Redstone do
if tmp_cfg.Redstone[i].port == port and tmp_cfg.Redstone[i].unit == def.unit then
rs_pane.set_value(7)
return
end
end
end
table.insert(tmp_cfg.Redstone, def) table.insert(tmp_cfg.Redstone, def)
else else
def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port def.port = tmp_cfg.Redstone[self.rs_cfg_editing].port
@@ -318,33 +442,55 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
table.insert(tmp_cfg.Redstone, { table.insert(tmp_cfg.Redstone, {
unit = tri(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, port = IO.WASTE_PU + i,
relay = self.rs_cfg_phy,
side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]), side = tri(self.rs_cfg_bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil) color = tri(self.rs_cfg_bundled.get_value(), default_colors[i + 1], nil)
}) })
end end
end end
rs_pane.set_value(1) rs_pane.set_value(2)
tool_ctl.gen_rs_summary() tool_ctl.gen_rs_summary()
side.set_value(1) side.set_value(1)
self.rs_cfg_bundled.set_value(false) self.rs_cfg_bundled.set_value(false)
self.rs_cfg_color.set_value(1) self.rs_cfg_color.set_value(1)
self.rs_cfg_color.disable() self.rs_cfg_color.disable()
self.rs_cfg_inverted.set_value(false)
self.rs_cfg_advanced.disable()
else rs_err.show() end else rs_err.show() end
end end
PushButton{parent=rs_c_3,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_4,x=1,y=14,text="\x1b Back",callback=back_from_rs_opts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_3,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} self.rs_cfg_advanced = PushButton{parent=rs_c_4,x=30,y=14,min_width=10,text="Advanced",callback=function()rs_pane.set_value(9)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=rs_c_4,x=41,y=14,min_width=9,text="Confirm",callback=save_rs_entry,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_4,x=1,y=1,text="Settings saved!"} --#endregion
PushButton{parent=rs_c_4,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}
PushButton{parent=rs_c_4,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_5,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."} TextBox{parent=rs_c_5,x=1,y=1,text="Settings saved!"}
PushButton{parent=rs_c_5,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} PushButton{parent=rs_c_5,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=rs_c_5,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_6,x=1,y=1,height=5,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
PushButton{parent=rs_c_6,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=rs_c_6,x=44,y=14,min_width=6,text="Home",callback=function()tool_ctl.go_home()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_7,x=1,y=1,height=6,text="You already configured this input for this facility/unit assignment. There can only be one entry for each input per each unit or the facility (for facility inputs).\n\nPlease select a different port."}
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}
TextBox{parent=rs_c_8,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_8,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_8,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_8,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_9,x=1,y=1,height=5,text="Advanced Options"}
self.rs_cfg_inverted = Checkbox{parent=rs_c_9,x=1,y=3,label="Invert",default=false,box_fg_bg=cpair(colors.red,colors.black),callback=function()end,disable_fg_bg=g_lg_fg_bg}
TextBox{parent=rs_c_9,x=3,y=4,height=4,text="Digital I/O is already inverted (or not) based on intended use. If you have a non-standard setup, you can use this option to avoid needing a redstone inverter.",fg_bg=cpair(colors.gray,colors.lightGray)}
PushButton{parent=rs_c_9,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
TextBox{parent=rs_c_10,x=1,y=1,height=10,text="Make sure your relay is either touching the RTU gateway or connected via wired modems. There should be a wired modem on a side of the RTU gateway then one on the device, connected by a cable. The modem on the device needs to be right clicked to connect it (which will turn its border red), at which point the peripheral name will be shown in the chat."}
PushButton{parent=rs_c_10,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}
--#endregion --#endregion
--#region Tool Functions --#region Tool Functions
@@ -373,9 +519,11 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
if rsio.is_analog(def.port) then if rsio.is_analog(def.port) then
self.rs_cfg_bundled.set_value(false) self.rs_cfg_bundled.set_value(false)
self.rs_cfg_bundled.disable() self.rs_cfg_bundled.disable()
self.rs_cfg_advanced.disable()
else else
self.rs_cfg_bundled.enable() self.rs_cfg_bundled.enable()
self.rs_cfg_bundled.set_value(def.color ~= nil) self.rs_cfg_bundled.set_value(def.color ~= nil)
self.rs_cfg_advanced.enable()
end end
local value = 1 local value = 1
@@ -390,7 +538,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
self.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side")) self.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)) side.set_value(side_to_idx(def.side))
self.rs_cfg_color.set_value(value) self.rs_cfg_color.set_value(value)
rs_pane.set_value(3) self.rs_cfg_inverted.set_value(def.invert or false)
rs_pane.set_value(4)
end end
local function delete_rs_entry(idx) local function delete_rs_entry(idx)
@@ -400,33 +549,41 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
-- generate the redstone summary list -- generate the redstone summary list
function tool_ctl.gen_rs_summary() function tool_ctl.gen_rs_summary()
assert(self.rs_cfg_phy ~= false, "tried to generate a summary without a phy set")
rs_list.remove_all() rs_list.remove_all()
local modified = #ini_cfg.Redstone ~= #tmp_cfg.Redstone local ini = redstone_subset(ini_cfg.Redstone, self.rs_cfg_phy)
local tmp = redstone_subset(tmp_cfg.Redstone, self.rs_cfg_phy)
local modified = #ini ~= #tmp
for i = 1, #tmp_cfg.Redstone do for i = 1, #tmp_cfg.Redstone do
local def = tmp_cfg.Redstone[i] local def = tmp_cfg.Redstone[i]
local name = rsio.to_string(def.port) if def.relay == self.rs_cfg_phy then
local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") local name = rsio.to_string(def.port)
local conn = def.side local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local unit = util.strval(def.unit or "F") local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple)
local conn = def.side
local unit = util.strval(def.unit or "F")
if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end if def.color ~= nil then conn = def.side .. "/" .. rsio.color_name(def.color) end
local entry = Div{parent=rs_list,height=1} local entry = Div{parent=rs_list,height=1}
TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)} TextBox{parent=entry,x=1,y=1,width=1,text=io_dir,fg_bg=cpair(tri(def.invert,colors.orange,io_c),colors.white)}
TextBox{parent=entry,x=2,y=1,width=14,text=name} TextBox{parent=entry,x=2,y=1,width=14,text=name}
TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=16,y=1,width=string.len(conn),text=conn,fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)} TextBox{parent=entry,x=33,y=1,width=1,text=unit,fg_bg=cpair(colors.gray,colors.white)}
PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg} PushButton{parent=entry,x=35,y=1,min_width=6,height=1,text="EDIT",callback=function()edit_rs_entry(i)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=entry,x=41,y=1,min_width=8,height=1,text="DELETE",callback=function()delete_rs_entry(i)end,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
if not modified then if not modified then
local a = ini_cfg.Redstone[i] local a = ini_cfg.Redstone[i]
local b = tmp_cfg.Redstone[i] local b = tmp_cfg.Redstone[i]
modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.side ~= b.side) or (a.color ~= b.color) modified = (a.unit ~= b.unit) or (a.port ~= b.port) or (a.relay ~= b.relay) or (a.side ~= b.side) or (a.color ~= b.color) or (a.invert ~= b.invert)
end
end end
end end

View File

@@ -646,7 +646,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local c = tri(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 alternate = not alternate
if string.len(val) > val_max_w then if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width) local lines = util.strwrap(val, inner_width)
height = #lines + 1 height = #lines + 1
end end

View File

@@ -7,6 +7,7 @@ local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local util = require("scada-common.util") local util = require("scada-common.util")
local check = require("rtu.config.check")
local peripherals = require("rtu.config.peripherals") local peripherals = require("rtu.config.peripherals")
local redstone = require("rtu.config.redstone") local redstone = require("rtu.config.redstone")
local system = require("rtu.config.system") local system = require("rtu.config.system")
@@ -34,7 +35,9 @@ local changes = {
{ "v1.7.9", { "ConnTimeout can now have a fractional part" } }, { "v1.7.9", { "ConnTimeout can now have a fractional part" } },
{ "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.7.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } }, { "v1.9.2", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } } { "v1.10.2", { "Re-organized peripheral configuration UI, resulting in some input fields being re-ordered" } },
{ "v1.11.8", { "Added advanced option to invert digital redstone signals" } },
{ "v1.12.0", { "Added support for redstone relays" } }
} }
---@class rtu_configurator ---@class rtu_configurator
@@ -74,6 +77,7 @@ local tool_ctl = {
gen_summary = nil, ---@type function gen_summary = nil, ---@type function
load_legacy = nil, ---@type function load_legacy = nil, ---@type function
update_peri_list = nil, ---@type function update_peri_list = nil, ---@type function
update_relay_list = nil, ---@type function
gen_peri_summary = nil, ---@type function gen_peri_summary = nil, ---@type function
gen_rs_summary = nil, ---@type function gen_rs_summary = nil, ---@type function
} }
@@ -115,6 +119,7 @@ local fields = {
} }
-- deep copy peripherals defs -- deep copy peripherals defs
---@param data rtu_peri_definition[]
function tool_ctl.deep_copy_peri(data) function tool_ctl.deep_copy_peri(data)
local array = {} local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end for _, d in ipairs(data) do table.insert(array, { unit = d.unit, index = d.index, name = d.name }) end
@@ -122,9 +127,10 @@ function tool_ctl.deep_copy_peri(data)
end end
-- deep copy redstone defs -- deep copy redstone defs
---@param data rtu_rs_definition[]
function tool_ctl.deep_copy_rs(data) function tool_ctl.deep_copy_rs(data)
local array = {} local array = {}
for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, side = d.side, color = d.color }) end for _, d in ipairs(data) do table.insert(array, { unit = d.unit, port = d.port, relay = d.relay, side = d.side, color = d.color, invert = d.invert }) end
return array return array
end end
@@ -169,8 +175,9 @@ local function config_view(display)
local changelog = Div{parent=root_pane_div,x=1,y=1} local changelog = Div{parent=root_pane_div,x=1,y=1}
local peri_cfg = Div{parent=root_pane_div,x=1,y=1} local peri_cfg = Div{parent=root_pane_div,x=1,y=1}
local rs_cfg = Div{parent=root_pane_div,x=1,y=1} local rs_cfg = Div{parent=root_pane_div,x=1,y=1}
local check_sys = Div{parent=root_pane_div,x=1,y=1}
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg}} local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,spkr_cfg,net_cfg,log_cfg,clr_cfg,summary,changelog,peri_cfg,rs_cfg,check_sys}}
--#region Main Page --#region Main Page
@@ -203,7 +210,6 @@ local function config_view(display)
end end
local function show_rs_conns() local function show_rs_conns()
tool_ctl.gen_rs_summary()
main_pane.set_value(9) main_pane.set_value(9)
end end
@@ -226,8 +232,9 @@ local function config_view(display)
PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg} PushButton{parent=main_page,x=2,y=17,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
local start_btn = PushButton{parent=main_page,x=42,y=17,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} local start_btn = PushButton{parent=main_page,x=42,y=17,min_width=9,text="Startup",callback=startup,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg} PushButton{parent=main_page,x=39,y=y_start,min_width=12,text="Self-Check",callback=function()main_pane.set_value(10)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=y_start+2,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} tool_ctl.color_cfg = PushButton{parent=main_page,x=36,y=y_start+2,min_width=15,text="Color Options",callback=jump_color,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=main_page,x=39,y=y_start+4,min_width=12,text="Change Log",callback=function()main_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
if tool_ctl.ask_config then start_btn.disable() end if tool_ctl.ask_config then start_btn.disable() end
@@ -283,6 +290,12 @@ local function config_view(display)
PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=cl,x=1,y=14,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
--#region Self-Check
check.create(main_pane, settings_cfg, check_sys, style)
--#endregion
end end
-- reset terminal screen -- reset terminal screen
@@ -317,7 +330,7 @@ function configurator.configure(ask_config)
config_view(display) config_view(display)
while true do while true do
local event, param1, param2, param3 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
-- handle event -- handle event
if event == "timer" then if event == "timer" then
@@ -330,14 +343,18 @@ function configurator.configure(ask_config)
if k_e then display.handle_key(k_e) end if k_e then display.handle_key(k_e) end
elseif event == "paste" then elseif event == "paste" then
display.handle_paste(param1) display.handle_paste(param1)
elseif event == "modem_message" then
check.receive_sv(param1, param2, param3, param4, param5)
elseif event == "peripheral_detach" then elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns ---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1) ppm.handle_unmount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
elseif event == "peripheral" then elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns ---@diagnostic disable-next-line: discard-returns
ppm.mount(param1) ppm.mount(param1)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
tool_ctl.update_relay_list()
end end
if event == "terminate" then return end if event == "terminate" then return end

View File

@@ -11,10 +11,14 @@ local digital_write = rsio.digital_write
-- create new redstone device -- create new redstone device
---@nodiscard ---@nodiscard
---@param relay? table optional redstone relay to use instead of the computer's redstone interface
---@return rtu_rs_device interface, boolean faulted ---@return rtu_rs_device interface, boolean faulted
function redstone_rtu.new() function redstone_rtu.new(relay)
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- physical interface to use
local phy = relay or rs
-- get RTU interface -- get RTU interface
local interface = unit.interface() local interface = unit.interface()
@@ -30,85 +34,114 @@ function redstone_rtu.new()
write_holding_reg = interface.write_holding_reg write_holding_reg = interface.write_holding_reg
} }
-- change the phy in use (a relay or rs)
---@param new_phy table
function public.remount_phy(new_phy) phy = new_phy end
-- NOTE: for runtime speed, inversion logic results in extra code here but less code when functions are called
-- link digital input -- link digital input
---@param side string ---@param side string
---@param color integer ---@param color integer
function public.link_di(side, color) ---@param invert boolean|nil
local f_read ---@type function ---@return integer count count of digital inputs
function public.link_di(side, color, invert)
local f_read ---@type function
if color then if color then
f_read = function () if invert then
return digital_read(rs.testBundledInput(side, color)) f_read = function () return digital_read(not phy.testBundledInput(side, color)) end
else
f_read = function () return digital_read(phy.testBundledInput(side, color)) end
end end
else else
f_read = function () if invert then
return digital_read(rs.getInput(side)) f_read = function () return digital_read(not phy.getInput(side)) end
else
f_read = function () return digital_read(phy.getInput(side)) end
end end
end end
unit.connect_di(f_read) return unit.connect_di(f_read)
end end
-- link digital output -- link digital output
---@param side string ---@param side string
---@param color integer ---@param color integer
function public.link_do(side, color) ---@param invert boolean|nil
local f_read ---@type function ---@return integer count count of digital outputs
local f_write ---@type function function public.link_do(side, color, invert)
local f_read ---@type function
local f_write ---@type function
if color then if color then
f_read = function () if invert then
return digital_read(colors.test(rs.getBundledOutput(side), color)) f_read = function () return digital_read(not colors.test(phy.getBundledOutput(side), color)) end
end
f_write = function (level) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = rs.getBundledOutput(side) local output = phy.getBundledOutput(side)
if digital_write(level) then -- inverted conditions
output = colors.combine(output, color) if digital_write(level) then
else output = colors.subtract(output, color)
output = colors.subtract(output, color) else output = colors.combine(output, color) end
phy.setBundledOutput(side, output)
end end
end
else
f_read = function () return digital_read(colors.test(phy.getBundledOutput(side), color)) end
rs.setBundledOutput(side, output) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
local output = phy.getBundledOutput(side)
if digital_write(level) then
output = colors.combine(output, color)
else output = colors.subtract(output, color) end
phy.setBundledOutput(side, output)
end
end end
end end
else else
f_read = function () if invert then
return digital_read(rs.getOutput(side)) f_read = function () return digital_read(not phy.getOutput(side)) end
end
f_write = function (level) f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
rs.setOutput(side, digital_write(level)) phy.setOutput(side, not digital_write(level))
end
end
else
f_read = function () return digital_read(phy.getOutput(side)) end
f_write = function (level)
if level ~= IO_LVL.FLOATING and level ~= IO_LVL.DISCONNECT then
phy.setOutput(side, digital_write(level))
end
end end
end end
end end
unit.connect_coil(f_read, f_write) return unit.connect_coil(f_read, f_write)
end end
-- link analog input -- link analog input
---@param side string ---@param side string
---@return integer count count of analog inputs
function public.link_ai(side) function public.link_ai(side)
unit.connect_input_reg( return unit.connect_input_reg(function () return phy.getAnalogInput(side) end)
function ()
return rs.getAnalogInput(side)
end
)
end end
-- link analog output -- link analog output
---@param side string ---@param side string
---@return integer count count of analog outputs
function public.link_ao(side) function public.link_ao(side)
unit.connect_holding_reg( return unit.connect_holding_reg(
function () function () return phy.getAnalogOutput(side) end,
return rs.getAnalogOutput(side) function (value) phy.setAnalogOutput(side, value) end
end,
function (value)
rs.setAnalogOutput(side, value)
end
) )
end end

View File

@@ -399,43 +399,41 @@ function modbus.new(rtu_dev, use_parallel_read)
return public return public
end end
-- create an error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@param code MODBUS_EXCODE exception code
---@return modbus_packet reply
local function excode_reply(packet, code)
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
reply.make(packet.txn_id, packet.unit_id, fcode, { code })
return reply
end
-- return a SERVER_DEVICE_FAIL error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_fail(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_FAIL) end
-- return a SERVER_DEVICE_BUSY error reply -- return a SERVER_DEVICE_BUSY error reply
---@nodiscard ---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet) function modbus.reply__srv_device_busy(packet) return excode_reply(packet, MODBUS_EXCODE.SERVER_DEVICE_BUSY) end
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a NEG_ACKNOWLEDGE error reply -- return a NEG_ACKNOWLEDGE error reply
---@nodiscard ---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__neg_ack(packet) function modbus.reply__neg_ack(packet) return excode_reply(packet, MODBUS_EXCODE.NEG_ACKNOWLEDGE) end
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply -- return a GATEWAY_PATH_UNAVAILABLE error reply
---@nodiscard ---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet) function modbus.reply__gw_unavailable(packet) return excode_reply(packet, MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE) end
-- reply back with error flag and exception code
local reply = comms.modbus_packet()
local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG)
local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE }
reply.make(packet.txn_id, packet.unit_id, fcode, data)
return reply
end
return modbus return modbus

View File

@@ -19,7 +19,8 @@ local LED = require("graphics.elements.indicators.LED")
local LEDPair = require("graphics.elements.indicators.LEDPair") local LEDPair = require("graphics.elements.indicators.LEDPair")
local RGBLED = require("graphics.elements.indicators.RGBLED") local RGBLED = require("graphics.elements.indicators.RGBLED")
local LINK_STATE = types.PANEL_LINK_STATE local LINK_STATE = types.PANEL_LINK_STATE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
@@ -35,13 +36,15 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
local function init(panel, units) local function init(panel, units)
local disabled_fg = style.fp.disabled_fg local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,fg_bg=style.theme.header} TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
-- --
-- system indicators -- system indicators
-- --
local system = Div{parent=panel,width=14,height=18,x=2,y=3} local system = Div{parent=panel,width=14,height=term_h-5,x=2,y=3}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)} local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
@@ -53,7 +56,7 @@ local function init(panel, units)
local modem = LED{parent=system,label="MODEM",colors=ind_grn} local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED) network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update) network.register(databus.ps, "link_state", network.update)
else else
@@ -100,17 +103,17 @@ local function init(panel, units)
local comp_id = util.sprintf("(%d)", os.getComputerID()) local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg} TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
TextBox{parent=system,x=1,y=14,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg} TextBox{parent=system,y=term_h-5,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box} local speaker_count = DataIndicator{parent=system,x=10,y=term_h-5,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
speaker_count.register(databus.ps, "speaker_count", speaker_count.update) speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
-- --
-- about label -- about label
-- --
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg} local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"} local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"} local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -119,38 +122,53 @@ local function init(panel, units)
-- unit status list -- unit status list
-- --
local threads = Div{parent=panel,width=8,height=18,x=17,y=3} local threads = Div{parent=panel,width=8,height=term_h-3,x=17,y=3}
-- display up to 16 units -- display as many units as we can with 1 line of padding above and below
local list_length = math.min(#units, 16) local list_length = math.min(#units, term_h - 3)
-- show routine statuses -- show routine statuses
for i = 1, list_length do for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)} TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i)}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=ind_grn} local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=util.trinary(units[i].type~=RTU_UNIT_TYPE.REDSTONE,ind_grn,cpair(style.ind_bkg,style.ind_bkg))}
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update) rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end end
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3} local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3}
local relay_counter = 0
-- show hardware statuses -- show hardware statuses
for i = 1, list_length do for i = 1, list_length do
local unit = units[i] local unit = units[i]
local is_rs = unit.type == RTU_UNIT_TYPE.REDSTONE
-- hardware status -- hardware status
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}} local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update) unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index) -- unit name identifier (type + index)
local function get_name(t) return util.c(UNIT_TYPE_LABELS[t + 1], " ", util.trinary(util.is_int(unit.index), unit.index, "")) end local function get_name()
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(unit.type),width=15} if is_rs then
local is_local = unit.name == "redstone_local"
relay_counter = relay_counter + util.trinary(is_local, 0, 1)
return util.c("REDSTONE", util.trinary(is_local, "", " RELAY " .. relay_counter))
else
return util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", util.trinary(util.is_int(unit.index), unit.index, ""))
end
end
name_box.register(databus.ps, "unit_type_" .. i, function (t) name_box.set_value(get_name(t)) end) local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=get_name(),width=util.trinary(is_rs,24,15)}
name_box.register(databus.ps, "unit_type_" .. i, function () name_box.set_value(get_name()) end)
-- assignment (unit # or facility) -- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) if unit.reactor then
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,fg_bg=disabled_fg} local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
end
end end
end end

View File

@@ -46,36 +46,42 @@ function rtu.load_config()
config.FrontPanelTheme = settings.get("FrontPanelTheme") config.FrontPanelTheme = settings.get("FrontPanelTheme")
config.ColorMode = settings.get("ColorMode") config.ColorMode = settings.get("ColorMode")
return rtu.validate_config(config)
end
-- validate an RTU gateway configuration
---@param cfg rtu_config
function rtu.validate_config(cfg)
local cfv = util.new_validator() local cfv = util.new_validator()
cfv.assert_type_num(config.SpeakerVolume) cfv.assert_type_num(cfg.SpeakerVolume)
cfv.assert_range(config.SpeakerVolume, 0, 3) cfv.assert_range(cfg.SpeakerVolume, 0, 3)
cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(cfg.SVR_Channel)
cfv.assert_channel(config.RTU_Channel) cfv.assert_channel(cfg.RTU_Channel)
cfv.assert_type_num(config.ConnTimeout) cfv.assert_type_num(cfg.ConnTimeout)
cfv.assert_min(config.ConnTimeout, 2) cfv.assert_min(cfg.ConnTimeout, 2)
cfv.assert_type_num(config.TrustedRange) cfv.assert_type_num(cfg.TrustedRange)
cfv.assert_min(config.TrustedRange, 0) cfv.assert_min(cfg.TrustedRange, 0)
cfv.assert_type_str(config.AuthKey) cfv.assert_type_str(cfg.AuthKey)
if type(config.AuthKey) == "string" then if type(cfg.AuthKey) == "string" then
local len = string.len(config.AuthKey) local len = string.len(cfg.AuthKey)
cfv.assert(len == 0 or len >= 8) cfv.assert(len == 0 or len >= 8)
end end
cfv.assert_type_int(config.LogMode) cfv.assert_type_int(cfg.LogMode)
cfv.assert_range(config.LogMode, 0, 1) cfv.assert_range(cfg.LogMode, 0, 1)
cfv.assert_type_str(config.LogPath) cfv.assert_type_str(cfg.LogPath)
cfv.assert_type_bool(config.LogDebug) cfv.assert_type_bool(cfg.LogDebug)
cfv.assert_type_int(config.FrontPanelTheme) cfv.assert_type_int(cfg.FrontPanelTheme)
cfv.assert_range(config.FrontPanelTheme, 1, 2) cfv.assert_range(cfg.FrontPanelTheme, 1, 2)
cfv.assert_type_int(config.ColorMode) cfv.assert_type_int(cfg.ColorMode)
cfv.assert_range(config.ColorMode, 1, themes.COLOR_MODE.NUM_MODES) cfv.assert_range(cfg.ColorMode, 1, themes.COLOR_MODE.NUM_MODES)
cfv.assert_type_table(config.Peripherals) cfv.assert_type_table(cfg.Peripherals)
cfv.assert_type_table(config.Redstone) cfv.assert_type_table(cfg.Redstone)
return cfv.valid() return cfv.valid()
end end
@@ -332,13 +338,7 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[i] local unit = units[i]
if unit.type ~= nil then if unit.type ~= nil then
local advert = { unit.type, unit.index, unit.reactor } insert(advertisement, { unit.type, unit.index, unit.reactor or -1, unit.rs_conns })
if unit.type == RTU_UNIT_TYPE.REDSTONE then
insert(advert, unit.device)
end
insert(advertisement, advert)
end end
end end
@@ -471,9 +471,10 @@ function rtu.comms(version, nic, conn_watchdog)
local unit = units[packet.unit_id] local unit = units[packet.unit_id]
local unit_dbg_tag = " (unit " .. packet.unit_id .. ")" local unit_dbg_tag = " (unit " .. packet.unit_id .. ")"
if unit.name == "redstone_io" then if unit.type == RTU_UNIT_TYPE.REDSTONE then
-- immediately execute redstone RTU requests -- immediately execute redstone RTU requests
return_code, reply = unit.modbus_io.handle_packet(packet) return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then if not return_code then
log.warning("requested MODBUS operation failed" .. unit_dbg_tag) log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end end
@@ -490,7 +491,7 @@ function rtu.comms(version, nic, conn_watchdog)
unit.pkt_queue.push_packet(packet) unit.pkt_queue.push_packet(packet)
end end
else else
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag) log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end end
end end
else else

View File

@@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.11.0" local RTU_VERSION = "v1.12.1"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE local RTU_HW_STATE = databus.RTU_HW_STATE
@@ -140,32 +140,36 @@ local function main()
local rtu_redstone = config.Redstone local rtu_redstone = config.Redstone
local rtu_devices = config.Peripherals local rtu_devices = config.Peripherals
-- get a string representation of a port interface
---@param entry rtu_rs_definition
---@return string
local function entry_iface_name(entry)
return util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side)
end
-- configure RTU gateway based on settings file definitions -- configure RTU gateway based on settings file definitions
local function sys_config() local function sys_config()
-- redstone interfaces --#region Redstone Interfaces
local rs_rtus = {} ---@type { rtu: rtu_rs_device, capabilities: IO_PORT[] }[]
local rs_rtus = {} ---@type { name: string, hw_state: RTU_HW_STATE, rtu: rtu_rs_device, phy: table, banks: rtu_rs_definition[][] }[]
local all_conns = { [0] = {}, {}, {}, {}, {} }
-- go through redstone definitions list -- go through redstone definitions list
for entry_idx = 1, #rtu_redstone do for entry_idx = 1, #rtu_redstone do
local entry = rtu_redstone[entry_idx] local entry = rtu_redstone[entry_idx]
local assignment local assignment
local for_reactor = entry.unit local for_reactor = entry.unit
local iface_name = util.trinary(entry.color ~= nil, util.c(entry.side, "/", rsio.color_name(entry.color)), entry.side) local phy = entry.relay or 0
local phy_name = entry.relay or "local"
local iface_name = entry_iface_name(entry)
if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then if util.is_int(entry.unit) and entry.unit > 0 and entry.unit < 5 then
---@cast for_reactor integer ---@cast for_reactor integer
assignment = "reactor unit " .. entry.unit assignment = "reactor unit " .. entry.unit
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for reactor unit ", entry.unit))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
elseif entry.unit == nil then elseif entry.unit == nil then
assignment = "facility" assignment = "facility"
for_reactor = 0 for_reactor = 0
if rs_rtus[for_reactor] == nil then
log.debug(util.c("sys_config> allocated redstone RTU for the facility"))
rs_rtus[for_reactor] = { rtu = redstone_rtu.new(), capabilities = {} }
end
else else
local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx) local message = util.c("sys_config> invalid unit assignment at block index #", entry_idx)
println(message) println(message)
@@ -173,14 +177,44 @@ local function main()
return false return false
end end
-- create the appropriate RTU if it doesn't exist and check relay name validity
if entry.relay then
if type(entry.relay) ~= "string" then
local message = util.c("sys_config> invalid redstone relay '", entry.relay, '"')
println(message)
log.fatal(message)
return false
elseif not rs_rtus[entry.relay] then
log.debug(util.c("sys_config> allocated relay redstone RTU on interface ", entry.relay))
local hw_state = RTU_HW_STATE.OK
local relay = ppm.get_periph(entry.relay)
if not relay then
hw_state = RTU_HW_STATE.OFFLINE
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not connected"))
local _, v_device = ppm.mount_virtual()
relay = v_device
elseif ppm.get_type(entry.relay) ~= "redstone_relay" then
hw_state = RTU_HW_STATE.FAULTED
log.warning(util.c("sys_config> redstone relay ", entry.relay, " is not a redstone relay"))
end
rs_rtus[entry.relay] = { name = entry.relay, hw_state = hw_state, rtu = redstone_rtu.new(relay), phy = relay, banks = { [0] = {}, {}, {}, {}, {} } }
end
elseif rs_rtus[0] == nil then
log.debug(util.c("sys_config> allocated local redstone RTU"))
rs_rtus[0] = { name = "redstone_local", hw_state = RTU_HW_STATE.OK, rtu = redstone_rtu.new(), phy = rs, banks = { [0] = {}, {}, {}, {}, {} } }
end
-- verify configuration -- verify configuration
local valid = false local valid = false
if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then if rsio.is_valid_port(entry.port) and rsio.is_valid_side(entry.side) then
valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color)) valid = util.trinary(entry.color == nil, true, rsio.is_color(entry.color))
end end
local rs_rtu = rs_rtus[for_reactor].rtu local bank = rs_rtus[phy].banks[for_reactor]
local capabilities = rs_rtus[for_reactor].capabilities local conns = all_conns[for_reactor]
if not valid then if not valid then
local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx) local message = util.c("sys_config> invalid redstone definition at block index #", entry_idx)
@@ -192,73 +226,105 @@ local function main()
local mode = rsio.get_io_mode(entry.port) local mode = rsio.get_io_mode(entry.port)
if mode == rsio.IO_MODE.DIGITAL_IN then if mode == rsio.IO_MODE.DIGITAL_IN then
-- can't have duplicate inputs -- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name) local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message) println(message)
log.warning(message) log.warning(message)
else else
rs_rtu.link_di(entry.side, entry.color) table.insert(bank, entry)
end end
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
rs_rtu.link_do(entry.side, entry.color)
elseif mode == rsio.IO_MODE.ANALOG_IN then elseif mode == rsio.IO_MODE.ANALOG_IN then
-- can't have duplicate inputs -- can't have duplicate inputs
if util.table_contains(capabilities, entry.port) then if util.table_contains(conns, entry.port) then
local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name) local message = util.c("sys_config> skipping duplicate input for port ", rsio.to_string(entry.port), " on side ", iface_name, " @ ", phy_name)
println(message) println(message)
log.warning(message) log.warning(message)
else else
rs_rtu.link_ai(entry.side) table.insert(bank, entry)
end end
elseif mode == rsio.IO_MODE.ANALOG_OUT then elseif (mode == rsio.IO_MODE.DIGITAL_OUT) or (mode == rsio.IO_MODE.ANALOG_OUT) then
rs_rtu.link_ao(entry.side) table.insert(bank, entry)
else else
-- should be unreachable code, we already validated ports -- should be unreachable code, we already validated ports
log.error("sys_config> fell through if chain attempting to identify IO mode at block index #" .. entry_idx, true) log.fatal("sys_config> failed to identify IO mode at block index #" .. entry_idx)
println("sys_config> encountered a software error, check logs") println("sys_config> encountered a software error, check logs")
return false return false
end end
table.insert(capabilities, entry.port) table.insert(conns, entry.port)
log.debug(util.c("sys_config> linked redstone ", #capabilities, ": ", rsio.to_string(entry.port), " (", iface_name, ") for ", assignment)) log.debug(util.c("sys_config> banked redstone ", #conns, ": ", rsio.to_string(entry.port), " (", iface_name, " @ ", phy_name, ") for ", assignment))
end end
end end
-- create unit entries for redstone RTUs -- create unit entries for redstone RTUs
for for_reactor, def in pairs(rs_rtus) do for _, def in pairs(rs_rtus) do
---@class rtu_registry_entry local rtu_conns = { [0] = {}, {}, {}, {}, {} }
-- connect the IO banks
for for_reactor = 0, #def.banks do
local bank = def.banks[for_reactor]
local conns = rtu_conns[for_reactor]
local assign = util.trinary(for_reactor > 0, "reactor unit " .. for_reactor, "the facility")
-- link redstone to the RTU
for i = 1, #bank do
local conn = bank[i]
local phy_name = conn.relay or "local"
local mode = rsio.get_io_mode(conn.port)
if mode == rsio.IO_MODE.DIGITAL_IN then
def.rtu.link_di(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.DIGITAL_OUT then
def.rtu.link_do(conn.side, conn.color, conn.invert)
elseif mode == rsio.IO_MODE.ANALOG_IN then
def.rtu.link_ai(conn.side)
elseif mode == rsio.IO_MODE.ANALOG_OUT then
def.rtu.link_ao(conn.side)
else
log.fatal(util.c("sys_config> failed to identify IO mode of ", rsio.to_string(conn.port), " (", entry_iface_name(conn), " @ ", phy_name, ") for ", assign))
println("sys_config> encountered a software error, check logs")
return false
end
table.insert(conns, conn.port)
log.debug(util.c("sys_config> linked redstone ", for_reactor, ".", #conns, ": ", rsio.to_string(conn.port), " (", entry_iface_name(conn), ")", " @ ", phy_name, ") for ", assign))
end
end
---@type rtu_registry_entry
local unit = { local unit = {
uid = 0, ---@type integer uid = 0,
name = "redstone_io", ---@type string name = def.name,
type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE type = RTU_UNIT_TYPE.REDSTONE,
index = false, ---@type integer|false index = false,
reactor = for_reactor, ---@type integer reactor = nil,
device = def.capabilities, ---@type IO_PORT[] use device field for redstone ports device = def.phy,
is_multiblock = false, ---@type boolean rs_conns = rtu_conns,
formed = nil, ---@type boolean|nil is_multiblock = false,
hw_state = RTU_HW_STATE.OK, ---@type RTU_HW_STATE formed = nil,
rtu = def.rtu, ---@type rtu_device|rtu_rs_device hw_state = def.hw_state,
rtu = def.rtu,
modbus_io = modbus.new(def.rtu, false), modbus_io = modbus.new(def.rtu, false),
pkt_queue = nil, ---@type mqueue|nil pkt_queue = nil,
thread = nil ---@type parallel_thread|nil thread = nil
} }
table.insert(units, unit) table.insert(units, unit)
local for_message = "facility" local type = util.trinary(def.phy == rs, "redstone", "redstone_relay")
if util.is_int(for_reactor) then
for_message = util.c("reactor unit ", for_reactor)
end
log.info(util.c("sys_config> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message)) log.info(util.c("sys_config> initialized RTU unit #", #units, ": ", unit.name, " (", type, ")"))
unit.uid = #units unit.uid = #units
databus.tx_unit_hw_status(unit.uid, unit.hw_state) databus.tx_unit_hw_status(unit.uid, unit.hw_state)
end end
-- mounted peripherals --#endregion
--#region Mounted Peripherals
for i = 1, #rtu_devices do for i = 1, #rtu_devices do
local entry = rtu_devices[i] ---@type rtu_peri_definition local entry = rtu_devices[i] ---@type rtu_peri_definition
local name = entry.name local name = entry.name
@@ -439,19 +505,20 @@ local function main()
---@class rtu_registry_entry ---@class rtu_registry_entry
local rtu_unit = { local rtu_unit = {
uid = 0, ---@type integer uid = 0, ---@type integer RTU unit ID
name = name, ---@type string name = name, ---@type string unit name
type = rtu_type, ---@type RTU_UNIT_TYPE type = rtu_type, ---@type RTU_UNIT_TYPE unit type
index = index or false, ---@type integer|false index = index or false, ---@type integer|false device index
reactor = for_reactor, ---@type integer reactor = for_reactor, ---@type integer|nil unit/facility assignment
device = device, ---@type table peripheral reference device = device, ---@type table peripheral reference
is_multiblock = is_multiblock, ---@type boolean rs_conns = nil, ---@type IO_PORT[][]|nil available redstone connections
formed = formed, ---@type boolean|nil is_multiblock = is_multiblock, ---@type boolean if this is for a multiblock peripheral
hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE formed = formed, ---@type boolean|nil if this peripheral is currently formed
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device hw_state = RTU_HW_STATE.OFFLINE, ---@type RTU_HW_STATE hardware device status
modbus_io = modbus.new(rtu_iface, true), rtu = rtu_iface, ---@type rtu_device|rtu_rs_device RTU hardware interface
pkt_queue = mqueue.new(), ---@type mqueue|nil modbus_io = modbus.new(rtu_iface, true), ---@type modbus MODBUS interface
thread = nil ---@type parallel_thread|nil pkt_queue = mqueue.new(), ---@type mqueue|nil packet queue
thread = nil ---@type parallel_thread|nil associated RTU thread
} }
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
@@ -485,6 +552,8 @@ local function main()
databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state) databus.tx_unit_hw_status(rtu_unit.uid, rtu_unit.hw_state)
end end
--#endregion
return true return true
end end
@@ -495,17 +564,6 @@ local function main()
log.debug("boot> running sys_config()") log.debug("boot> running sys_config()")
if sys_config() then if sys_config() then
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- check modem -- check modem
if smem_dev.modem == nil then if smem_dev.modem == nil then
println("startup> wireless modem not found") println("startup> wireless modem not found")
@@ -527,6 +585,17 @@ local function main()
databus.tx_hw_spkr_count(#smem_dev.sounders) databus.tx_hw_spkr_count(#smem_dev.sounders)
-- start UI
local message
rtu_state.fp_ok, message = renderer.try_start_ui(units, config.FrontPanelTheme, config.ColorMode)
if not rtu_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("startup> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("startup> running in headless mode without front panel")
end
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout) smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")
@@ -553,7 +622,7 @@ local function main()
-- run threads -- run threads
parallel.waitForAll(table.unpack(_threads)) parallel.waitForAll(table.unpack(_threads))
else else
println("configuration failed, exiting...") println("system initialization failed, exiting...")
end end
renderer.close_ui() renderer.close_ui()

View File

@@ -132,6 +132,8 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
unit.rtu, faulted = sna_rtu.new(device) unit.rtu, faulted = sna_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu, faulted = envd_rtu.new(device) unit.rtu, faulted = envd_rtu.new(device)
elseif unit.type == RTU_UNIT_TYPE.REDSTONE then
unit.rtu.remount_phy(device)
else else
unknown = true unknown = true
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)

View File

@@ -17,7 +17,7 @@ local max_distance = nil
local comms = {} local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version) -- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "3.0.4" comms.version = "3.0.6"
comms.api_version = "0.0.9" comms.api_version = "0.0.9"
---@enum PROTOCOL ---@enum PROTOCOL
@@ -60,18 +60,19 @@ local MGMT_TYPE = {
---@enum CRDN_TYPE ---@enum CRDN_TYPE
local CRDN_TYPE = { local CRDN_TYPE = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds PROCESS_READY = 1, -- process init is complete + last set of info for supervisor startup recovery
FAC_STATUS = 2, -- state of facility and facility devices FAC_BUILDS = 2, -- facility RTU builds
FAC_CMD = 3, -- faility command FAC_STATUS = 3, -- state of facility and facility devices
UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs) FAC_CMD = 4, -- faility command
UNIT_STATUSES = 5, -- state of each of the reactor units UNIT_BUILDS = 5, -- build of each reactor unit (reactor + RTUs)
UNIT_CMD = 6, -- command a reactor unit UNIT_STATUSES = 6, -- state of each of the reactor units
API_GET_FAC = 7, -- API: get the facility general data UNIT_CMD = 7, -- command a reactor unit
API_GET_FAC_DTL = 8, -- API: get (detailed) data for the facility app API_GET_FAC = 8, -- API: get the facility general data
API_GET_UNIT = 9, -- API: get reactor unit data API_GET_FAC_DTL = 9, -- API: get (detailed) data for the facility app
API_GET_CTRL = 10, -- API: get data for the control app API_GET_UNIT = 10, -- API: get reactor unit data
API_GET_PROC = 11, -- API: get data for the process app API_GET_CTRL = 11, -- API: get data for the control app
API_GET_WASTE = 12 -- API: get data for the waste app API_GET_PROC = 12, -- API: get data for the process app
API_GET_WASTE = 13 -- API: get data for the waste app
} }
---@enum ESTABLISH_ACK ---@enum ESTABLISH_ACK

View File

@@ -72,6 +72,8 @@ local rs = {}
rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW 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 rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH
rs.AUX_COOL_ENABLE = 0.60 -- actiation threshold (less than or equal) for U_AUX_COOL
rs.AUX_COOL_DISABLE = 1.00 -- deactivation threshold (greater than or equal) for U_AUX_COOL
constants.RS_THRESHOLDS = rs constants.RS_THRESHOLDS = rs

View File

@@ -2,6 +2,9 @@
-- Crash Handler -- Crash Handler
-- --
---@diagnostic disable-next-line: undefined-global
local _is_pocket_env = pocket -- luacheck: ignore pocket
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
@@ -36,6 +39,74 @@ local function log_versions(log_msg)
if has_lockbox then log_msg(util.c("LOCKBOX VERSION: ", lockbox.version)) end if has_lockbox then log_msg(util.c("LOCKBOX VERSION: ", lockbox.version)) end
end end
-- render the standard computer crash screen
---@param exit function callback on exit button press
---@return DisplayBox display
local function draw_computer_crash(exit)
local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)}
local warning = Div{parent=display,x=2,y=2}
TextBox{parent=warning,x=7,text="\x90\n \x90\n \x90\n \x90\n \x90",fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=5,y=1,text="\x9f ",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=4,text="\x9f ",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=3,text="\x9f ",width=6,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=2,text="\x9f ",width=8,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,text="\x9f ",width=10,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,text="\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f\x8f",width=11,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=6,y=3,text=" \n \x83",width=1,fg_bg=core.cpair(colors.yellow,colors.white)}
TextBox{parent=display,x=13,y=2,text="Critical Software Fault Encountered",alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)}
TextBox{parent=display,x=15,y=4,text="Please consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER}
TextBox{parent=display,x=14,y=7,text="refer to the log file for more info",alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors._INHERIT)}
local box = Rectangle{parent=display,x=2,y=9,width=display.get_width()-2,height=8,border=core.border(1,colors.gray,true),thin=true,fg_bg=core.cpair(colors.black,colors.white)}
TextBox{parent=box,text=err}
PushButton{parent=display,x=23,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)}
return display
end
-- render the pocket crash screen
---@param exit function callback on exit button press
---@return DisplayBox display
local function draw_pocket_crash(exit)
local DisplayBox = require("graphics.elements.DisplayBox")
local Div = require("graphics.elements.Div")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local display = DisplayBox{window=term.current(),fg_bg=core.cpair(colors.white,colors.lightGray)}
local warning = Div{parent=display,x=2,y=1}
TextBox{parent=warning,x=4,y=1,text="\x90",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=3,text="\x81 ",width=2,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=5,y=2,text="\x94",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=2,text="\x81 ",width=4,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=6,y=3,text="\x94",width=1,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,text="\x8e\x8f\x8f\x8e\x8f\x8f\x84",width=7,fg_bg=core.cpair(colors.yellow,colors.lightGray)}
TextBox{parent=warning,x=4,y=2,text="\x90",width=1,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=warning,x=4,y=3,text="\x85",width=1,fg_bg=core.cpair(colors.lightGray,colors.yellow)}
TextBox{parent=display,x=10,y=2,text=" Critical Software Fault",width=16,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.yellow,colors._INHERIT)}
TextBox{parent=display,x=2,y=5,text="Consider reporting this on the cc-mek-scada Discord or GitHub.",width=36,alignment=core.ALIGN.CENTER}
local box = Rectangle{parent=display,y=9,width=display.get_width(),height=8,fg_bg=core.cpair(colors.black,colors.white)}
TextBox{parent=box,text=err}
PushButton{parent=display,x=11,y=18,text=" Exit ",callback=exit,active_fg_bg=core.cpair(colors.white,colors.gray),fg_bg=core.cpair(colors.black,colors.red)}
TextBox{parent=display,x=2,y=20,text="see logs for details",width=24,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors._INHERIT)}
return display
end
-- when running with debug logs, log the useful information that the crash handler knows -- when running with debug logs, log the useful information that the crash handler knows
function crash.dbg_log_env() log_versions(log.debug) end function crash.dbg_log_env() log_versions(log.debug) end
@@ -54,9 +125,41 @@ end
-- final error print on failed xpcall, app exits here -- final error print on failed xpcall, app exits here
function crash.exit() function crash.exit()
local handled, run = false, true
local display ---@type DisplayBox
-- special graphical crash screen
if has_graphics then
handled, display = pcall(util.trinary(_is_pocket_env, draw_pocket_crash, draw_computer_crash), function () run = false end)
-- event loop
while display and run do
local event, param1, param2, param3 = util.pull_event()
-- handle event
if event == "mouse_click" or event == "mouse_up" or event == "double_click" then
local mouse = core.events.new_mouse_event(event, param1, param2, param3)
if mouse then display.handle_mouse(mouse) end
elseif event == "terminate" then
break
end
end
display.delete()
term.setCursorPos(1, 1)
term.setTextColor(colors.white)
term.setBackgroundColor(colors.black)
term.clear()
end
log.close() log.close()
util.println("fatal error occured in main application:")
error(err, 0) -- default text failure message
if not handled then
util.println("fatal error occured in main application:")
error(err, 0)
end
end end
return crash return crash

View File

@@ -78,6 +78,7 @@ local IO_PORT = {
-- unit outputs -- unit outputs
U_ALARM = 25, -- active high, unit alarm 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
U_AUX_COOL = 30, -- active low, auxiliary coolant control
-- analog outputs -- -- analog outputs --
@@ -90,8 +91,8 @@ rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT rsio.IO = IO_PORT
rsio.NUM_PORTS = 29 rsio.NUM_PORTS = 30
rsio.NUM_DIG_PORTS = 28 rsio.NUM_DIG_PORTS = 29
rsio.NUM_ANA_PORTS = 1 rsio.NUM_ANA_PORTS = 1
-- self checks -- self checks
@@ -149,6 +150,7 @@ local MODES = {
[IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT, [IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT, [IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT, [IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
[IO.U_AUX_COOL] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT [IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
} }
@@ -208,10 +210,11 @@ local RS_DIO_MAP = {
[IO.R_PLC_TIMEOUT] = { _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 },
[IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, 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 } [IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.U_AUX_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
} }
assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect") assert(rsio.NUM_DIG_PORTS == util.table_len(RS_DIO_MAP), "RS_DIO_MAP length incorrect")
-- get the I/O direction of a port -- get the I/O direction of a port
---@nodiscard ---@nodiscard

View File

@@ -125,7 +125,7 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@field type RTU_UNIT_TYPE ---@field type RTU_UNIT_TYPE
---@field index integer|false ---@field index integer|false
---@field reactor integer ---@field reactor integer
---@field rsio IO_PORT[]|nil ---@field rs_conns IO_PORT[][]|nil
-- create a new reactor database -- create a new reactor database
---@nodiscard ---@nodiscard
@@ -465,7 +465,8 @@ types.ALARM = {
ReactorHighWaste = 9, ReactorHighWaste = 9,
RPSTransient = 10, RPSTransient = 10,
RCSTransient = 11, RCSTransient = 11,
TurbineTrip = 12 TurbineTrip = 12,
FacilityRadiation = 13
} }
types.ALARM_NAMES = { types.ALARM_NAMES = {
@@ -480,7 +481,8 @@ types.ALARM_NAMES = {
"ReactorHighWaste", "ReactorHighWaste",
"RPSTransient", "RPSTransient",
"RCSTransient", "RCSTransient",
"TurbineTrip" "TurbineTrip",
"FacilityRadiation"
} }
---@enum ALARM_PRIORITY ---@enum ALARM_PRIORITY

View File

@@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.4.10" util.version = "1.5.2"
util.TICK_TIME_S = 0.05 util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50 util.TICK_TIME_MS = 50

137
supervisor/alarm_ctl.lua Normal file
View File

@@ -0,0 +1,137 @@
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM_STATE = types.ALARM_STATE
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
local alarm_ctl = {}
alarm_ctl.AISTATE = AISTATE
alarm_ctl.AISTATE_NAMES = AISTATE_NAMES
-- update an alarm state based on its current status and if it is tripped
---@param caller_tag string tag to use in log messages
---@param alarm_states { [ALARM]: ALARM_STATE } unit instance
---@param tripped boolean if the alarm condition is sti ll active
---@param alarm alarm_def alarm table
---@param no_ring_back boolean? true to skip the ring back state, returning to inactive instead
---@return boolean new_trip if the alarm just changed to being tripped
function alarm_ctl.update_alarm_state(caller_tag, alarm_states, tripped, alarm, no_ring_back)
local int_state = alarm.state
local ext_state = alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
elseif no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
if no_ring_back then
alarm.state = AISTATE.INACTIVE
alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.RING_BACK
alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c(caller_tag, " invalid alarm state for alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c(caller_tag, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
end
return alarm_ctl

View File

@@ -185,8 +185,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
local fac_c_6 = Div{parent=fac_cfg,x=2,y=4,width=49} local fac_c_6 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_7 = Div{parent=fac_cfg,x=2,y=4,width=49} local fac_c_7 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_8 = Div{parent=fac_cfg,x=2,y=4,width=49} local fac_c_8 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_9 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7, fac_c_8}} local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7,fac_c_8,fac_c_9}}
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
@@ -205,10 +206,18 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
nu_error.hide(true) nu_error.hide(true)
tmp_cfg.UnitCount = count tmp_cfg.UnitCount = count
local confs = tool_ctl.cooling_elems local c_confs = tool_ctl.cooling_elems
if count >= 2 then confs[2].line.show() else confs[2].line.hide(true) end local a_confs = tool_ctl.aux_cool_elems
if count >= 3 then confs[3].line.show() else confs[3].line.hide(true) end
if count == 4 then confs[4].line.show() else confs[4].line.hide(true) end for i = 2, 4 do
if count >= i then
c_confs[i].line.show()
a_confs[i].line.show()
else
c_confs[i].line.hide(true)
a_confs[i].line.hide(true)
end
end
fac_pane.set_value(2) fac_pane.set_value(2)
else nu_error.show() end else nu_error.show() end
@@ -285,6 +294,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
else elem.div.hide(true) end else elem.div.hide(true) end
end end
if not any_has_tank then
tmp_cfg.FacilityTankMode = 0
tmp_cfg.FacilityTankDefs = {}
tmp_cfg.FacilityTankList = {}
tmp_cfg.FacilityTankConns = {}
tmp_cfg.TankFluidTypes = {}
end
if any_has_tank then fac_pane.set_value(3) else main_pane.set_value(3) end if any_has_tank then fac_pane.set_value(3) else main_pane.set_value(3) end
end end
end end
@@ -672,25 +689,48 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_tank_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_tank_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Auxiliary Coolant
TextBox{parent=fac_c_8,height=5,text="Auxiliary water coolant can be enabled for units to provide extra water during turbine ramp-up. For water cooled reactors, this goes to the reactor. For sodium cooled reactors, water goes to the boiler."}
for i = 1, 4 do
local line = Div{parent=fac_c_8,x=1,y=7+i,height=1}
TextBox{parent=line,text="Unit "..i.." -",width=8}
local aux_cool = Checkbox{parent=line,x=10,y=1,label="Has Auxiliary Coolant",default=ini_cfg.AuxiliaryCoolant[i],box_fg_bg=cpair(colors.yellow,colors.black)}
tool_ctl.aux_cool_elems[i] = { line = line, enable = aux_cool }
end
local function submit_aux_cool()
tmp_cfg.AuxiliaryCoolant = {}
for i = 1, tmp_cfg.UnitCount do
tmp_cfg.AuxiliaryCoolant[i] = tool_ctl.aux_cool_elems[i].enable.get_value()
end
fac_pane.set_value(9)
end
PushButton{parent=fac_c_8,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_8,x=44,y=14,text="Next \x1a",callback=submit_aux_cool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
--#region Extended Idling --#region Extended Idling
TextBox{parent=fac_c_8,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=fac_c_9,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=fac_c_8,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."} TextBox{parent=fac_c_9,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=fac_c_8,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)} local ext_idling = Checkbox{parent=fac_c_9,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()
fac_pane.set_value(tri(tmp_cfg.FacilityTankMode == 0, 3, 7))
end
local function submit_idling() local function submit_idling()
tmp_cfg.ExtChargeIdling = ext_idling.get_value() tmp_cfg.ExtChargeIdling = ext_idling.get_value()
main_pane.set_value(3) main_pane.set_value(3)
end end
PushButton{parent=fac_c_8,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=fac_c_9,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_8,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=fac_c_9,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion

View File

@@ -402,6 +402,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
try_set(tool_ctl.tank_elems[i].tank_opt, ini_cfg.FacilityTankDefs[i]) try_set(tool_ctl.tank_elems[i].tank_opt, ini_cfg.FacilityTankDefs[i])
end end
for i = 1, #ini_cfg.AuxiliaryCoolant do
try_set(tool_ctl.aux_cool_elems[i].enable, ini_cfg.AuxiliaryCoolant[i])
end
tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0) tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
tool_ctl.view_cfg.enable() tool_ctl.view_cfg.enable()
@@ -588,7 +592,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end end
if val == "" then val = "no facility tanks" end if val == "" then val = "no facility tanks" end
elseif f[1] == "FacilityTankMode" and raw == 0 then val = "0 (n/a, unit mode)" elseif f[1] == "FacilityTankMode" and raw == 0 then val = "no facility tanks"
elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then
local tank_name_list = { table.unpack(cfg.FacilityTankList) } ---@type (string|integer)[] local tank_name_list = { table.unpack(cfg.FacilityTankList) } ---@type (string|integer)[]
local next_f = 1 local next_f = 1
@@ -625,6 +629,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
val = "" val = ""
local count = 0
for idx = 1, #tank_list do
if tank_list[idx] > 0 then count = count + 1 end
end
local bullet = tri(count < 2, "", " \x07 ")
for idx = 1, #tank_list do for idx = 1, #tank_list do
local prefix = "?" local prefix = "?"
local fluid = "water" local fluid = "water"
@@ -642,11 +653,28 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
fluid = "sodium" fluid = "sodium"
end end
val = val .. tri(val == "", "", "\n") .. util.sprintf(" \x07 tank %s - %s", prefix, fluid) val = val .. tri(val == "", "", "\n") .. util.sprintf(bullet .. "tank %s - %s", prefix, fluid)
end end
end end
if val == "" then val = "no emergency coolant tanks" end if val == "" then val = "no emergency coolant tanks" end
elseif f[1] == "AuxiliaryCoolant" then
val = ""
local count = 0
for idx = 1, #cfg.AuxiliaryCoolant do
if cfg.AuxiliaryCoolant[idx] then count = count + 1 end
end
local bullet = tri(count < 2, "", " \x07 ")
for idx = 1, #cfg.AuxiliaryCoolant do
if cfg.AuxiliaryCoolant[idx] then
val = val .. tri(val == "", "", "\n") .. util.sprintf(bullet .. "unit %d", idx)
end
end
if val == "" then val = "no auxiliary coolant" end
end end
if not skip then if not skip then
@@ -655,7 +683,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
local c = tri(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 alternate = not alternate
if string.len(val) > val_max_w then if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width) local lines = util.strwrap(val, inner_width)
height = #lines + 1 height = #lines + 1
end end

View File

@@ -72,7 +72,8 @@ local tool_ctl = {
load_legacy = nil, ---@type function load_legacy = nil, ---@type function
cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[] cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[]
tank_elems = {} ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[] tank_elems = {}, ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
aux_cool_elems = {} ---@type { line: Div, enable: Checkbox }[]
} }
---@class svr_config ---@class svr_config
@@ -84,6 +85,7 @@ local tmp_cfg = {
FacilityTankList = {}, ---@type integer[] list of tanks by slot (0 = none or covered by an above tank, 1 = unit tank, 2 = facility tank) FacilityTankList = {}, ---@type integer[] list of tanks by slot (0 = none or covered by an above tank, 1 = unit tank, 2 = facility tank)
FacilityTankConns = {}, ---@type integer[] map of unit tank connections (indicies are units, values are tank indicies in the tank list) FacilityTankConns = {}, ---@type integer[] map of unit tank connections (indicies are units, values are tank indicies in the tank list)
TankFluidTypes = {}, ---@type integer[] which type of fluid each tank in the tank list should be containing TankFluidTypes = {}, ---@type integer[] which type of fluid each tank in the tank list should be containing
AuxiliaryCoolant = {}, ---@type boolean[] if a unit has auxiliary coolant
ExtChargeIdling = false, ExtChargeIdling = false,
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer PLC_Channel = nil, ---@type integer
@@ -117,6 +119,7 @@ local fields = {
{ "FacilityTankList", "Facility Tank List", {} }, -- hidden { "FacilityTankList", "Facility Tank List", {} }, -- hidden
{ "FacilityTankConns", "Facility Tank Connections", {} }, -- hidden { "FacilityTankConns", "Facility Tank Connections", {} }, -- hidden
{ "TankFluidTypes", "Tank Fluid Types", {} }, { "TankFluidTypes", "Tank Fluid Types", {} },
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
{ "ExtChargeIdling", "Extended Charge Idling", false }, { "ExtChargeIdling", "Extended Charge Idling", false },
{ "SVR_Channel", "SVR Channel", 16240 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 }, { "PLC_Channel", "PLC Channel", 16241 },

View File

@@ -2,13 +2,19 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local alarm_ctl = require("supervisor.alarm_ctl")
local unit = require("supervisor.unit") local unit = require("supervisor.unit")
local fac_update = require("supervisor.facility_update") local fac_update = require("supervisor.facility_update")
local rsctl = require("supervisor.session.rsctl") local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local AISTATE = alarm_ctl.AISTATE
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP local AUTO_GROUP = types.AUTO_GROUP
local PRIO = types.ALARM_PRIORITY
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@@ -31,6 +37,17 @@ local START_STATUS = {
BLADE_MISMATCH = 2 BLADE_MISMATCH = 2
} }
---@enum RECOVERY_STATE
local RCV_STATE = {
INACTIVE = 0,
PRIMED = 1,
RUNNING = 2,
STOPPED = 3
}
local CHARGE_SCALER = 1000000 -- convert MFE to FE
local GEN_SCALER = 1000 -- convert kFE to FE
---@class facility_management ---@class facility_management
local facility = {} local facility = {}
@@ -41,7 +58,7 @@ function facility.new(config)
---@class _facility_self ---@class _facility_self
local self = { local self = {
units = {}, ---@type reactor_unit[] units = {}, ---@type reactor_unit[]
types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS }, types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS, RCV_STATE = RCV_STATE },
status_text = { "START UP", "initializing..." }, status_text = { "START UP", "initializing..." },
all_sys_ok = false, all_sys_ok = false,
allow_testing = false, allow_testing = false,
@@ -53,7 +70,8 @@ function facility.new(config)
fac_tank_defs = config.FacilityTankDefs, fac_tank_defs = config.FacilityTankDefs,
fac_tank_list = config.FacilityTankList, fac_tank_list = config.FacilityTankList,
fac_tank_conns = config.FacilityTankConns, fac_tank_conns = config.FacilityTankConns,
tank_fluid_types = config.TankFluidTypes tank_fluid_types = config.TankFluidTypes,
aux_coolant = config.AuxiliaryCoolant
}, },
-- rtus -- rtus
rtu_gw_conn_count = 0, rtu_gw_conn_count = 0,
@@ -66,12 +84,15 @@ function facility.new(config)
-- redstone I/O control -- redstone I/O control
io_ctl = nil, ---@type rs_controller io_ctl = nil, ---@type rs_controller
-- process control -- process control
recovery = RCV_STATE.INACTIVE, ---@type RECOVERY_STATE
recovery_boot_state = nil, ---@type sv_boot_state|nil
last_unit_states = {}, ---@type boolean[]
units_ready = false, units_ready = false,
mode = PROCESS.INACTIVE, mode = PROCESS.INACTIVE, ---@type PROCESS
last_mode = PROCESS.INACTIVE, last_mode = PROCESS.INACTIVE, ---@type PROCESS
return_mode = PROCESS.INACTIVE, return_mode = PROCESS.INACTIVE, ---@type PROCESS
mode_set = PROCESS.MAX_BURN, mode_set = PROCESS.MAX_BURN, ---@type PROCESS
start_fail = START_STATUS.OK, start_fail = START_STATUS.OK, ---@type START_STATUS
max_burn_combined = 0.0, -- maximum burn rate to clamp at max_burn_combined = 0.0, -- maximum burn rate to clamp at
burn_target = 0.1, -- burn rate target for aggregate burn mode burn_target = 0.1, -- burn rate target for aggregate burn mode
charge_setpoint = 0, -- FE charge target setpoint charge_setpoint = 0, -- FE charge target setpoint
@@ -101,8 +122,8 @@ function facility.new(config)
last_error = 0.0, last_error = 0.0,
last_time = 0.0, last_time = 0.0,
-- waste processing -- waste processing
waste_product = WASTE.PLUTONIUM, waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
current_waste_product = WASTE.PLUTONIUM, current_waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
pu_fallback = false, pu_fallback = false,
sps_low_power = false, sps_low_power = false,
disabled_sps = false, disabled_sps = false,
@@ -123,24 +144,36 @@ function facility.new(config)
imtx_last_charge = 0, imtx_last_charge = 0,
imtx_last_charge_t = 0, imtx_last_charge_t = 0,
-- track faulted induction matrix update times to reject -- track faulted induction matrix update times to reject
imtx_faulted_times = { 0, 0, 0 } imtx_faulted_times = { 0, 0, 0 },
-- facility alarms
---@type { [string]: alarm_def }
alarms = {
-- radiation monitor alarm for the facility
FacilityRadiation = { state = AISTATE.INACTIVE, trip_time = 0, hold_time = 0, id = ALARM.FacilityRadiation, tier = PRIO.CRITICAL },
},
---@type { [ALARM]: ALARM_STATE }
alarm_states = {
[ALARM.FacilityRadiation] = ALARM_STATE.INACTIVE
}
} }
--#region SETUP
-- provide self to facility update functions -- provide self to facility update functions
local f_update = fac_update(self) local f_update = fac_update(self)
-- create units -- create units
for i = 1, config.UnitCount do for i = 1, config.UnitCount do
table.insert(self.units, table.insert(self.units, unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling, self.cooling_conf.aux_coolant[i]))
unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling))
table.insert(self.group_map, AUTO_GROUP.MANUAL) table.insert(self.group_map, AUTO_GROUP.MANUAL)
table.insert(self.last_unit_states, false)
end end
-- list for RTU session management -- list for RTU session management
self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd } self.rtu_list = { self.redstone, self.induction, self.sps, self.tanks, self.envd }
-- init redstone RTU I/O controller -- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone) self.io_ctl = rsctl.new(self.redstone, 0)
-- fill blank alarm/tone states -- fill blank alarm/tone states
for _ = 1, 12 do table.insert(self.test_alarm_states, false) end for _ = 1, 12 do table.insert(self.test_alarm_states, false) end
@@ -149,6 +182,70 @@ function facility.new(config)
table.insert(self.test_tone_states, false) table.insert(self.test_tone_states, false)
end end
-- init next boot state
settings.set("LastProcessState", PROCESS.INACTIVE)
settings.set("LastUnitStates", self.last_unit_states)
if not settings.save("/supervisor.settings") then
log.warning("FAC: failed to save initial control state into supervisor settings file")
end
--#endregion
-- PRIVATE FUNCTIONS --
-- check an auto process control configuration and save it if its valid (does not start the process)
---@param auto_cfg start_auto_config configuration
---@return boolean ready, number[] unit_limits
local function _auto_check_and_save(auto_cfg)
local ready = false
-- load up current limits
local limits = {}
for i = 1, config.UnitCount do
limits[i] = self.units[i].get_control_inf().lim_br100 * 100
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
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(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
self.burn_target = auto_cfg.burn_target
end
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(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(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
self.units[i].set_burn_limit(limit)
end
end
end
ready = self.mode_set > 0
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
ready = false
end
end
return ready, limits
end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class facility ---@class facility
@@ -239,6 +336,9 @@ function facility.new(config)
-- update (iterate) the facility management -- update (iterate) the facility management
function public.update() function public.update()
-- run reboot recovery routine if needed
f_update.boot_recovery()
-- run process control and evaluate automatic SCRAM -- run process control and evaluate automatic SCRAM
f_update.pre_auto() f_update.pre_auto()
f_update.auto_control(config.ExtChargeIdling) f_update.auto_control(config.ExtChargeIdling)
@@ -251,6 +351,9 @@ function facility.new(config)
-- unit tasks -- unit tasks
f_update.unit_mgmt() f_update.unit_mgmt()
-- update alarm states right before updating the audio
f_update.update_alarms()
-- update alarm tones -- update alarm tones
f_update.alarm_audio() f_update.alarm_audio()
end end
@@ -267,6 +370,50 @@ function facility.new(config)
--#endregion --#endregion
--#region Startup Recovery
-- on exit, use this to clear the boot state so we don't resume when exiting cleanly
function public.clear_boot_state()
settings.unset("LastProcessState")
settings.unset("LastUnitStates")
if not settings.save("/supervisor.settings") then
log.warning("facility.clear_boot_state(): failed to save supervisor settings file")
else
log.debug("FAC: cleared boot state on exit")
end
end
-- initialize facility resume boot recovery
---@param state sv_boot_state|nil
function public.boot_recovery_init(state)
if self.recovery == RCV_STATE.INACTIVE and state then
self.recovery_boot_state = state
self.recovery = RCV_STATE.PRIMED
log.info("FAC: startup resume ready")
end
end
-- attempt facility resume boot recovery
---@param auto_cfg start_auto_config configuration
function public.boot_recovery_start(auto_cfg)
if self.recovery == RCV_STATE.PRIMED then
self.recovery = util.trinary(_auto_check_and_save(auto_cfg), RCV_STATE.RUNNING, RCV_STATE.STOPPED)
log.info(util.c("FAC: startup resume ", util.trinary(self.recovery == RCV_STATE.RUNNING, "started", "failed")))
else self.recovery = RCV_STATE.STOPPED end
end
-- used on certain coordinator commands to end reboot recovery (remain in current operational state)
function public.cancel_recovery()
if self.recovery == RCV_STATE.RUNNING then
self.recovery = RCV_STATE.STOPPED
self.recovery_boot_state = nil
log.info("FAC: process startup resume cancelled by user operation")
end
end
--#endregion
--#region Commands --#region Commands
-- SCRAM all reactor units -- SCRAM all reactor units
@@ -276,10 +423,14 @@ function facility.new(config)
end end
end end
-- ack all alarms on all reactor units -- ack all alarms on all reactor units and the facility
function public.ack_all() function public.ack_all()
for i = 1, #self.units do -- unit alarms
self.units[i].ack_all() for i = 1, #self.units do self.units[i].ack_all() end
-- facility alarms
for id, state in pairs(self.alarm_states) do
if state == ALARM_STATE.TRIPPED then self.alarm_states[id] = ALARM_STATE.ACKED end
end end
end end
@@ -290,59 +441,13 @@ function facility.new(config)
function public.auto_stop() self.mode = PROCESS.INACTIVE end function public.auto_stop() self.mode = PROCESS.INACTIVE end
-- set automatic control configuration and start the process -- set automatic control configuration and start the process
---@param auto_cfg sys_auto_config configuration ---@param auto_cfg start_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating) ---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(auto_cfg) function public.auto_start(auto_cfg)
local charge_scaler = 1000000 -- convert MFE to FE local ready, limits = _auto_check_and_save(auto_cfg)
local gen_scaler = 1000 -- convert kFE to FE
local ready = false
-- load up current limits if ready and self.units_ready then
local limits = {} self.mode = self.mode_set
for i = 1, config.UnitCount do
limits[i] = self.units[i].get_control_inf().lim_br100 * 100
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
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(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
self.burn_target = auto_cfg.burn_target
end
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(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(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
self.units[i].set_burn_limit(limit)
end
end
end
ready = self.mode_set > 0
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
ready = false
end
ready = ready and self.units_ready
if ready then self.mode = self.mode_set end
end end
log.debug(util.c("FAC: process start ", util.trinary(ready, "accepted", "rejected"))) log.debug(util.c("FAC: process start ", util.trinary(ready, "accepted", "rejected")))
@@ -351,8 +456,8 @@ function facility.new(config)
ready, ready,
self.mode_set, self.mode_set,
self.burn_target, self.burn_target,
self.charge_setpoint / charge_scaler, self.charge_setpoint / CHARGE_SCALER,
self.gen_rate_setpoint / gen_scaler, self.gen_rate_setpoint / GEN_SCALER,
limits limits
} }
end end

View File

@@ -1,17 +1,23 @@
local audio = require("scada-common.audio") local audio = require("scada-common.audio")
local const = require("scada-common.constants") local const = require("scada-common.constants")
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes") local alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc")
local svsessions = require("supervisor.session.svsessions")
local qtypes = require("supervisor.session.rtu.qtypes")
local TONE = audio.TONE local TONE = audio.TONE
local ALARM = types.ALARM local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP
local CONTAINER_MODE = types.CONTAINER_MODE local CONTAINER_MODE = types.CONTAINER_MODE
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES local PROCESS_NAMES = types.PROCESS_NAMES
@@ -131,6 +137,54 @@ end
--#region PUBLIC FUNCTIONS --#region PUBLIC FUNCTIONS
-- run reboot recovery routine if needed
function update.boot_recovery()
local RCV_STATE = self.types.RCV_STATE
-- attempt reboot recovery if in progress
if self.recovery == RCV_STATE.RUNNING then
local was_inactive = self.recovery_boot_state.mode == PROCESS.INACTIVE or self.recovery_boot_state.mode == PROCESS.SYSTEM_ALARM_IDLE
-- try to start auto control
if self.recovery_boot_state.mode ~= nil and self.units_ready then
if not was_inactive then
self.mode = self.mode_set
log.info("FAC: process startup resume initiated")
end
self.recovery_boot_state.mode = nil
end
local recovered = self.recovery_boot_state.mode == nil or was_inactive
-- restore manual control reactors
for i = 1, #self.units do
local u = self.units[i]
if self.recovery_boot_state.unit_states[i] and self.group_map[i] == AUTO_GROUP.MANUAL then
recovered = false
if u.get_control_inf().ready then
local plc_s = svsessions.get_reactor_session(i)
if plc_s ~= nil then
plc_s.in_queue.push_command(plc.PLC_S_CMDS.ENABLE)
log.info("FAC: startup resume enabling manually controlled reactor unit #" .. i)
-- only execute once
self.recovery_boot_state.unit_states[i] = nil
end
end
end
end
if recovered then
self.recovery = RCV_STATE.STOPPED
self.recovery_boot_state = nil
log.info("FAC: startup resume sequence completed")
end
end
end
-- automatic control pre-update logic -- automatic control pre-update logic
function update.pre_auto() function update.pre_auto()
-- unlink RTU sessions if they are closed -- unlink RTU sessions if they are closed
@@ -243,6 +297,11 @@ function update.auto_control(ExtChargeIdling)
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1])) log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
settings.set("LastProcessState", self.mode)
if not settings.save("/supervisor.settings") then
log.warning("facility_update.auto_control(): failed to save supervisor settings file")
end
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK self.start_fail = START_STATUS.OK
@@ -586,7 +645,7 @@ function update.auto_safety()
end end
if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.SYSTEM_ALARM_IDLE) then
local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.gen_fault local scram = astatus.matrix_fault or astatus.matrix_fill or astatus.crit_alarm or astatus.radiation or astatus.gen_fault
if scram and not self.ascram then if scram and not self.ascram then
-- SCRAM all units -- SCRAM all units
@@ -642,25 +701,32 @@ function update.auto_safety()
self.ascram_reason = AUTO_SCRAM.NONE self.ascram_reason = AUTO_SCRAM.NONE
-- reset PLC RPS trips if we should -- reset PLC RPS trips if we should
for i = 1, #self.units do for i = 1, #self.prio_defs do
local u = self.units[i] for _, u in pairs(self.prio_defs[i]) do
u.auto_cond_rps_reset() u.auto_cond_rps_reset()
end
end end
end end
end end
end end
-- update last mode and set next mode -- update last mode, set next mode, and update saved state as needed
function update.post_auto() function update.post_auto()
self.last_mode = self.mode self.last_mode = self.mode
self.mode = next_mode self.mode = next_mode
end end
-- update facility alarm states
function update.update_alarms()
-- Facility Radiation
alarm_ctl.update_alarm_state("FAC", self.alarm_states, self.ascram_status.radiation, self.alarms.FacilityRadiation, true)
end
-- update alarm audio control -- update alarm audio control
function update.alarm_audio() function update.alarm_audio()
local allow_test = self.allow_testing and self.test_tone_set local allow_test = self.allow_testing and self.test_tone_set
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false } local alarms = { false, false, false, false, false, false, false, false, false, false, false, false, false }
-- reset tone states before re-evaluting -- reset tone states before re-evaluting
for i = 1, #self.tone_states do self.tone_states[i] = false end for i = 1, #self.tone_states do self.tone_states[i] = false end
@@ -676,8 +742,11 @@ function update.alarm_audio()
end end
end end
-- record facility alarms
alarms[ALARM.FacilityRadiation] = self.alarm_states[ALARM.FacilityRadiation] == ALARM_STATE.TRIPPED
-- clear testing alarms if we aren't using them
if not self.test_tone_reset then if not self.test_tone_reset then
-- clear testing alarms if we aren't using them
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
end end
end end
@@ -716,7 +785,7 @@ function update.alarm_audio()
end end
-- radiation is a big concern, always play this CRITICAL level alarm if active -- radiation is a big concern, always play this CRITICAL level alarm if active
if alarms[ALARM.ContainmentRadiation] then if alarms[ALARM.ContainmentRadiation] or alarms[ALARM.FacilityRadiation] then
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled -- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one -- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
@@ -792,6 +861,7 @@ end
function update.unit_mgmt() function update.unit_mgmt()
local insufficent_po_rate = false local insufficent_po_rate = false
local need_emcool = false local need_emcool = false
local write_state = false
for i = 1, #self.units do for i = 1, #self.units do
local u = self.units[i] local u = self.units[i]
@@ -807,6 +877,21 @@ function update.unit_mgmt()
if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then
need_emcool = true need_emcool = true
end end
-- check for enabled state changes to save
if self.last_unit_states[i] ~= u.is_reactor_enabled() then
self.last_unit_states[i] = u.is_reactor_enabled()
write_state = true
end
end
-- record unit control states
if write_state then
settings.set("LastUnitStates", self.last_unit_states)
if not settings.save("/supervisor.settings") then
log.warning("facility_update.unit_mgmt(): failed to save supervisor settings file")
end
end end
-- update waste product -- update waste product

View File

@@ -25,6 +25,8 @@ local function init(parent, id)
local label_fg = style.fp.label_fg local label_fg = style.fp.label_fg
local term_w, _ = term.getSize()
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright}
@@ -40,9 +42,9 @@ local function init(parent, id)
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg} local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value) pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4} TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg} local pdg_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg} TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update) pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor) pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)

View File

@@ -25,6 +25,8 @@ local function init(parent, id)
local label_fg = style.fp.label_fg local label_fg = style.fp.label_fg
local term_w, _ = term.getSize()
-- root div -- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2} local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright} local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright}
@@ -40,13 +42,13 @@ local function init(parent, id)
local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.fp.label_d_fg} local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.fp.label_d_fg}
unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value) unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
TextBox{parent=entry,x=21,y=2,text="FW:",width=3} TextBox{parent=entry,x=term_w-30,y=2,text="FW:",width=3}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg} local rtu_fw_v = TextBox{parent=entry,x=term_w-26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value) rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4} TextBox{parent=entry,x=term_w-15,y=2,text="RTT:",width=4}
local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg} local rtu_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg} TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update) rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor) rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)

View File

@@ -41,6 +41,8 @@ local function init(panel)
local label_fg = style.fp.label_fg local label_fg = style.fp.label_fg
local label_d_fg = style.fp.label_d_fg local label_d_fg = style.fp.label_d_fg
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,fg_bg=style.theme.header} TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
local page_div = Div{parent=panel,x=1,y=3} local page_div = Div{parent=panel,x=1,y=3}
@@ -73,9 +75,9 @@ local function init(panel)
-- about footer -- about footer
-- --
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg} local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"} local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"} local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end) fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end) comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -87,7 +89,7 @@ local function init(panel)
-- plc sessions page -- plc sessions page
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true} local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49} local plc_list = Div{parent=plc_page,x=2,y=2,width=term_w-2}
for i = 1, supervisor.config.UnitCount do for i = 1, supervisor.config.UnitCount do
local ps_prefix = "plc_" .. i .. "_" local ps_prefix = "plc_" .. i .. "_"
@@ -103,13 +105,13 @@ local function init(panel)
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,fg_bg=label_d_fg} local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,fg_bg=label_d_fg}
plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value) plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value)
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3} TextBox{parent=plc_entry,x=term_w-28,y=2,text="FW:",width=3}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,fg_bg=label_fg} local plc_fw_v = TextBox{parent=plc_entry,x=term_w-24,y=2,text=" ------- ",width=9,fg_bg=label_fg}
plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value) plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4} TextBox{parent=plc_entry,x=term_w-14,y=2,text="RTT:",width=4}
local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg} local plc_rtt = DataIndicator{parent=plc_entry,x=term_w-9,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,fg_bg=label_fg} TextBox{parent=plc_entry,x=term_w-4,y=2,text="ms",width=4,fg_bg=label_fg}
plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update) plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor) plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
@@ -119,13 +121,13 @@ local function init(panel)
-- rtu sessions page -- rtu sessions page
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true} local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local rtu_list = ListBox{parent=rtu_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=rtu_list,height=1} -- padding local _ = Div{parent=rtu_list,height=1} -- padding
-- coordinator session page -- coordinator session page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true} local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright} local crd_box = Div{parent=crd_page,x=2,y=2,width=term_w-2,height=4,fg_bg=s_hi_bright}
local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green_hc,colors.green_off)} local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green_hc,colors.green_off)}
crd_conn.register(databus.ps, "crd_conn", crd_conn.update) crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
@@ -138,27 +140,27 @@ local function init(panel)
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,fg_bg=label_fg} local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value) crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4} TextBox{parent=crd_box,x=term_w-15,y=2,text="RTT:",width=4}
local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg} local crd_rtt = DataIndicator{parent=crd_box,x=term_w-10,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,fg_bg=label_fg} TextBox{parent=crd_box,x=term_w-4,y=2,text="ms",width=4,fg_bg=label_fg}
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update) crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor) crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
-- pocket sessions page -- pocket sessions page
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true} local pkt_page = Div{parent=page_div,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local pdg_list = ListBox{parent=pkt_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=pdg_list,height=1} -- padding local _ = Div{parent=pdg_list,height=1} -- padding
-- RTU device ID check/diagnostics page -- RTU device ID check/diagnostics page
local chk_page = Div{parent=page_div,x=1,y=1,hidden=true} local chk_page = Div{parent=page_div,y=1,hidden=true}
local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)} local chk_list = ListBox{parent=chk_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=chk_list,height=1} -- padding local _ = Div{parent=chk_list,height=1} -- padding
-- info page -- info page
local info_page = Div{parent=page_div,x=1,y=1,hidden=true} local info_page = Div{parent=page_div,y=1,hidden=true}
local info = Div{parent=info_page,height=6,x=2,y=2} local info = Div{parent=info_page,height=6,x=2,y=2}
TextBox{parent=info,text="SVR \x1a Supervisor Status"} TextBox{parent=info,text="SVR \x1a Supervisor Status"}
@@ -168,7 +170,7 @@ local function init(panel)
TextBox{parent=info,text="PKT \x1a Pocket Connections"} TextBox{parent=info,text="PKT \x1a Pocket Connections"}
TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"} TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"}
local notes = Div{parent=info_page,width=49,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg} local notes = Div{parent=info_page,width=term_w-2,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."} TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."}

View File

@@ -234,6 +234,23 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if pkt.type == CRDN_TYPE.INITIAL_BUILDS then if pkt.type == CRDN_TYPE.INITIAL_BUILDS then
-- acknowledgement to coordinator receiving builds -- acknowledgement to coordinator receiving builds
self.acks.builds = true self.acks.builds = true
elseif pkt.type == CRDN_TYPE.PROCESS_READY then
if pkt.length == 5 then
-- coordinator has sent all initial process data, power-on recovery is now possible
---@type start_auto_config
local config = {
mode = pkt.data[1],
burn_target = pkt.data[2],
charge_target = pkt.data[3],
gen_target = pkt.data[4],
limits = pkt.data[5]
}
facility.boot_recovery_start(config)
else
log.debug(log_tag .. "CRDN process ready packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.FAC_BUILDS then elseif pkt.type == CRDN_TYPE.FAC_BUILDS then
-- acknowledgement to coordinator receiving builds -- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true self.acks.fac_builds = true
@@ -243,8 +260,11 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if cmd == FAC_COMMAND.SCRAM_ALL then if cmd == FAC_COMMAND.SCRAM_ALL then
facility.scram_all() facility.scram_all()
facility.cancel_recovery()
_send(CRDN_TYPE.FAC_CMD, { cmd, true }) _send(CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMAND.STOP then elseif cmd == FAC_COMMAND.STOP then
facility.cancel_recovery()
local was_active = facility.auto_is_active() local was_active = facility.auto_is_active()
if was_active then if was_active then
@@ -253,15 +273,16 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
_send(CRDN_TYPE.FAC_CMD, { cmd, was_active }) _send(CRDN_TYPE.FAC_CMD, { cmd, was_active })
elseif cmd == FAC_COMMAND.START then elseif cmd == FAC_COMMAND.START then
facility.cancel_recovery()
if pkt.length == 6 then if pkt.length == 6 then
---@type sys_auto_config ---@class start_auto_config
---@diagnostic disable-next-line: missing-fields
local config = { local config = {
mode = pkt.data[2], mode = pkt.data[2], ---@type PROCESS
burn_target = pkt.data[3], burn_target = pkt.data[3], ---@type number
charge_target = pkt.data[4], charge_target = pkt.data[4], ---@type number
gen_target = pkt.data[5], gen_target = pkt.data[5], ---@type number
limits = pkt.data[6] limits = pkt.data[6] ---@type number[]
} }
_send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) }) _send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
@@ -313,8 +334,11 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL
if cmd == UNIT_COMMAND.SCRAM then if cmd == UNIT_COMMAND.SCRAM then
facility.cancel_recovery()
out_queue.push_data(SV_Q_DATA.SCRAM, data) out_queue.push_data(SV_Q_DATA.SCRAM, data)
elseif cmd == UNIT_COMMAND.START then elseif cmd == UNIT_COMMAND.START then
facility.cancel_recovery()
if manual then if manual then
out_queue.push_data(SV_Q_DATA.START, data) out_queue.push_data(SV_Q_DATA.START, data)
else else
@@ -324,6 +348,8 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
elseif cmd == UNIT_COMMAND.RESET_RPS then elseif cmd == UNIT_COMMAND.RESET_RPS then
out_queue.push_data(SV_Q_DATA.RESET_RPS, data) out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
elseif cmd == UNIT_COMMAND.SET_BURN then elseif cmd == UNIT_COMMAND.SET_BURN then
facility.cancel_recovery()
if pkt.length == 3 then if pkt.length == 3 then
if manual then if manual then
out_queue.push_data(SV_Q_DATA.SET_BURN, data) out_queue.push_data(SV_Q_DATA.SET_BURN, data)
@@ -354,6 +380,8 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id") log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end end
elseif cmd == UNIT_COMMAND.SET_GROUP then elseif cmd == UNIT_COMMAND.SET_GROUP then
facility.cancel_recovery()
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then (pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
facility.set_group(unit.get_id(), pkt.data[3]) facility.set_group(unit.get_id(), pkt.data[3])

View File

@@ -53,15 +53,15 @@ local PERIODICS = {
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
---@param initial_reset boolean[] initial PLC reset on timeout flags, indexed by reactor_id
---@param fp_ok boolean if the front panel UI is running ---@param fp_ok boolean if the front panel UI is running
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, fp_ok) function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, initial_reset, fp_ok)
-- print a log message to the terminal as long as the UI isn't running -- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end local function println(message) if not fp_ok then util.println_ts(message) end end
local log_tag = "plc_session(" .. id .. "): " local log_tag = "plc_session(" .. id .. "): "
local self = { local self = {
commanded_state = false,
commanded_burn_rate = 0.0, commanded_burn_rate = 0.0,
auto_cmd_token = 0, auto_cmd_token = 0,
ramping_rate = false, ramping_rate = false,
@@ -72,6 +72,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
connected = true, connected = true,
received_struct = false, received_struct = false,
received_status_cache = false, received_status_cache = false,
received_rps_status = false,
conn_watchdog = util.new_watchdog(timeout), conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0, last_rtt = 0,
-- periodic messages -- periodic messages
@@ -381,6 +382,16 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local status = pcall(_copy_rps_status, pkt.data) local status = pcall(_copy_rps_status, pkt.data)
if status then if status then
-- copied in RPS status data OK -- copied in RPS status data OK
self.received_rps_status = true
-- try initial reset if needed
if initial_reset[reactor_id] then
initial_reset[reactor_id] = false
if self.sDB.rps_trip_cause == "timeout" then
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
log.debug(log_tag .. "initial RPS reset on timeout status sent")
end
end
else else
-- error copying RPS status data -- error copying RPS status data
log.error(log_tag .. "failed to parse RPS status packet data") log.error(log_tag .. "failed to parse RPS status packet data")
@@ -394,6 +405,16 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) }) local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) })
if status then if status then
-- copied in RPS status data OK -- copied in RPS status data OK
self.received_rps_status = true
-- try initial reset if needed
if initial_reset[reactor_id] then
initial_reset[reactor_id] = false
if self.sDB.rps_trip_cause == "timeout" then
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
log.debug(log_tag .. "initial RPS reset on timeout alarm sent")
end
end
else else
-- error copying RPS status data -- error copying RPS status data
log.error(log_tag .. "failed to parse RPS alarm status data") log.error(log_tag .. "failed to parse RPS alarm status data")
@@ -487,6 +508,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
---@nodiscard ---@nodiscard
function public.get_db() return self.sDB end function public.get_db() return self.sDB end
-- check if the reactor structure, status, and RPS status have been received
---@nodiscard
function public.check_received_all_data() return self.received_struct and self.received_status_cache and self.received_rps_status end
-- check if ramping is completed by first verifying auto command token ack -- check if ramping is completed by first verifying auto command token ack
---@nodiscard ---@nodiscard
function public.is_ramp_complete() function public.is_ramp_complete()

View File

@@ -9,7 +9,8 @@ local rsctl = {}
-- create a new redstone RTU I/O controller -- create a new redstone RTU I/O controller
---@nodiscard ---@nodiscard
---@param redstone_rtus redstone_session[] redstone RTU sessions ---@param redstone_rtus redstone_session[] redstone RTU sessions
function rsctl.new(redstone_rtus) ---@param bank integer I/O bank (unit/facility assignment) to interface with
function rsctl.new(redstone_rtus, bank)
---@class rs_controller ---@class rs_controller
local public = {} local public = {}
@@ -18,7 +19,7 @@ function rsctl.new(redstone_rtus)
---@return boolean ---@return boolean
function public.is_connected(port) function public.is_connected(port)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
if redstone_rtus[i].get_db().io[port] ~= nil then return true end if redstone_rtus[i].get_db().io[bank][port] ~= nil then return true end
end end
return false return false
@@ -29,7 +30,7 @@ function rsctl.new(redstone_rtus)
---@param value boolean ---@param value boolean
function public.digital_write(port, value) function public.digital_write(port, value)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port] local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then io.write(value) end if io ~= nil then io.write(value) end
end end
end end
@@ -40,7 +41,7 @@ function rsctl.new(redstone_rtus)
---@return boolean|nil ---@return boolean|nil
function public.digital_read(port) function public.digital_read(port)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port] local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then return io.read() --[[@as boolean|nil]] end if io ~= nil then return io.read() --[[@as boolean|nil]] end
end end
end end
@@ -52,7 +53,7 @@ function rsctl.new(redstone_rtus)
---@param max number maximum value for scaling 0 to 15 ---@param max number maximum value for scaling 0 to 15
function public.analog_write(port, value, min, max) function public.analog_write(port, value, min, max)
for i = 1, #redstone_rtus do for i = 1, #redstone_rtus do
local io = redstone_rtus[i].get_db().io[port] local io = redstone_rtus[i].get_db().io[bank][port]
if io ~= nil then io.write(rsio.analog_write(value, min, max)) end if io ~= nil then io.write(rsio.analog_write(value, min, max)) end
end end
end end

View File

@@ -93,7 +93,7 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
type = self.advert[i][1], type = self.advert[i][1],
index = self.advert[i][2], index = self.advert[i][2],
reactor = self.advert[i][3], reactor = self.advert[i][3],
rsio = self.advert[i][4] rs_conns = self.advert[i][4]
} }
local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean local u_type = unit_advert.type ---@type RTU_UNIT_TYPE|boolean
@@ -104,14 +104,17 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false)) advert_validator.assert(util.is_int(unit_advert.index) or (unit_advert.index == false))
advert_validator.assert_type_int(unit_advert.reactor) advert_validator.assert_type_int(unit_advert.reactor)
if u_type == RTU_UNIT_TYPE.REDSTONE then
advert_validator.assert_type_table(unit_advert.rsio)
end
if advert_validator.valid() then if advert_validator.valid() then
if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end if util.is_int(unit_advert.index) then advert_validator.assert_min(unit_advert.index, 1) end
advert_validator.assert_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units) if (unit_advert.reactor == -1) or (u_type == RTU_UNIT_TYPE.REDSTONE) then
advert_validator.assert((unit_advert.reactor == -1) and (u_type == RTU_UNIT_TYPE.REDSTONE))
advert_validator.assert_type_table(unit_advert.rs_conns)
else
advert_validator.assert_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units)
end
if not advert_validator.valid() then u_type = false end if not advert_validator.valid() then u_type = false end
else else
u_type = false u_type = false
@@ -126,15 +129,34 @@ function rtu.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, ad
-- validation fail -- validation fail
log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure") log.debug(log_tag .. "_handle_advertisement(): advertisement unit validation failure")
else else
if unit_advert.reactor > 0 then if unit_advert.reactor == -1 then
local target_unit = self.fac_units[unit_advert.reactor] -- redstone RTUs can be used in multiple different assignments
-- unit RTUs
if u_type == RTU_UNIT_TYPE.REDSTONE then if u_type == RTU_UNIT_TYPE.REDSTONE then
-- redstone -- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then -- link this to any subsystems this RTU provides connections for
if type(unit) ~= "nil" then
for assignment, conns in pairs(unit_advert.rs_conns) do
if #conns > 0 then
if assignment == 0 then
facility.add_redstone(unit)
elseif assignment > 0 and assignment <= #self.fac_units then
self.fac_units[assignment].add_redstone(unit)
else
log.warning(util.c(log_tag, "_handle_advertisement(): invalid redstone RTU assignment ", assignment))
end
end
end
end
else
log.warning(util.c(log_tag, "_handle_advertisement(): encountered unsupported multi-assignment RTU type ", type_string))
end
elseif unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor]
-- unit RTUs
if u_type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler -- boiler
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q) unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end if type(unit) ~= "nil" then target_unit.add_boiler(unit) end

View File

@@ -10,7 +10,6 @@ local redstone = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_FCODE = types.MODBUS_FCODE
local IO_PORT = rsio.IO
local IO_LVL = rsio.IO_LVL local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE local IO_MODE = rsio.IO_MODE
@@ -39,6 +38,9 @@ local PERIODICS = {
OUTPUT_SYNC = 200 OUTPUT_SYNC = 200
} }
-- create a new block of IO banks (facility, then each unit)
local function new_io_block() return { [0] = {}, {}, {}, {}, {} } end
---@class dig_phy_entry ---@class dig_phy_entry
---@field phy IO_LVL actual value ---@field phy IO_LVL actual value
---@field req IO_LVL commanded value ---@field req IO_LVL commanded value
@@ -74,27 +76,27 @@ function redstone.new(session_id, unit_id, advert, out_queue)
next_ir_req = 0, next_ir_req = 0,
next_hr_sync = 0 next_hr_sync = 0
}, },
---@class rs_io_list ---@class rs_io_map
io_list = { io_map = {
digital_in = {}, ---@type IO_PORT[] discrete inputs digital_in = {}, ---@type { bank: integer, port: IO_PORT }[] discrete inputs
digital_out = {}, ---@type IO_PORT[] coils digital_out = {}, ---@type { bank: integer, port: IO_PORT }[] coils
analog_in = {}, ---@type IO_PORT[] input registers analog_in = {}, ---@type { bank: integer, port: IO_PORT }[] input registers
analog_out = {} ---@type IO_PORT[] holding registers analog_out = {} ---@type { bank: integer, port: IO_PORT }[] holding registers
}, },
phy_trans = { coils = -1, hold_regs = -1 }, phy_trans = { coils = -1, hold_regs = -1 },
-- last set/read ports (reflecting the current state of the RTU) -- last set/read ports (reflecting the current state of the RTU)
---@class rs_io_states ---@class rs_io_states
phy_io = { phy_io = {
digital_in = {}, ---@type dig_phy_entry[] discrete inputs digital_in = new_io_block(), ---@type dig_phy_entry[][] discrete inputs
digital_out = {}, ---@type dig_phy_entry[] coils digital_out = new_io_block(), ---@type dig_phy_entry[][] coils
analog_in = {}, ---@type ana_phy_entry[] input registers analog_in = new_io_block(), ---@type ana_phy_entry[][] input registers
analog_out = {} ---@type ana_phy_entry[] holding registers analog_out = new_io_block() ---@type ana_phy_entry[][] holding registers
}, },
---@class redstone_session_db ---@class redstone_session_db
db = { db = {
-- read/write functions for connected I/O -- read/write functions for connected I/O
---@type (rs_db_dig_io|rs_db_ana_io)[] ---@type (rs_db_dig_io|rs_db_ana_io)[][]
io = {} io = new_io_block()
} }
} }
@@ -103,93 +105,91 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- INITIALIZE -- -- INITIALIZE --
-- create all ports as disconnected
for _ = 1, #IO_PORT do
table.insert(self.db, IO_LVL.DISCONNECT)
end
-- setup I/O -- setup I/O
for i = 1, #advert.rsio do for bank = 0, 4 do
local port = advert.rsio[i] for i = 1, #advert.rs_conns[bank] do
local port = advert.rs_conns[bank][i]
if rsio.is_valid_port(port) then if rsio.is_valid_port(port) then
local mode = rsio.get_io_mode(port) local mode = rsio.get_io_mode(port)
local io_entry = { bank = bank, port = port }
if mode == IO_MODE.DIGITAL_IN then if mode == IO_MODE.DIGITAL_IN then
self.has_di = true self.has_di = true
table.insert(self.io_list.digital_in, port) table.insert(self.io_map.digital_in, io_entry)
self.phy_io.digital_in[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } self.phy_io.digital_in[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
---@class rs_db_dig_io ---@class rs_db_dig_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end, read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[bank][port].phy) end,
write = function () end write = function () end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
elseif mode == IO_MODE.DIGITAL_OUT then elseif mode == IO_MODE.DIGITAL_OUT then
self.has_do = true self.has_do = true
table.insert(self.io_list.digital_out, port) table.insert(self.io_map.digital_out, io_entry)
self.phy_io.digital_out[port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING } self.phy_io.digital_out[bank][port] = { phy = IO_LVL.FLOATING, req = IO_LVL.FLOATING }
---@class rs_db_dig_io ---@class rs_db_dig_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end, read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[bank][port].phy) end,
---@param active boolean ---@param active boolean
write = function (active) write = function (active)
local level = rsio.digital_write_active(port, active) local level = rsio.digital_write_active(port, active)
if level ~= nil then self.phy_io.digital_out[port].req = level end if level ~= nil then self.phy_io.digital_out[bank][port].req = level end
end
}
self.db.io[port] = io_f
elseif mode == IO_MODE.ANALOG_IN then
self.has_ai = true
table.insert(self.io_list.analog_in, port)
self.phy_io.analog_in[port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_in[port].phy end,
write = function () end
}
self.db.io[port] = io_f
elseif mode == IO_MODE.ANALOG_OUT then
self.has_ao = true
table.insert(self.io_list.analog_out, port)
self.phy_io.analog_out[port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_out[port].phy end,
---@param value integer
write = function (value)
if value >= 0 and value <= 15 then
self.phy_io.analog_out[port].req = value
end end
end }
}
self.db.io[port] = io_f self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_IN then
self.has_ai = true
table.insert(self.io_map.analog_in, io_entry)
self.phy_io.analog_in[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_in[bank][port].phy end,
write = function () end
}
self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_OUT then
self.has_ao = true
table.insert(self.io_map.analog_out, io_entry)
self.phy_io.analog_out[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io
local io_f = {
---@nodiscard
---@return integer
read = function () return self.phy_io.analog_out[bank][port].phy end,
---@param value integer
write = function (value)
if value >= 0 and value <= 15 then
self.phy_io.analog_out[bank][port].req = value
end
end
}
self.db.io[bank][port] = io_f
else
-- should be unreachable code, we already validated ports
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true)
return nil
end
else else
-- should be unreachable code, we already validated ports log.error(util.c(log_tag, "invalid advertisement port (", bank, ":", port, ")"), true)
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true)
return nil return nil
end end
else
log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true)
return nil
end end
end end
@@ -197,12 +197,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- query discrete inputs -- query discrete inputs
local function _request_discrete_inputs() local function _request_discrete_inputs()
self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in }) self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_map.digital_in })
end end
-- query input registers -- query input registers
local function _request_input_registers() local function _request_input_registers()
self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_map.analog_in })
end end
-- write all coil outputs -- write all coil outputs
@@ -210,9 +210,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local params = { 1 } local params = { 1 }
local outputs = self.phy_io.digital_out local outputs = self.phy_io.digital_out
for i = 1, #self.io_list.digital_out do for i = 1, #self.io_map.digital_out do
local port = self.io_list.digital_out[i] local entry = self.io_map.digital_out[i]
table.insert(params, outputs[port].req) table.insert(params, outputs[entry.bank][entry.port].req)
end end
self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params) self.phy_trans.coils = self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, params)
@@ -220,7 +220,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- read all coil outputs -- read all coil outputs
local function _read_coils() local function _read_coils()
self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_list.digital_out }) self.session.send_request(TXN_TYPES.COIL_READ, MODBUS_FCODE.READ_COILS, { 1, #self.io_map.digital_out })
end end
-- write all holding register outputs -- write all holding register outputs
@@ -228,9 +228,9 @@ function redstone.new(session_id, unit_id, advert, out_queue)
local params = { 1 } local params = { 1 }
local outputs = self.phy_io.analog_out local outputs = self.phy_io.analog_out
for i = 1, #self.io_list.analog_out do for i = 1, #self.io_map.analog_out do
local port = self.io_list.analog_out[i] local entry = self.io_map.analog_out[i]
table.insert(params, outputs[port].req) table.insert(params, outputs[entry.bank][entry.port].req)
end end
self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params) self.phy_trans.hold_regs = self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, params)
@@ -238,7 +238,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- read all holding register outputs -- read all holding register outputs
local function _read_holding_registers() local function _read_holding_registers()
self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_list.analog_out }) self.session.send_request(TXN_TYPES.HOLD_REG_READ, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, #self.io_map.analog_out })
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@@ -259,24 +259,24 @@ function redstone.new(session_id, unit_id, advert, out_queue)
end end
elseif txn_type == TXN_TYPES.DI_READ then elseif txn_type == TXN_TYPES.DI_READ then
-- discrete input read response -- discrete input read response
if m_pkt.length == #self.io_list.digital_in then if m_pkt.length == #self.io_map.digital_in then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.digital_in[i] local entry = self.io_map.digital_in[i]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.digital_in[port].phy = value self.phy_io.digital_in[entry.bank][entry.port].phy = value
end end
else else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end end
elseif txn_type == TXN_TYPES.INPUT_REG_READ then elseif txn_type == TXN_TYPES.INPUT_REG_READ then
-- input register read response -- input register read response
if m_pkt.length == #self.io_list.analog_in then if m_pkt.length == #self.io_map.analog_in then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.analog_in[i] local entry = self.io_map.analog_in[i]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.analog_in[port].phy = value self.phy_io.analog_in[entry.bank][entry.port].phy = value
end end
else else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
@@ -288,15 +288,14 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- update phy I/O table -- update phy I/O table
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical) -- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done -- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
if m_pkt.length == #self.io_list.digital_out then if m_pkt.length == #self.io_map.digital_out then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.digital_out[i] local entry = self.io_map.digital_out[i]
local state = self.phy_io.digital_out[entry.bank][entry.port]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.digital_out[port].phy = value state.phy = value
if self.phy_io.digital_out[port].req == IO_LVL.FLOATING then if state.req == IO_LVL.FLOATING then state.req = value end
self.phy_io.digital_out[port].req = value
end
end end
self.phy_trans.coils = TXN_READY self.phy_trans.coils = TXN_READY
@@ -310,12 +309,12 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- update phy I/O table -- update phy I/O table
-- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical) -- if there are multiple outputs for the same port, they will overwrite eachother (but *should* be identical)
-- given these are redstone outputs, if one worked they all should have, so no additional verification will be done -- given these are redstone outputs, if one worked they all should have, so no additional verification will be done
if m_pkt.length == #self.io_list.analog_out then if m_pkt.length == #self.io_map.analog_out then
for i = 1, m_pkt.length do for i = 1, m_pkt.length do
local port = self.io_list.analog_out[i] local entry = self.io_map.analog_out[i]
local value = m_pkt.data[i] local value = m_pkt.data[i]
self.phy_io.analog_out[port].phy = value self.phy_io.analog_out[entry.bank][entry.port].phy = value
end end
else else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")") log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
@@ -343,8 +342,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- sync digital outputs -- sync digital outputs
if self.has_do then if self.has_do then
if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then if (self.periodics.next_cl_sync <= time_now) and (self.phy_trans.coils == TXN_READY) then
for _, entry in pairs(self.phy_io.digital_out) do for bank = 0, 4 do
if entry.phy ~= entry.req then local changed = false
for _, entry in pairs(self.phy_io.digital_out[bank]) do
if entry.phy ~= entry.req then
changed = true
break
end
end
if changed then
_write_coils() _write_coils()
break break
end end
@@ -365,8 +373,17 @@ function redstone.new(session_id, unit_id, advert, out_queue)
-- sync analog outputs -- sync analog outputs
if self.has_ao then if self.has_ao then
if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then if (self.periodics.next_hr_sync <= time_now) and (self.phy_trans.hold_regs == TXN_READY) then
for _, entry in pairs(self.phy_io.analog_out) do for bank = 0, 4 do
if entry.phy ~= entry.req then local changed = false
for _, entry in pairs(self.phy_io.analog_out[bank]) do
if entry.phy ~= entry.req then
changed = true
break
end
end
if changed then
_write_holding_registers() _write_holding_registers()
break break
end end
@@ -379,9 +396,10 @@ function redstone.new(session_id, unit_id, advert, out_queue)
self.session.post_update() self.session.post_update()
end end
-- invalidate build cache -- force a re-read of cached outputs
function public.invalidate_cache() function public.invalidate_cache()
-- no build cache for this device _read_coils()
_read_holding_registers()
end end
-- get the unit session database -- get the unit session database

View File

@@ -45,6 +45,7 @@ local self = {
fp_ok = false, fp_ok = false,
config = nil, ---@type svr_config config = nil, ---@type svr_config
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
plc_ini_reset = {},
-- lists of connected sessions -- lists of connected sessions
---@diagnostic disable: missing-fields ---@diagnostic disable: missing-fields
sessions = { sessions = {
@@ -391,6 +392,7 @@ function svsessions.init(nic, fp_ok, config, facility)
conns.tanks[1] = true conns.tanks[1] = true
end end
self.plc_ini_reset[i] = true
self.dev_dbg.connected.units[i] = conns self.dev_dbg.connected.units[i] = conns
end end
end end
@@ -486,7 +488,7 @@ function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, v
local id = self.next_ids.plc local id = self.next_ids.plc
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok) plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.plc_ini_reset, self.fp_ok)
table.insert(self.sessions.plc, plc_s) table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units() local units = self.facility.get_units()

View File

@@ -10,6 +10,7 @@ local log = require("scada-common.log")
local network = require("scada-common.network") local network = require("scada-common.network")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd") local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local core = require("graphics.core") local core = require("graphics.core")
@@ -22,7 +23,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.6.2" local SUPERVISOR_VERSION = "v1.7.0"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts
@@ -72,6 +73,21 @@ if config.FacilityTankMode > 0 then
cfv.assert_type_int(def) cfv.assert_type_int(def)
cfv.assert_range(def, 0, 2) cfv.assert_range(def, 0, 2)
assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i) assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i)
local entry = config.FacilityTankList[i]
cfv.assert_type_int(entry)
cfv.assert_range(entry, 0, 2)
assert(cfv.valid(), "startup> invalid facility tank list entry for tank " .. i)
local conn = config.FacilityTankConns[i]
cfv.assert_type_int(conn)
cfv.assert_range(conn, 0, #config.FacilityTankDefs)
assert(cfv.valid(), "startup> invalid facility tank connection for reactor unit " .. i)
local type = config.TankFluidTypes[i]
cfv.assert_type_int(type)
cfv.assert_range(type, 0, types.COOLANT_TYPE.SODIUM)
assert(cfv.valid(), "startup> invalid tank fluid type for tank " .. i)
end end
end end
@@ -147,6 +163,9 @@ local function main()
-- halve the rate heartbeat LED flash -- halve the rate heartbeat LED flash
local heartbeat_toggle = true local heartbeat_toggle = true
-- init startup recovery
sv_facility.boot_recovery_init(supervisor.boot_state)
-- event loop -- event loop
while true do while true do
local event, param1, param2, param3, param4, param5 = util.pull_event() local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -237,6 +256,8 @@ local function main()
end end
end end
sv_facility.clear_boot_state()
renderer.close_ui() renderer.close_ui()
util.println_ts("exited") util.println_ts("exited")

View File

@@ -19,10 +19,24 @@ local config = {}
supervisor.config = config supervisor.config = config
-- load the supervisor configuration -- control state from last unexpected shutdown
supervisor.boot_state = nil ---@type sv_boot_state|nil
-- load the supervisor configuration and startup state
function supervisor.load_config() function supervisor.load_config()
if not settings.load("/supervisor.settings") then return false end if not settings.load("/supervisor.settings") then return false end
---@class sv_boot_state
local boot_state = {
mode = settings.get("LastProcessState"), ---@type PROCESS
unit_states = settings.get("LastUnitStates") ---@type boolean[]
}
-- only record boot state if likely valid
if type(boot_state.mode) == "number" and type(boot_state.unit_states) == "table" then
supervisor.boot_state = boot_state
end
config.UnitCount = settings.get("UnitCount") config.UnitCount = settings.get("UnitCount")
config.CoolingConfig = settings.get("CoolingConfig") config.CoolingConfig = settings.get("CoolingConfig")
config.FacilityTankMode = settings.get("FacilityTankMode") config.FacilityTankMode = settings.get("FacilityTankMode")
@@ -30,6 +44,7 @@ function supervisor.load_config()
config.FacilityTankList = settings.get("FacilityTankList") config.FacilityTankList = settings.get("FacilityTankList")
config.FacilityTankConns = settings.get("FacilityTankConns") config.FacilityTankConns = settings.get("FacilityTankConns")
config.TankFluidTypes = settings.get("TankFluidTypes") config.TankFluidTypes = settings.get("TankFluidTypes")
config.AuxiliaryCoolant = settings.get("AuxiliaryCoolant")
config.ExtChargeIdling = settings.get("ExtChargeIdling") config.ExtChargeIdling = settings.get("ExtChargeIdling")
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
@@ -64,6 +79,7 @@ function supervisor.load_config()
cfv.assert_type_table(config.FacilityTankList) cfv.assert_type_table(config.FacilityTankList)
cfv.assert_type_table(config.FacilityTankConns) cfv.assert_type_table(config.FacilityTankConns)
cfv.assert_type_table(config.TankFluidTypes) cfv.assert_type_table(config.TankFluidTypes)
cfv.assert_type_table(config.AuxiliaryCoolant)
cfv.assert_range(config.FacilityTankMode, 0, 8) cfv.assert_range(config.FacilityTankMode, 0, 8)
cfv.assert_type_bool(config.ExtChargeIdling) cfv.assert_type_bool(config.ExtChargeIdling)
@@ -246,20 +262,32 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- PLC linking request -- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4] local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then -- check ID validity
-- reactor already has a PLC assigned if reactor_id < 1 or reactor_id > config.UnitCount then
if last_ack ~= ESTABLISH_ACK.COLLISION then -- reactor index out of range
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) if last_ack ~= ESTABLISH_ACK.DENY then
log.warning(util.c("PLC_ESTABLISH: denied assignment ", reactor_id, " outside of configured unit count ", config.UnitCount))
end end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
else else
-- got an ID; assigned to a reactor successfully -- try to establish the session
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected")) local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
end end
else else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")

View File

@@ -3,20 +3,23 @@ local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local logic = require("supervisor.unitlogic") local alarm_ctl = require("supervisor.alarm_ctl")
local unit_logic = require("supervisor.unit_logic")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local rsctl = require("supervisor.session.rsctl") local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local WASTE_MODE = types.WASTE_MODE local AISTATE = alarm_ctl.AISTATE
local WASTE = types.WASTE_PRODUCT
local ALARM = types.ALARM local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local TRI_FAIL = types.TRI_FAIL local PRIO = types.ALARM_PRIORITY
local RTU_ID_FAIL = types.RTU_ID_FAIL local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local TRI_FAIL = types.TRI_FAIL
local WASTE_MODE = types.WASTE_MODE
local WASTE = types.WASTE_PRODUCT
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
@@ -37,23 +40,6 @@ local DT_KEYS = {
TurbinePower = "TPR" TurbinePower = "TPR"
} }
---@enum ALARM_INT_STATE
local AISTATE = {
INACTIVE = 1,
TRIPPING = 2,
TRIPPED = 3,
ACKED = 4,
RING_BACK = 5,
RING_BACK_TRIPPING = 6
}
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
---@field hold_time integer time (s) to hold before tripping
---@field id ALARM alarm ID
---@field tier integer alarm urgency tier (0 = highest)
-- burn rate to idle at -- burn rate to idle at
local IDLE_RATE = 0.01 local IDLE_RATE = 0.01
@@ -66,7 +52,8 @@ local unit = {}
---@param num_boilers integer number of boilers expected ---@param num_boilers integer number of boilers expected
---@param num_turbines integer number of turbines expected ---@param num_turbines integer number of turbines expected
---@param ext_idle boolean extended idling mode ---@param ext_idle boolean extended idling mode
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@param aux_coolant boolean if this unit has auxiliary coolant
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
-- time (ms) to idle for auto idling -- time (ms) to idle for auto idling
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000) local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
@@ -79,7 +66,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
plc_i = nil, ---@type plc_session plc_i = nil, ---@type plc_session
num_boilers = num_boilers, num_boilers = num_boilers,
num_turbines = num_turbines, num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, aux_coolant = aux_coolant,
types = { DT_KEYS = DT_KEYS },
-- rtus -- rtus
rtu_list = {}, ---@type unit_session[][] rtu_list = {}, ---@type unit_session[][]
redstone = {}, ---@type redstone_session[] redstone = {}, ---@type redstone_session[]
@@ -92,7 +80,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
io_ctl = nil, ---@type rs_controller io_ctl = nil, ---@type rs_controller
---@diagnostic disable-next-line: missing-fields ---@diagnostic disable-next-line: missing-fields
valves = {}, ---@type unit_valves valves = {}, ---@type unit_valves
emcool_opened = false, em_cool_opened = false,
aux_cool_opened = false,
-- auto control -- auto control
auto_engaged = false, auto_engaged = false,
auto_idle = false, auto_idle = false,
@@ -111,6 +100,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
damage_est_last = 0, damage_est_last = 0,
waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
status_text = { "UNKNOWN", "awaiting connection..." }, status_text = { "UNKNOWN", "awaiting connection..." },
enable_aux_cool = false,
-- logic for alarms -- logic for alarms
had_reactor = false, had_reactor = false,
turbine_flow_stable = false, turbine_flow_stable = false,
@@ -254,7 +244,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd } self.rtu_list = { self.redstone, self.boilers, self.turbines, self.tanks, self.snas, self.envd }
-- init redstone RTU I/O controller -- init redstone RTU I/O controller
self.io_ctl = rsctl.new(self.redstone) self.io_ctl = rsctl.new(self.redstone, reactor_id)
-- init boiler table fields -- init boiler table fields
for _ = 1, num_boilers do for _ = 1, num_boilers do
@@ -373,6 +363,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
local waste_po = _make_valve_iface(IO.WASTE_POPL) local waste_po = _make_valve_iface(IO.WASTE_POPL)
local waste_sps = _make_valve_iface(IO.WASTE_AM) local waste_sps = _make_valve_iface(IO.WASTE_AM)
local emer_cool = _make_valve_iface(IO.U_EMER_COOL) local emer_cool = _make_valve_iface(IO.U_EMER_COOL)
local aux_cool = _make_valve_iface(IO.U_AUX_COOL)
---@class unit_valves ---@class unit_valves
self.valves = { self.valves = {
@@ -380,7 +371,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
waste_sna = waste_sna, waste_sna = waste_sna,
waste_po = waste_po, waste_po = waste_po,
waste_sps = waste_sps, waste_sps = waste_sps,
emer_cool = emer_cool emer_cool = emer_cool,
aux_cool = aux_cool
} }
-- route reactor waste for a given waste product -- route reactor waste for a given waste product
@@ -591,22 +583,22 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
_dt__compute_all() _dt__compute_all()
-- update annunciator logic -- update annunciator logic
logic.update_annunciator(self) unit_logic.update_annunciator(self)
-- update alarm status -- update alarm status
logic.update_alarms(self) unit_logic.update_alarms(self)
-- if in auto mode, SCRAM on certain alarms -- if in auto mode, SCRAM on certain alarms
logic.update_auto_safety(public, self) unit_logic.update_auto_safety(self, public)
-- update status text -- update status text
logic.update_status_text(self) unit_logic.update_status_text(self)
-- handle redstone I/O -- handle redstone I/O
if #self.redstone > 0 then if #self.redstone > 0 then
logic.handle_redstone(self) unit_logic.handle_redstone(self)
elseif not self.plc_cache.rps_trip then elseif not self.plc_cache.rps_trip then
self.emcool_opened = false self.em_cool_opened = false
end end
end end
@@ -724,7 +716,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- queue a command to clear timeout/auto-scram if set -- queue a command to clear timeout/auto-scram if set
function public.auto_cond_rps_reset() function public.auto_cond_rps_reset()
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.em_cool_opened) then
local rps = self.plc_i.get_rps() local rps = self.plc_i.get_rps()
if rps.timeout or rps.automatic then if rps.timeout or rps.automatic then
self.plc_i.auto_lock(true) -- if it timed out/restarted, auto lock was lost, so re-lock it self.plc_i.auto_lock(true) -- if it timed out/restarted, auto lock was lost, so re-lock it
@@ -769,10 +761,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- acknowledge all alarms (if possible) -- acknowledge all alarms (if possible)
function public.ack_all() function public.ack_all()
for i = 1, #self.db.alarm_states do for id, state in pairs(self.db.alarm_states) do
if self.db.alarm_states[i] == ALARM_STATE.TRIPPED then if state == ALARM_STATE.TRIPPED then self.db.alarm_states[id] = ALARM_STATE.ACKED end
self.db.alarm_states[i] = ALARM_STATE.ACKED
end
end end
end end
@@ -840,6 +830,12 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
return false return false
end end
-- check the active state of the reactor (if connected)
---@nodiscard
function public.is_reactor_enabled()
if self.plc_i ~= nil then return self.plc_i.get_status().status else return false end
end
-- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active -- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active
---@nodiscard ---@nodiscard
function public.is_safe_idle() function public.is_safe_idle()
@@ -859,7 +855,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- check if emergency coolant activation has been tripped -- check if emergency coolant activation has been tripped
---@nodiscard ---@nodiscard
function public.is_emer_cool_tripped() return self.emcool_opened end function public.is_emer_cool_tripped() return self.em_cool_opened end
-- get build properties of machines -- get build properties of machines
-- --
@@ -1053,7 +1049,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
v.waste_sna.check(), v.waste_sna.check(),
v.waste_po.check(), v.waste_po.check(),
v.waste_sps.check(), v.waste_sps.check(),
v.emer_cool.check() v.emer_cool.check(),
v.aux_cool.check()
} }
end end

View File

@@ -1,12 +1,16 @@
local const = require("scada-common.constants") local const = require("scada-common.constants")
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local plc = require("supervisor.session.plc") local alarm_ctl = require("supervisor.alarm_ctl")
local qtypes = require("supervisor.session.rtu.qtypes") local plc = require("supervisor.session.plc")
local qtypes = require("supervisor.session.rtu.qtypes")
local AISTATE = alarm_ctl.AISTATE
local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
local TRI_FAIL = types.TRI_FAIL local TRI_FAIL = types.TRI_FAIL
@@ -22,19 +26,10 @@ local IO = rsio.IO
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
local AISTATE_NAMES = { local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
"INACTIVE", local ALARM_LIMS = const.ALARM_LIMITS
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local RS_THRESH = const.RS_THRESHOLDS
local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
local ALARM_LIMS = const.ALARM_LIMITS
---@class unit_logic_extension ---@class unit_logic_extension
local logic = {} local logic = {}
@@ -54,6 +49,10 @@ function logic.update_annunciator(self)
-- variables for boiler, or reactor if no boilers used -- variables for boiler, or reactor if no boilers used
local total_boil_rate = 0.0 local total_boil_rate = 0.0
-- auxiliary coolant control
local need_aux_cool = false
local dis_aux_cool = true
--#region Reactor --#region Reactor
annunc.AutoControl = self.auto_engaged annunc.AutoControl = self.auto_engaged
@@ -67,11 +66,10 @@ function logic.update_annunciator(self)
local plc_db = self.plc_i.get_db() local plc_db = self.plc_i.get_db()
-- update ready state -- update ready state
-- - can't be tripped -- - must be connected to a formed reactor
-- - must have received status at least once -- - can't have a tripped RPS
-- - must have received struct at least once -- - must have received status, struct, and RPS status at least once
plc_ready = plc_db.formed and (not plc_db.no_reactor) and (not plc_db.rps_tripped) and plc_ready = plc_db.formed and (not plc_db.no_reactor) and (not plc_db.rps_tripped) and self.plc_i.check_received_all_data()
(next(self.plc_i.get_status()) ~= nil) and (next(self.plc_i.get_struct()) ~= nil)
-- update auto control limit -- update auto control limit
if (plc_db.mek_struct.max_burn > 0) and ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then if (plc_db.mek_struct.max_burn > 0) and ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then
@@ -149,6 +147,9 @@ function logic.update_annunciator(self)
-- if no boilers, use reactor heating rate to check for boil rate mismatch -- if no boilers, use reactor heating rate to check for boil rate mismatch
if num_boilers == 0 then if num_boilers == 0 then
total_boil_rate = plc_db.mek_status.heating_rate total_boil_rate = plc_db.mek_status.heating_rate
need_aux_cool = plc_db.mek_status.ccool_fill <= RS_THRESH.AUX_COOL_ENABLE
dis_aux_cool = plc_db.mek_status.ccool_fill >= RS_THRESH.AUX_COOL_DISABLE
end end
else else
self.plc_cache.ok = false self.plc_cache.ok = false
@@ -172,12 +173,8 @@ function logic.update_annunciator(self)
annunc.EmergencyCoolant = 1 annunc.EmergencyCoolant = 1
for i = 1, #self.redstone do if self.io_ctl.is_connected(IO.U_EMER_COOL) then
local io = self.redstone[i].get_db().io[IO.U_EMER_COOL] annunc.EmergencyCoolant = util.trinary(self.io_ctl.digital_read(IO.U_EMER_COOL), 3, 2)
if io ~= nil then
annunc.EmergencyCoolant = util.trinary(io.read(), 3, 2)
break
end
end end
--#endregion --#endregion
@@ -216,6 +213,9 @@ function logic.update_annunciator(self)
annunc.BoilerOnline[idx] = true annunc.BoilerOnline[idx] = true
annunc.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow annunc.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow
need_aux_cool = need_aux_cool or (boiler.tanks.water_fill <= RS_THRESH.AUX_COOL_ENABLE)
dis_aux_cool = dis_aux_cool and (boiler.tanks.water_fill >= RS_THRESH.AUX_COOL_DISABLE)
end end
-- check heating rate low -- check heating rate low
@@ -342,11 +342,11 @@ function logic.update_annunciator(self)
end end
if rotation_stable then if rotation_stable then
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached rotational stability (", rotation, ")")) log.debug(util.c("UNIT ", self.r_id, " turbine ", idx, " reached rotational stability (", rotation, ")"))
end end
if flow_stable then if flow_stable then
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached flow stability (", turbine.state.flow_rate, " mB/t)")) log.debug(util.c("UNIT ", self.r_id, " turbine ", idx, " reached flow stability (", turbine.state.flow_rate, " mB/t)"))
end end
turbines_stable = turbines_stable and (rotation_stable or flow_stable) turbines_stable = turbines_stable and (rotation_stable or flow_stable)
@@ -358,7 +358,7 @@ function logic.update_annunciator(self)
turbines_stable = false turbines_stable = false
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reset stability (new rate ", turbine.state.steam_input_rate, " != ", last.input_rate," mB/t)")) log.debug(util.c("UNIT ", self.r_id, " turbine ", idx, " reset stability (new rate ", turbine.state.steam_input_rate, " != ", last.input_rate," mB/t)"))
end end
last.input_rate = turbine.state.steam_input_rate last.input_rate = turbine.state.steam_input_rate
@@ -407,100 +407,25 @@ function logic.update_annunciator(self)
-- update auto control ready state for this unit -- update auto control ready state for this unit
self.db.control.ready = plc_ready and boilers_ready and turbines_ready self.db.control.ready = plc_ready and boilers_ready and turbines_ready
-- update auxiliary coolant command
if plc_ready then
self.enable_aux_cool = self.plc_i.get_db().mek_status.status and
(self.enable_aux_cool or need_aux_cool) and not (dis_aux_cool and self.turbine_flow_stable)
else self.enable_aux_cool = false end
end end
-- update an alarm state given conditions -- update an alarm state given conditions
---@param self _unit_self unit instance ---@param self _unit_self
---@param tripped boolean if the alarm condition is still active ---@param tripped boolean if the alarm condition is still active
---@param alarm alarm_def alarm table ---@param alarm alarm_def alarm table
---@return boolean new_trip if the alarm just changed to being tripped ---@return boolean new_trip if the alarm just changed to being tripped
local function _update_alarm_state(self, tripped, alarm) local function _update_alarm_state(self, tripped, alarm)
local AISTATE = self.types.AISTATE return alarm_ctl.update_alarm_state("UNIT " .. self.r_id, self.db.alarm_states, tripped, alarm)
local int_state = alarm.state
local ext_state = self.db.alarm_states[alarm.id]
-- alarm inactive
if int_state == AISTATE.INACTIVE then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.TRIPPING
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
else
alarm.trip_time = util.time_ms()
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm condition met, but not yet for required hold time
elseif (int_state == AISTATE.TRIPPING) or (int_state == AISTATE.RING_BACK_TRIPPING) then
if tripped then
local elapsed = util.time_ms() - alarm.trip_time
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
else
alarm.trip_time = 0
alarm.state = AISTATE.INACTIVE
self.db.alarm_states[alarm.id] = ALARM_STATE.INACTIVE
end
-- alarm tripped and alarming
elseif int_state == AISTATE.TRIPPED then
if tripped then
if ext_state == ALARM_STATE.ACKED then
-- was acked by coordinator
alarm.state = AISTATE.ACKED
end
else
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm acknowledged but still tripped
elseif int_state == AISTATE.ACKED then
if not tripped then
alarm.state = AISTATE.RING_BACK
self.db.alarm_states[alarm.id] = ALARM_STATE.RING_BACK
end
-- alarm no longer tripped, operator must reset to clear
elseif int_state == AISTATE.RING_BACK then
if tripped then
alarm.trip_time = util.time_ms()
if alarm.hold_time > 0 then
alarm.state = AISTATE.RING_BACK_TRIPPING
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
end
elseif ext_state == ALARM_STATE.INACTIVE then
-- was reset by coordinator
alarm.state = AISTATE.INACTIVE
alarm.trip_time = 0
end
else
log.error(util.c("invalid alarm state for unit ", self.r_id, " alarm ", alarm.id), true)
end
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
return alarm.state == AISTATE.TRIPPED
else return false end
end end
-- evaluate alarm conditions -- evaluate alarm conditions
---@param self _unit_self unit instance ---@param self _unit_self
function logic.update_alarms(self) function logic.update_alarms(self)
local annunc = self.db.annunciator local annunc = self.db.annunciator
local plc_cache = self.plc_cache local plc_cache = self.plc_cache
@@ -613,11 +538,9 @@ function logic.update_alarms(self)
end end
-- update the internal automatic safety control performed while in auto control mode -- update the internal automatic safety control performed while in auto control mode
---@param self _unit_self
---@param public reactor_unit reactor unit public functions ---@param public reactor_unit reactor unit public functions
---@param self _unit_self unit instance function logic.update_auto_safety(self, public)
function logic.update_auto_safety(public, self)
local AISTATE = self.types.AISTATE
if self.auto_engaged then if self.auto_engaged then
local alarmed = false local alarmed = false
@@ -644,9 +567,8 @@ function logic.update_auto_safety(public, self)
end end
-- update the two unit status text messages -- update the two unit status text messages
---@param self _unit_self unit instance ---@param self _unit_self
function logic.update_status_text(self) function logic.update_status_text(self)
local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator local annunc = self.db.annunciator
-- check if an alarm is active (tripped or ack'd) -- check if an alarm is active (tripped or ack'd)
@@ -729,7 +651,7 @@ function logic.update_status_text(self)
self.status_text = { "RCS TRANSIENT", "check coolant system" } self.status_text = { "RCS TRANSIENT", "check coolant system" }
-- elseif is_active(self.alarms.RPSTransient) then -- elseif is_active(self.alarms.RPSTransient) then
-- RPS status handled when checking reactor status -- RPS status handled when checking reactor status
elseif self.emcool_opened then elseif self.em_cool_opened then
self.status_text = { "EMERGENCY COOLANT OPENED", "reset RPS to close valve" } self.status_text = { "EMERGENCY COOLANT OPENED", "reset RPS to close valve" }
-- connection dependent states -- connection dependent states
elseif self.plc_i ~= nil then elseif self.plc_i ~= nil then
@@ -808,9 +730,8 @@ function logic.update_status_text(self)
end end
-- handle unit redstone I/O -- handle unit redstone I/O
---@param self _unit_self unit instance ---@param self _unit_self
function logic.handle_redstone(self) function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE
local annunc = self.db.annunciator local annunc = self.db.annunciator
local cache = self.plc_cache local cache = self.plc_cache
local rps = cache.rps_status local rps = cache.rps_status
@@ -887,10 +808,10 @@ function logic.handle_redstone(self)
(annunc.CoolantLevelLow or (boiler_water_low and rps.ex_hcool)) and (annunc.CoolantLevelLow or (boiler_water_low and rps.ex_hcool)) and
is_active(self.alarms.ReactorOverTemp)) is_active(self.alarms.ReactorOverTemp))
if enable_emer_cool and not self.emcool_opened then if enable_emer_cool and not self.em_cool_opened then
log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<")) log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<"))
log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]")) log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]"))
log.debug(util.c("| ReactorOverTemp[", AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]")) log.debug(util.c("| ReactorOverTemp[", alarm_ctl.AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]"))
for i = 1, #annunc.WaterLevelLow do for i = 1, #annunc.WaterLevelLow do
log.debug(util.c("| WaterLevelLow(", i, ")[", annunc.WaterLevelLow[i], "]")) log.debug(util.c("| WaterLevelLow(", i, ")[", annunc.WaterLevelLow[i], "]"))
@@ -911,13 +832,13 @@ function logic.handle_redstone(self)
end end
end end
if annunc.EmergencyCoolant > 1 and self.emcool_opened then if annunc.EmergencyCoolant > 1 and self.em_cool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed")) log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed"))
log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam")) log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam"))
end end
self.emcool_opened = false self.em_cool_opened = false
elseif enable_emer_cool or self.emcool_opened then elseif enable_emer_cool or self.em_cool_opened then
-- set turbines to dump excess steam -- set turbines to dump excess steam
for i = 1, #self.turbines do for i = 1, #self.turbines do
local session = self.turbines[i] local session = self.turbines[i]
@@ -938,16 +859,33 @@ function logic.handle_redstone(self)
end end
end end
if annunc.EmergencyCoolant > 1 and not self.emcool_opened then if annunc.EmergencyCoolant > 1 and not self.em_cool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened")) log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened"))
log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam")) log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam"))
end end
self.emcool_opened = true self.em_cool_opened = true
end end
-- set valve state always -- set valve state always
if self.emcool_opened then self.valves.emer_cool.open() else self.valves.emer_cool.close() end if self.em_cool_opened then self.valves.emer_cool.open() else self.valves.emer_cool.close() end
-----------------------
-- Auxiliary Coolant --
-----------------------
if self.aux_coolant then
if self.enable_aux_cool and (not self.aux_cool_opened) then
log.info(util.c("UNIT ", self.r_id, " auxiliary coolant valve opened"))
self.aux_cool_opened = true
elseif (not self.enable_aux_cool) and self.aux_cool_opened then
log.info(util.c("UNIT ", self.r_id, " auxiliary coolant valve closed"))
self.aux_cool_opened = false
end
-- set valve state always
if self.aux_cool_opened then self.valves.aux_cool.open() else self.valves.aux_cool.close() end
end
end end
return logic return logic