Compare commits

...

116 Commits

Author SHA1 Message Date
9d4cd13911 changed to raw
Some checks failed
Lua Checks / check (push) Successful in 6s
Deploy Installation Data / deploy (push) Failing after 10s
2025-10-29 13:53:09 +01:00
cc2cb8feab changed to http
Some checks failed
Lua Checks / check (push) Successful in 6s
Deploy Installation Data / deploy (push) Failing after 11s
2025-10-29 13:51:54 +01:00
c0dcffe14a Changed installer to use Befator inc git
Some checks failed
Lua Checks / check (push) Successful in 5s
Deploy Installation Data / deploy (push) Failing after 10s
2025-10-29 13:48:18 +01:00
4b8aa3e858 allowed for non-wireless modem setup
Some checks failed
Lua Checks / check (push) Successful in 9s
Deploy Installation Data / deploy (push) Failing after 1m25s
Hardcoded that the Application thinks its in an emulated env to thus allow the use of Wired modems instead of Wireless (because who wants to use wireless modems??!)
2025-10-29 13:45:08 +01:00
Mikayla
c6d526163f
Create CONTRIBUTING.md 2025-10-06 11:50:05 -04:00
Mikayla
6e7c843258
Merge pull request #631 from MikaylaFischler/devel
2025.09.14 Release
2025-09-13 16:10:00 -04:00
Mikayla Fischler
f7fe9754fe #629 luacheck fix for remaining unused variables 2025-09-13 16:07:12 -04:00
Mikayla Fischler
ff68eeae1a #629 removed unused render commands 2025-09-13 16:04:45 -04:00
Mikayla Fischler
20f949a9dd fixed pocket main thread crash on nil current page, added info page to alarm test app 2025-09-13 15:50:25 -04:00
Mikayla
99c9fec195
Merge pull request #628 from MikaylaFischler/pocket-beta-dev
Pocket Beta
2025-09-12 21:56:12 -04:00
Mikayla Fischler
e7a859438e added additional terms to glossary 2025-09-12 21:54:58 -04:00
Mikayla
c81c83f432
Merge pull request #630 from MikaylaFischler/629-pocket-release-cleanup
629 pocket release cleanup
2025-09-12 21:49:26 -04:00
Mikayla Fischler
6ad63aedeb include reordering 2025-09-12 21:49:08 -04:00
Mikayla Fischler
a1bb5ce50b #629 rework of alarm testing app and other cleanup 2025-09-12 21:46:02 -04:00
Mikayla
afc89ac727 #629 code cleanup 2025-09-13 00:55:15 +00:00
Mikayla Fischler
55685fb6a6 #629 home page cleanup 2025-09-12 20:24:42 -04:00
Mikayla Fischler
61f1af7f4e #403 resolved todos in documentation 2025-09-12 20:20:11 -04:00
Mikayla Fischler
16f62bc32a pocket version update 2025-09-12 19:54:51 -04:00
Mikayla Fischler
a2ec418d53 luacheck fix 2025-09-12 19:52:58 -04:00
Mikayla Fischler
384ebb461f #622 comment change and version increment 2025-09-12 19:50:43 -04:00
Mikayla Fischler
0392385037 #622 reinforced induction matrix support 2025-09-12 19:45:15 -04:00
Mikayla Fischler
9e020b2852 #403 remaining documentation 2025-09-12 19:24:29 -04:00
Mikayla Fischler
4f7285573f #403 work on configuration guide 2025-09-12 00:00:01 -04:00
Mikayla Fischler
59f99f70a4 #403 basic connections guide 2025-09-11 23:39:06 -04:00
Mikayla Fischler
df9f1195e3 #403 fixed scroll bar sticking around sometimes 2025-09-11 22:30:39 -04:00
Mikayla Fischler
aba79e88cf #403 auto control usage guide 2025-09-11 22:28:58 -04:00
Mikayla
d0276e149b #400 revert changes 2025-09-12 01:22:27 +00:00
Mikayla Fischler
eb95f2331d #403 waste control doc updates 2025-09-02 14:33:10 -04:00
Mikayla
28150042cc #403 waste control usage documentation 2025-09-02 15:38:43 +00:00
Mikayla
4bc5af46ab #403 spelling fixes 2025-09-02 14:17:43 +00:00
Mikayla Fischler
2cee1ea895 #403 start of auto and waste control docs 2025-09-01 23:06:13 -04:00
Mikayla Fischler
7d0bbafd6c #403 manual control guide 2025-09-01 19:09:49 -04:00
Mikayla Fischler
dc19127836 #403 guide loading detailed info 2025-09-01 18:18:51 -04:00
Mikayla Fischler
6db6a7d7b7 #403 flow display documentation 2025-09-01 17:54:21 -04:00
Mikayla Fischler
017deec06e #403 unit display documentation 2025-09-01 17:05:31 -04:00
Mikayla Fischler
d0401fe51f #403 main display documentation 2025-09-01 16:37:21 -04:00
Mikayla Fischler
92113671ff #403 front panel documentation complete 2025-08-24 19:33:31 -04:00
Mikayla Fischler
e3d0692dcc #400 start of peripherals list app 2025-08-24 18:46:07 -04:00
Mikayla Fischler
83e29abea7 #401 bug fixes 2025-08-19 22:14:19 -04:00
Mikayla Fischler
4a1730ec47 #401 cleanup and global RTT limit constants 2025-08-19 18:43:43 -04:00
Mikayla Fischler
691b781c52 #401 slowed polling rate for computer app 2025-08-19 18:42:48 -04:00
Mikayla Fischler
3b856655c3 #401 comment updates 2025-08-13 11:46:26 -03:00
Mikayla Fischler
415cb71294 #401 working RTU and PKT computer lists 2025-08-13 11:32:49 -03:00
Mikayla Fischler
a678b8dbe0 #401 fix for supervisor coordinator RTT reporting 2025-08-13 11:17:30 -03:00
Mikayla Fischler
9bb3f59496 reworked pocket app loader 2025-08-13 11:15:33 -03:00
Mikayla Fischler
170cba702c PSIL updates stored value before notifying subscribers 2025-08-13 10:46:09 -03:00
Mikayla Fischler
fe78360948 #401 working main page of computer list app 2025-08-13 10:45:32 -03:00
Mikayla
e29a88eeea #401 sidebar updates 2025-08-06 16:00:28 +00:00
Mikayla
f3eb6d0464 #401 PLC, RTU, PKT computer lists 2025-08-06 15:56:06 +00:00
Mikayla
f4d4de659c #401 supervisor and coordinator computer display 2025-08-05 13:35:29 +00:00
Mikayla
1ce0bbfc65 #401 work on pocket computer list app 2025-08-01 21:43:19 +00:00
Mikayla
0bfe767710 #401 pocket handling of computer data 2025-08-01 21:41:40 +00:00
Mikayla
4fb39213f2 #401 supervisor support of pocket computer list app 2025-07-01 14:41:09 +00:00
Mikayla
6eb9ac5845
Merge pull request #626 from MikaylaFischler/devel
2025.06.29 Release
2025-06-29 16:48:45 -04:00
Mikayla Fischler
acaa9369f4 added new! tags to reactor PLC configurator 2025-06-29 16:32:40 -04:00
Mikayla Fischler
2998371b89 #623 PLC option to invert emergency coolant control 2025-06-27 11:46:50 -04:00
Mikayla
12664c6190
Merge pull request #624 from MikaylaFischler/pocket-alpha-dev
Radiation App
2025-06-21 14:59:46 -04:00
Mikayla Fischler
abe0c45534 cleanup, #593 fixes, version increment 2025-06-21 14:58:45 -04:00
Mikayla Fischler
a104d8ba83 #593 radiation app fixes/cleanup 2025-06-21 10:45:43 -04:00
Mikayla
a629c04d11 #593 radiation monitor lists 2025-06-16 19:21:16 +00:00
Mikayla
6d3b35a41d #621 support environment_detector peripheral type 2025-06-16 15:07:56 +00:00
Mikayla Fischler
e1ac42f5f8 #593 structure and graphics elements for the radiation monitor app 2025-06-15 18:24:36 -04:00
Mikayla
9e59883a84 #593 radiation monitor data comms 2025-06-03 14:10:30 +00:00
Mikayla Fischler
79d63fce78 #593 start of radiation monitor app 2025-05-24 17:17:57 -04:00
Mikayla Fischler
ce92fd15ef Merge branch 'devel' into pocket-alpha-dev 2025-05-10 17:51:17 -04:00
Mikayla
919ca6f0af
Merge pull request #620 from MikaylaFischler/devel
2025.05.10 Release
2025-05-10 17:45:19 -04:00
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
86dc92f09a Merge branch 'devel' into pocket-alpha-dev 2025-04-29 14:33:05 +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
13509136b8 cleanup 2025-04-19 22:20:57 +00: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
81 changed files with 3133 additions and 996 deletions

57
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,57 @@
# Contribution Guide
>[!NOTE]
Until the system is out of beta, contributions will be limited as I wrap up the specific release feature set.
This project is highly complex for a ComputerCraft Lua application. Contributions need to follow style guides and meet the code quality I've kept this project up to for years. Contributions must be tested appropriately with test results included.
I have extensively tested software components for stability required for safety, with tiers of software robustness.
1. **Critical: High-Impact** -
The Reactor-PLC is "uncrashable" and must remain so. I've extensively reviewed every line and behavior, so any code contributions must be at this high standard. Simple is stable, so the less code the better. Always check for parameter validity and extensively test any changes to critical thread functions.
2. **Important: Moderate-Impact** -
The Supervisor and RTU Gateway should rarely, if ever, crash. Certain places may not be held to as strict of a level as above, but should be written understanding all the possible inputs to and impacts of a section of code.
3. **Useful: Low-Impact** -
The Coordinator and Pocket are nice UI apps, and things can break. There's a lot of data going to and from them, so checking every single incoming value would have negative performance impacts and increase program size. If they break, the user can restart them. Don't introduce careless bugs, but making assumptions about the integrity of incoming data is acceptable.
## Valuable Contributions
Pull requests should not consist of purely whitespace changes, comment changes, or other trivial changes. They should target specific features, bug fixes, or functional improvements. I reserve the right to decline PRs that don't follow this in good faith.
## Project Management Guidelines
Any contributions should be linked to an open GitHub issue. These are used to track progress, discuss changes, etc. Surprise changes to this project might conflict with existing plans, so I prefer we coordinate changes ahead of time.
## Software Guidelines
These guidelines are subject to change. The general rule is make the code look like the rest of the code around it and elsewhere in the project.
### Style Guide
PRs will only be accepted if they match the style of this project and pass manual and automated code analysis. Listing out the whole style guide would take a while, so as stated above, please review code adjacent to your modifications.
1. **No Block Comments.**
These interfere with the minification used for the bundled installation files due to the complexity of parsing Lua block comments. The minification code is meant to be simple to have 0 risk of breaking anything, so I'm staying far away from those.
2. **Comment Your Code.**
This includes type hints as used elsewhere throughout the project. Your comments should be associated with parts of code that are more complex or unclear, or otherwise to split up sections of tasks. You'll see `--#region` used in various places.
- Type hints are intended to be utilized by the `sumneko.lua` vscode extension. You should use this while developing, as it provides extremely valuable functionality.
3. **Whitespace Usage.**
Whitespace should be used to separate function parameters and operators. The one exception is the unique styling of graphics elements, which you should compare against if modifying them.
- 4 spaces are used for all indentation.
- Try to align assignment operator lines as is done elsewhere (adding space before `=`).
- Use empty new lines to separate steps or distinct groups of operations.
- Generally add new lines for each step in loops and for statements. For some single-line ones, they may be compressed into a single line. This saves on space utilization, especially on deeply indented lines.
4. **Variables and Classes.**
- Variables, functions, and class-like tables follow the snake_case convention.
- Graphics objects and configuration settings follow PascalCase.
- Constants follow all-caps SNAKE_CASE and local ones should be declared at the top of files after `require` statements and external ones (like `local ALARM = types.ALARM`).
5. **No `goto`.**
These are generally frowned upon due to reducing code readability.
6. **Multiple `return`s.**
These are allowed to minimize code size, but if it is simple to avoid multiple, do so.
7. **Classes and Objects.**
Review the existing code for examples on how objects are implemented in this project. They do not use Lua's `:` operator and `self` functionality. A manual object-like table definition is used. Some global single-instance classes don't use a `new()` function, such as the [PPM](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/scada-common/ppm.lua). Multi-instance ones do, such as the Supervisor's [unit](https://github.com/MikaylaFischler/cc-mek-scada/blob/main/supervisor/unit.lua) class.
### No AI
Your code should follow the style guide, be succinct, make sense, and you should be able to explain what it does. Random changes done in multiple places will be deemed suspicious along with poor comments or nonsensical code.
Use your contributions as programming practice or to hone your skills; don't automate away thinking.

View File

@ -19,7 +19,7 @@ local CCMSI_VERSION = "v1.21"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/" local repo_path = "http://git.befatorinc.de/TheHomecraft/cc-mek-scada/raw/"
---@diagnostic disable-next-line: undefined-global ---@diagnostic disable-next-line: undefined-global
local _is_pkt_env = pocket -- luacheck: ignore pocket local _is_pkt_env = pocket -- luacheck: ignore pocket

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

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)

View File

@ -132,7 +132,9 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
sps_data_tbl = {}, ---@type sps_session_db[] sps_data_tbl = {}, ---@type sps_session_db[]
tank_ps_tbl = {}, ---@type psil[] tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[] tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
} }
-- create induction and SPS tables (currently only 1 of each is supported) -- create induction and SPS tables (currently only 1 of each is supported)
@ -242,7 +244,9 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
turbine_data_tbl = {}, ---@type turbinev_session_db[] turbine_data_tbl = {}, ---@type turbinev_session_db[]
tank_ps_tbl = {}, ---@type psil[] tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[] tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
} }
-- on other facility modes, overwrite unit TANK option with facility tank defs -- on other facility modes, overwrite unit TANK option with facility tank defs
@ -797,7 +801,9 @@ function iocontrol.update_facility_status(status)
if type(rtu_statuses.envds) == "table" then if type(rtu_statuses.envds) == "table" then
local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false local max_rad, max_reading, any_conn, any_faulted = 0, types.new_zero_radiation_reading(), false, false
for _, envd in pairs(rtu_statuses.envds) do fac.rad_monitors = {}
for id, envd in pairs(rtu_statuses.envds) do
local rtu_faulted = envd[1] ---@type boolean local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number local rad_raw = envd[3] ---@type number
@ -809,6 +815,10 @@ function iocontrol.update_facility_status(status)
max_rad = rad_raw max_rad = rad_raw
max_reading = radiation max_reading = radiation
end end
if not rtu_faulted then
fac.rad_monitors[id] = { radiation = radiation, raw = rad_raw }
end
end end
if any_conn then if any_conn then
@ -1099,7 +1109,10 @@ function iocontrol.update_unit_statuses(statuses)
if type(rtu_statuses.envds) == "table" then if type(rtu_statuses.envds) == "table" then
local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false local max_rad, max_reading, any_conn = 0, types.new_zero_radiation_reading(), false
for _, envd in pairs(rtu_statuses.envds) do unit.rad_monitors = {}
for id, envd in pairs(rtu_statuses.envds) do
local rtu_faulted = envd[1] ---@type boolean
local radiation = envd[2] ---@type radiation_reading local radiation = envd[2] ---@type radiation_reading
local rad_raw = envd[3] ---@type number local rad_raw = envd[3] ---@type number
@ -1109,6 +1122,10 @@ function iocontrol.update_unit_statuses(statuses)
max_rad = rad_raw max_rad = rad_raw
max_reading = radiation max_reading = radiation
end end
if not rtu_faulted then
unit.rad_monitors[id] = { radiation = radiation, raw = rad_raw }
end
end end
if any_conn then if any_conn then

View File

@ -427,6 +427,13 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
} }
_send(CRDN_TYPE.API_GET_WASTE, data) _send(CRDN_TYPE.API_GET_WASTE, data)
elseif pkt.type == CRDN_TYPE.API_GET_RAD then
local data = {}
for i = 1, #db.units do data[i] = db.units[i].rad_monitors end
data[#db.units + 1] = db.facility.rad_monitors
_send(CRDN_TYPE.API_GET_RAD, data)
else else
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
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.11" local COORDINATOR_VERSION = "v1.6.16"
local CHUNK_LOAD_DELAY_S = 30.0 local CHUNK_LOAD_DELAY_S = 30.0

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

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

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

View File

@ -7,11 +7,15 @@ 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,16 +227,22 @@ style.sps = {
} }
} }
style.waste = { -- get waste styling, which depends on the configuration
---@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 } }
function style.get_waste()
local pu_color = util.trinary(config.GreenPuPellet, colors.green, colors.cyan)
local po_color = util.trinary(config.GreenPuPellet, colors.cyan, colors.green)
return {
-- auto waste processing states -- auto waste processing states
states = { states = {
{ color = cpair(colors.black, colors.green), text = "PLUTONIUM" }, { color = cpair(colors.black, pu_color), text = "PLUTONIUM" },
{ color = cpair(colors.black, colors.cyan), text = "POLONIUM" }, { color = cpair(colors.black, po_color), text = "POLONIUM" },
{ color = cpair(colors.black, colors.purple), text = "ANTI MATTER" } { color = cpair(colors.black, colors.purple), text = "ANTI MATTER" }
}, },
states_abbrv = { states_abbrv = {
{ color = cpair(colors.black, colors.green), text = "Pu" }, { color = cpair(colors.black, pu_color), text = "Pu" },
{ color = cpair(colors.black, colors.cyan), text = "Po" }, { color = cpair(colors.black, po_color), text = "Po" },
{ color = cpair(colors.black, colors.purple), text = "AM" } { color = cpair(colors.black, colors.purple), text = "AM" }
}, },
-- process radio button options -- process radio button options
@ -240,10 +250,11 @@ style.waste = {
-- unit waste selection -- unit waste selection
unit_opts = { unit_opts = {
{ text = "Auto", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.white, colors.gray) }, { 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, colors.green) }, { 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, colors.cyan) }, { 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) } { text = "AM", fg_bg = cpair(colors.black, colors.lightGray), active_fg_bg = cpair(colors.black, colors.purple) }
} }
} }
end
return style return style

View File

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

View File

@ -86,6 +86,13 @@ return function (args)
e.redraw() e.redraw()
end end
-- change the foreground color of the text
---@param c color
function e.recolor(c)
e.w_set_fgd(c)
e.redraw()
end
---@class TextBox:graphics_element ---@class TextBox:graphics_element
local TextBox, id = e.complete(true) local TextBox, id = e.complete(true)

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

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

@ -17,7 +17,6 @@ local ENERGY_UNITS = types.ENERGY_SCALE_UNITS
local TEMP_SCALE = types.TEMP_SCALE local TEMP_SCALE = types.TEMP_SCALE
local TEMP_UNITS = types.TEMP_SCALE_UNITS local TEMP_UNITS = types.TEMP_SCALE_UNITS
---@todo nominal trip time is ping (0ms to 10ms usually)
local WARN_TT = 40 local WARN_TT = 40
local HIGH_TT = 80 local HIGH_TT = 80
@ -35,8 +34,9 @@ iocontrol.LINK_STATE = LINK_STATE
---@class pocket_ioctl ---@class pocket_ioctl
local io = { local io = {
version = "unknown", version = "unknown", -- pocket version
ps = psil.create() ps = psil.create(), -- pocket PSIL
loader_require = { sv = false, api = false }
} }
local config = nil ---@type pkt_config local config = nil ---@type pkt_config
@ -85,12 +85,13 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
get_tone_states = function () comms.diag__get_alarm_tones() end, get_tone_states = function () comms.diag__get_alarm_tones() end,
ready_warn = nil, ---@type TextBox
tone_buttons = {}, ---@type SwitchButton[] tone_buttons = {}, ---@type SwitchButton[]
alarm_buttons = {}, ---@type Checkbox[] alarm_buttons = {} ---@type Checkbox[]
tone_indicators = {} ---@type IndicatorLight[] indicators to update from supervisor tone states
} }
-- computer list
io.diag.get_comps = function () comms.diag__get_computers() end
-- API access -- API access
---@class pocket_ioctl_api ---@class pocket_ioctl_api
io.api = { io.api = {
@ -98,7 +99,8 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
get_unit = function (unit) comms.api__get_unit(unit) end, get_unit = function (unit) comms.api__get_unit(unit) end,
get_ctrl = function () comms.api__get_control() end, get_ctrl = function () comms.api__get_control() end,
get_proc = function () comms.api__get_process() end, get_proc = function () comms.api__get_process() end,
get_waste = function () comms.api__get_waste() end get_waste = function () comms.api__get_waste() end,
get_rad = function () comms.api__get_rad() end
} }
end end
@ -184,7 +186,9 @@ function iocontrol.init_fac(conf)
sps_data_tbl = {}, ---@type sps_session_db[] sps_data_tbl = {}, ---@type sps_session_db[]
tank_ps_tbl = {}, ---@type psil[] tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[] tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
} }
-- create induction and SPS tables (currently only 1 of each is supported) -- create induction and SPS tables (currently only 1 of each is supported)
@ -264,7 +268,9 @@ function iocontrol.init_fac(conf)
turbine_data_tbl = {}, ---@type turbinev_session_db[] turbine_data_tbl = {}, ---@type turbinev_session_db[]
tank_ps_tbl = {}, ---@type psil[] tank_ps_tbl = {}, ---@type psil[]
tank_data_tbl = {} ---@type dynamicv_session_db[] tank_data_tbl = {}, ---@type dynamicv_session_db[]
rad_monitors = {} ---@type { radiation: radiation_reading, raw: number }[]
} }
-- on other facility modes, overwrite unit TANK option with facility tank defs -- on other facility modes, overwrite unit TANK option with facility tank defs

View File

@ -2,10 +2,13 @@
-- I/O Control's Data Receive (Rx) Handlers -- I/O Control's Data Receive (Rx) Handlers
-- --
local comms = require("scada-common.comms")
local const = require("scada-common.constants") local const = require("scada-common.constants")
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 DEV_TYPE = comms.DEVICE_TYPE
local ALARM = types.ALARM local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
@ -57,7 +60,6 @@ local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed) ps.publish("formed", data.formed)
ps.publish("faulted", faulted) ps.publish("faulted", faulted)
---@todo revisit this
if data.build then if data.build then
for key, val in pairs(data.build) do ps.publish(key, val) end for key, val in pairs(data.build) do ps.publish(key, val) end
end end
@ -314,7 +316,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
@ -658,7 +660,6 @@ function iorx.record_waste_data(data)
fac.ps.publish("sps_process_rate", f_data[9]) fac.ps.publish("sps_process_rate", f_data[9])
end end
-- update facility app with facility and unit data from API_GET_FAC_DTL -- update facility app with facility and unit data from API_GET_FAC_DTL
---@param data table ---@param data table
function iorx.record_fac_detail_data(data) function iorx.record_fac_detail_data(data)
@ -819,6 +820,135 @@ function iorx.record_fac_detail_data(data)
s_ps.publish("SPSStateStatus", s_stat) s_ps.publish("SPSStateStatus", s_stat)
end end
-- update the radiation monitor app with radiation monitor data from API_GET_RAD
---@param data table
function iorx.record_radiation_data(data)
-- unit radiation monitors
for u_id = 1, #io.units do
local unit = io.units[u_id]
local max_rad = 0
local connected = {}
unit.radiation = types.new_zero_radiation_reading()
unit.rad_monitors = data[u_id]
for id, mon in pairs(unit.rad_monitors) do
table.insert(connected, id)
unit.unit_ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
unit.radiation = mon.radiation
end
end
unit.unit_ps.publish("radiation", unit.radiation)
unit.unit_ps.publish("radiation_monitors", textutils.serialize(connected))
end
-- facility radiation monitors
local fac = io.facility
fac.radiation = types.new_zero_radiation_reading()
fac.rad_monitors = data[#io.units + 1]
local max_rad = 0
local connected = {}
for id, mon in pairs(fac.rad_monitors) do
table.insert(connected, id)
fac.ps.publish("radiation@" .. id, mon.radiation)
if mon.raw > max_rad then
max_rad = mon.raw
fac.radiation = mon.radiation
end
end
fac.ps.publish("radiation", fac.radiation)
fac.ps.publish("radiation_monitors", textutils.serialize(connected))
end
local comp_record = {}
-- update the computers app with the network data from INFO_LIST_CMP
---@param data table
function iorx.record_network_data(data)
local ps = io.ps
local connected = {}
local crd_online = false
ps.publish("comp_online", #data)
-- add/update connected computers
for i = 1, #data do
local entry = data[i]
local type = entry[1]
local id = entry[2]
local pfx = "comp_" .. id
connected[id] = true
if type == DEV_TYPE.SVR then
ps.publish("comp_svr_addr", id)
ps.publish("comp_svr_fw", entry[3])
elseif type == DEV_TYPE.CRD then
crd_online = true
ps.publish("comp_crd_addr", id)
ps.publish("comp_crd_fw", entry[3])
ps.publish("comp_crd_rtt", entry[4])
else
ps.publish(pfx .. "_type", entry[1])
ps.publish(pfx .. "_addr", id)
ps.publish(pfx .. "_fw", entry[3])
ps.publish(pfx .. "_rtt", entry[4])
if type == DEV_TYPE.PLC then
ps.publish(pfx .. "_unit", entry[5])
end
if not comp_record[id] then
comp_record[id] = true
-- trigger the app to create the new element
ps.publish("comp_connect", id)
end
end
end
-- handle the coordinator being online or not
-- no need to worry about the supervisor since this data is from the supervisor, so it has to be 'online' if received
ps.publish("comp_crd_online", crd_online)
if not crd_online then
ps.publish("comp_crd_addr", "---")
ps.publish("comp_crd_fw", "---")
ps.publish("comp_crd_rtt", "---")
end
-- reset the published value
ps.publish("comp_connect", false)
-- remove disconnected computers
for id, state in pairs(comp_record) do
if state and not connected[id] then
comp_record[id] = false
-- trigger the app to delete the element
ps.publish("comp_disconnect", id)
end
end
-- reset the published value
ps.publish("comp_disconnect", false)
end
-- clear the tracked connected computer record
function iorx.clear_comp_record() comp_record = {} end
return function (io_obj) return function (io_obj)
io = io_obj io = io_obj
return iorx return iorx

View File

@ -16,16 +16,10 @@ local LINK_STATE = iocontrol.LINK_STATE
local pocket = {} local pocket = {}
local MQ__RENDER_CMD = {
UNLOAD_SV_APPS = 1,
UNLOAD_API_APPS = 2
}
local MQ__RENDER_DATA = { local MQ__RENDER_DATA = {
LOAD_APP = 1 LOAD_APP = 1
} }
pocket.MQ__RENDER_CMD = MQ__RENDER_CMD
pocket.MQ__RENDER_DATA = MQ__RENDER_DATA pocket.MQ__RENDER_DATA = MQ__RENDER_DATA
---@type pkt_config ---@type pkt_config
@ -38,6 +32,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 +49,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)
@ -86,7 +82,7 @@ local APP_ID = {
-- core UI -- core UI
ROOT = 1, ROOT = 1,
LOADER = 2, LOADER = 2,
-- main app pages -- main apps
UNITS = 3, UNITS = 3,
FACILITY = 4, FACILITY = 4,
CONTROL = 5, CONTROL = 5,
@ -94,11 +90,12 @@ local APP_ID = {
WASTE = 7, WASTE = 7,
GUIDE = 8, GUIDE = 8,
ABOUT = 9, ABOUT = 9,
-- diagnostic app pages RADMON = 10,
ALARMS = 10, -- diagnostic apps
-- other ALARMS = 11,
DUMMY = 11, COMPS = 12,
NUM_APPS = 11 -- count
NUM_APPS = 12
} }
pocket.APP_ID = APP_ID pocket.APP_ID = APP_ID
@ -147,7 +144,7 @@ function pocket.init_nav(smem)
---@class pocket_app ---@class pocket_app
local app = { local app = {
loaded = false, loaded = false,
cur_page = nil, ---@type nav_tree_page cur_page = nil, ---@type nav_tree_page|nil
pane = pane, pane = pane,
paned_pages = {}, ---@type nav_tree_page[] paned_pages = {}, ---@type nav_tree_page[]
sidebar_items = {} ---@type sidebar_entry[] sidebar_items = {} ---@type sidebar_entry[]
@ -267,21 +264,28 @@ 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
local app = self.apps[app_id] local app = self.apps[app_id]
if app then if app then
if app.requires_conn() and not smem.pkt_sys.pocket_comms.is_linked() then local p_comms = smem.pkt_sys.pocket_comms
local req_sv, req_api = app.check_requires()
if (req_sv and not p_comms.is_sv_linked()) or (req_api and not p_comms.is_api_linked()) then
-- report required connction(s)
iocontrol.get_db().loader_require = { sv = req_sv, api = req_api }
iocontrol.get_db().ps.toggle("loader_reqs")
-- bring up the app loader -- bring up the app loader
self.loader_return = app_id self.loader_return = app_id
app_id = APP_ID.LOADER app_id = APP_ID.LOADER
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 +293,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
@ -336,7 +342,7 @@ function pocket.init_nav(smem)
function nav.get_containers() return self.containers end function nav.get_containers() return self.containers end
-- get the currently active page -- get the currently active page
---@return nav_tree_page ---@return nav_tree_page|nil
function nav.get_current_page() function nav.get_current_page()
return self.apps[self.cur_app].get_current_page() return self.apps[self.cur_app].get_current_page()
end end
@ -365,8 +371,7 @@ function pocket.init_nav(smem)
self.help_return = self.cur_app self.help_return = self.cur_app
nav.open_app(APP_ID.GUIDE, function () nav.open_app(APP_ID.GUIDE, function ()
local show = self.help_map[key] if self.help_map[key] then self.help_map[key]() end
if show then show() end
end) end)
end end
@ -554,6 +559,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end end
-- supervisor get connected computers
function public.diag__get_computers()
if self.sv.linked then _send_sv(MGMT_TYPE.INFO_LIST_CMP, {}) end
end
-- coordinator get facility app data -- coordinator get facility app data
function public.api__get_facility() function public.api__get_facility()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) end if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) end
@ -579,6 +589,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end if self.api.linked then _send_api(CRDN_TYPE.API_GET_WASTE, {}) end
end end
-- coordinator get radiation app data
function public.api__get_rad()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_RAD, {}) end
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)
@ -655,6 +670,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
---@param packet mgmt_frame|crdn_frame|nil ---@param packet mgmt_frame|crdn_frame|nil
function public.handle_packet(packet) function public.handle_packet(packet)
local diag = iocontrol.get_db().diag local diag = iocontrol.get_db().diag
local ps = iocontrol.get_db().ps
if packet ~= nil then if packet ~= nil then
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
@ -755,6 +771,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if _check_length(packet, #iocontrol.get_db().units + 1) then if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_waste_data(packet.data) iocontrol.rx.record_waste_data(packet.data)
end end
elseif packet.type == CRDN_TYPE.API_GET_RAD then
if _check_length(packet, #iocontrol.get_db().units + 1) then
iocontrol.rx.record_radiation_data(packet.data)
end
else _fail_type(packet) end else _fail_type(packet) end
else else
log.debug("discarding coordinator SCADA_CRDN packet before linked") log.debug("discarding coordinator SCADA_CRDN packet before linked")
@ -902,23 +922,23 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then elseif packet.type == MGMT_TYPE.DIAG_TONE_GET then
if _check_length(packet, 8) then if _check_length(packet, 8) then
for i = 1, #packet.data do for i = 1, #packet.data do
diag.tone_test.tone_indicators[i].update(packet.data[i] == true) ps.publish("alarm_tone_" .. i, packet.data[i] == true)
end end
end end
elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then elseif packet.type == MGMT_TYPE.DIAG_TONE_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied") ps.publish("alarm_ready_warn", "testing denied")
log.debug("supervisor SCADA diag tone set failed") log.debug("supervisor SCADA diag tone set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1] local ready = packet.data[1]
local states = packet.data[2] local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not idle")) ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
for i = 1, #states do for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then if diag.tone_test.tone_buttons[i] ~= nil then
diag.tone_test.tone_buttons[i].set_value(states[i] == true) diag.tone_test.tone_buttons[i].set_value(states[i] == true)
diag.tone_test.tone_indicators[i].update(states[i] == true) ps.publish("alarm_tone_" .. i, states[i] == true)
end end
end end
else else
@ -926,13 +946,13 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
end end
elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then elseif packet.type == MGMT_TYPE.DIAG_ALARM_SET then
if packet.length == 1 and packet.data[1] == false then if packet.length == 1 and packet.data[1] == false then
diag.tone_test.ready_warn.set_value("testing denied") ps.publish("alarm_ready_warn", "testing denied")
log.debug("supervisor SCADA diag alarm set failed") log.debug("supervisor SCADA diag alarm set failed")
elseif packet.length == 2 and type(packet.data[2]) == "table" then elseif packet.length == 2 and type(packet.data[2]) == "table" then
local ready = packet.data[1] local ready = packet.data[1]
local states = packet.data[2] local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not idle")) ps.publish("alarm_ready_warn", util.trinary(ready, "", "system not idle"))
for i = 1, #states do for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then if diag.tone_test.alarm_buttons[i] ~= nil then
@ -942,6 +962,8 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
else else
log.debug("supervisor SCADA diag alarm set packet length/type mismatch") log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
end end
elseif packet.type == MGMT_TYPE.INFO_LIST_CMP then
iocontrol.rx.record_network_data(packet.data)
else _fail_type(packet) end else _fail_type(packet) end
elseif packet.type == MGMT_TYPE.ESTABLISH then elseif packet.type == MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established

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.1-beta" local POCKET_VERSION = "v1.0.3"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

View File

@ -14,7 +14,6 @@ local threads = {}
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
local RENDER_SLEEP = 100 -- (100ms, 2 ticks) local RENDER_SLEEP = 100 -- (100ms, 2 ticks)
local MQ__RENDER_CMD = pocket.MQ__RENDER_CMD
local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA local MQ__RENDER_DATA = pocket.MQ__RENDER_DATA
-- main thread -- main thread
@ -58,8 +57,10 @@ function threads.thread__main(smem)
pocket_comms.link_update() pocket_comms.link_update()
-- update any tasks for the active page -- update any tasks for the active page
if nav.get_current_page() then
local page_tasks = nav.get_current_page().tasks local page_tasks = nav.get_current_page().tasks
for i = 1, #page_tasks do page_tasks[i]() end for i = 1, #page_tasks do page_tasks[i]() end
end
loop_clock.start() loop_clock.start()
elseif sv_wd.is_timer(param1) then elseif sv_wd.is_timer(param1) then
@ -157,9 +158,6 @@ function threads.thread__render(smem)
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.COMMAND then if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
if msg.message == MQ__RENDER_CMD.UNLOAD_SV_APPS then
elseif msg.message == MQ__RENDER_CMD.UNLOAD_API_APPS then
end
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
-- received data -- received data
local cmd = msg.message ---@type queue_data local cmd = msg.message ---@type queue_data

View File

@ -1,5 +1,5 @@
-- --
-- System Apps -- About Page
-- --
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
@ -24,25 +24,21 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID local APP_ID = pocket.APP_ID
-- create system app pages -- create about page view
---@param root Container parent ---@param root Container parent
local function create_pages(root) local function create_pages(root)
local db = iocontrol.get_db() local db = iocontrol.get_db()
---------------- local frame = Div{parent=root,x=1,y=1}
-- About Page --
----------------
local about_root = Div{parent=root,x=1,y=1} local app = db.nav.register_app(APP_ID.ABOUT, frame)
local about_app = db.nav.register_app(APP_ID.ABOUT, about_root) local about_page = app.new_page(nil, 1)
local nt_page = app.new_page(about_page, 2)
local fw_page = app.new_page(about_page, 3)
local hw_page = app.new_page(about_page, 4)
local about_page = about_app.new_page(nil, 1) local about = Div{parent=frame,x=1,y=2}
local nt_page = about_app.new_page(about_page, 2)
local fw_page = about_app.new_page(about_page, 3)
local hw_page = about_app.new_page(about_page, 4)
local about = Div{parent=about_root,x=1,y=2}
TextBox{parent=about,y=1,text="System Information",alignment=ALIGN.CENTER} TextBox{parent=about,y=1,text="System Information",alignment=ALIGN.CENTER}
@ -58,7 +54,7 @@ local function create_pages(root)
local config = pocket.config local config = pocket.config
local nt_div = Div{parent=about_root,x=1,y=2} local nt_div = Div{parent=frame,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",alignment=ALIGN.CENTER} TextBox{parent=nt_div,y=1,text="Network Details",alignment=ALIGN.CENTER}
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@ -87,7 +83,7 @@ local function create_pages(root)
--#region Firmware Versions --#region Firmware Versions
local fw_div = Div{parent=about_root,x=1,y=2} local fw_div = Div{parent=frame,x=1,y=2}
TextBox{parent=fw_div,y=1,text="Firmware Versions",alignment=ALIGN.CENTER} TextBox{parent=fw_div,y=1,text="Firmware Versions",alignment=ALIGN.CENTER}
PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} PushButton{parent=fw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@ -123,7 +119,7 @@ local function create_pages(root)
--#region Host Versions --#region Host Versions
local hw_div = Div{parent=about_root,x=1,y=2} local hw_div = Div{parent=frame,x=1,y=2}
TextBox{parent=hw_div,y=1,text="Host Versions",alignment=ALIGN.CENTER} TextBox{parent=hw_div,y=1,text="Host Versions",alignment=ALIGN.CENTER}
PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to} PushButton{parent=hw_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
@ -138,9 +134,9 @@ local function create_pages(root)
--#endregion --#endregion
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}} local root_pane = MultiPane{parent=frame,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
about_app.set_root_pane(root_pane) app.set_root_pane(root_pane)
end end
return create_pages return create_pages

184
pocket/ui/apps/alarm.lua Normal file
View File

@ -0,0 +1,184 @@
--
-- Alarm Test App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local SwitchButton = require("graphics.elements.controls.SwitchButton")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
-- create alarm test page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local ps = db.ps
local ttest = db.diag.tone_test
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.ALARMS, frame, nil, true)
local main = Div{parent=frame,x=1,y=1}
local page_div = Div{parent=main,y=2,width=main.get_width()}
--#region alarm testing
local alarm_page = app.new_page(nil, 1)
alarm_page.tasks = { db.diag.tone_test.get_tone_states }
local alarms_div = Div{parent=page_div}
TextBox{parent=alarms_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
local alarm_ready_warn = TextBox{parent=alarms_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
alarm_ready_warn.register(ps, "alarm_ready_warn", alarm_ready_warn.set_value)
local alarm_page_states = Div{parent=alarms_div,x=2,y=3,height=5,width=8}
TextBox{parent=alarm_page_states,text="States",alignment=ALIGN.CENTER}
local ta_1 = IndicatorLight{parent=alarm_page_states,label="1",colors=c_blue_gray}
local ta_2 = IndicatorLight{parent=alarm_page_states,label="2",colors=c_blue_gray}
local ta_3 = IndicatorLight{parent=alarm_page_states,label="3",colors=c_blue_gray}
local ta_4 = IndicatorLight{parent=alarm_page_states,label="4",colors=c_blue_gray}
local ta_5 = IndicatorLight{parent=alarm_page_states,x=6,y=2,label="5",colors=c_blue_gray}
local ta_6 = IndicatorLight{parent=alarm_page_states,x=6,label="6",colors=c_blue_gray}
local ta_7 = IndicatorLight{parent=alarm_page_states,x=6,label="7",colors=c_blue_gray}
local ta_8 = IndicatorLight{parent=alarm_page_states,x=6,label="8",colors=c_blue_gray}
local ta = { ta_1, ta_2, ta_3, ta_4, ta_5, ta_6, ta_7, ta_8 }
for i = 1, #ta do
ta[i].register(ps, "alarm_tone_" .. i, ta[i].update)
end
local alarms = Div{parent=alarms_div,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=alarms_div.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
--#endregion
--#region direct tone testing
local tones_page = app.new_page(nil, 2)
tones_page.tasks = { db.diag.tone_test.get_tone_states }
local tones_div = Div{parent=page_div}
TextBox{parent=tones_div,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
local tone_ready_warn = TextBox{parent=tones_div,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
tone_ready_warn.register(ps, "alarm_ready_warn", tone_ready_warn.set_value)
local tone_page_states = Div{parent=tones_div,x=3,y=3,height=5,width=8}
TextBox{parent=tone_page_states,text="States",alignment=ALIGN.CENTER}
local tt_1 = IndicatorLight{parent=tone_page_states,label="1",colors=c_blue_gray}
local tt_2 = IndicatorLight{parent=tone_page_states,label="2",colors=c_blue_gray}
local tt_3 = IndicatorLight{parent=tone_page_states,label="3",colors=c_blue_gray}
local tt_4 = IndicatorLight{parent=tone_page_states,label="4",colors=c_blue_gray}
local tt_5 = IndicatorLight{parent=tone_page_states,x=6,y=2,label="5",colors=c_blue_gray}
local tt_6 = IndicatorLight{parent=tone_page_states,x=6,label="6",colors=c_blue_gray}
local tt_7 = IndicatorLight{parent=tone_page_states,x=6,label="7",colors=c_blue_gray}
local tt_8 = IndicatorLight{parent=tone_page_states,x=6,label="8",colors=c_blue_gray}
local tt = { tt_1, tt_2, tt_3, tt_4, tt_5, tt_6, tt_7, tt_8 }
for i = 1, #tt do
tt[i].register(ps, "alarm_tone_" .. i, tt[i].update)
end
local tones = Div{parent=tones_div,x=14,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=tones_div.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
--#endregion
--#region info page
app.new_page(nil, 3)
local info_div = Div{parent=page_div}
TextBox{parent=info_div,x=2,y=1,text="This app provides tools to test alarm sounds by alarm and by tone (1-8)."}
TextBox{parent=info_div,x=2,y=6,text="The system must be idle (all units stopped with no alarms active) for testing to run."}
TextBox{parent=info_div,x=2,y=12,text="Currently, testing will be denied unless you have a Facility Authentication Key set (this will change in the future)."}
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes={alarms_div,tones_div,info_div}}
app.set_root_pane(u_pane)
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = function () app.switcher(1) end },
{ label = " \x0f ", color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(2) end },
{ label = " ? ", color = core.cpair(colors.black, colors.blue), callback = function () app.switcher(3) end }
}
app.set_sidebar(list)
end
return new_view

297
pocket/ui/apps/comps.lua Normal file
View File

@ -0,0 +1,297 @@
--
-- Computer List App
--
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local DEV_TYPE = comms.DEVICE_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local lu_col = style.label_unit_pair
local box_label = cpair(colors.lightGray, colors.gray)
-- new computer list page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.COMPS, frame, nil, true, false)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local ps = db.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- create all page divs
for _ = 1, 4 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 1s it will re-send the query
local function update()
if util.time_ms() - last_update >= 1000 then
db.diag.get_comps()
last_update = util.time_ms()
end
end
-- create indicators for the ID, firmware, and RTT
---@param pfx string
---@param rect Rectangle
local function create_common_indicators(pfx, rect)
local first = TextBox{parent=rect,text="Computer",fg_bg=box_label}
TextBox{parent=rect,text="Firmware",fg_bg=box_label}
TextBox{parent=rect,text="RTT (ms)",fg_bg=box_label}
local y = first.get_y()
local addr = TextBox{parent=rect,x=10,y=y,text="---"}
local fw = TextBox{parent=rect,x=10,y=y+1,text="---"}
local rtt = TextBox{parent=rect,x=10,y=y+2,text="---"}
addr.register(ps, pfx .. "_addr", function (v) addr.set_value(util.strval(v)) end)
fw.register(ps, pfx .. "_fw", function (v) fw.set_value(util.strval(v)) end)
rtt.register(ps, pfx .. "_rtt", function (value)
rtt.set_value(util.strval(value))
if value == "---" then
rtt.recolor(colors.white)
elseif value > const.HIGH_RTT then
rtt.recolor(colors.red)
elseif value > const.WARN_RTT then
rtt.recolor(colors.yellow)
else
rtt.recolor(colors.green)
end
end)
end
--#region main computer page
local m_div = Div{parent=panes[1],x=2,width=main.get_width()-2}
local main_page = app.new_page(nil, 1)
main_page.tasks = { update }
TextBox{parent=m_div,y=1,text="Connected Computers",alignment=ALIGN.CENTER}
local conns = DataIndicator{parent=m_div,y=3,lu_colors=lu_col,label="Total Online",unit="",format="%8d",value=0,commas=true,width=21}
conns.register(ps, "comp_online", conns.update)
local svr_div = Div{parent=m_div,y=4,height=6}
local svr_rect = Rectangle{parent=svr_div,height=6,width=22,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=svr_rect,text="Supervisor"}
TextBox{parent=svr_rect,text="Status",fg_bg=box_label}
TextBox{parent=svr_rect,x=10,y=2,text="Online",fg_bg=cpair(colors.green,colors._INHERIT)}
TextBox{parent=svr_rect,text="Computer",fg_bg=box_label}
TextBox{parent=svr_rect,text="Firmware",fg_bg=box_label}
local svr_addr = TextBox{parent=svr_rect,x=10,y=3,text="?"}
local svr_fw = TextBox{parent=svr_rect,x=10,y=4,text="?"}
svr_addr.register(ps, "comp_svr_addr", function (v) svr_addr.set_value(util.strval(v)) end)
svr_fw.register(ps, "comp_svr_fw", function (v) svr_fw.set_value(util.strval(v)) end)
local crd_div = Div{parent=m_div,y=11,height=7}
local crd_rect = Rectangle{parent=crd_div,height=7,width=21,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=crd_rect,text="Coordinator"}
TextBox{parent=crd_rect,text="Status",fg_bg=box_label}
local crd_online = TextBox{parent=crd_rect,x=10,y=2,width=8,text="Off-line",fg_bg=cpair(colors.red,colors._INHERIT)}
create_common_indicators("comp_crd", crd_rect)
crd_online.register(ps, "comp_crd_online", function (online)
if online then
crd_online.recolor(colors.green)
crd_online.set_value("Online")
else
crd_online.recolor(colors.red)
crd_online.set_value("Off-line")
end
end)
--#endregion
--#region PLC page
local p_div = Div{parent=panes[2],width=main.get_width()}
local plc_page = app.new_page(nil, 2)
plc_page.tasks = { update }
TextBox{parent=p_div,y=1,text="PLC Devices",alignment=ALIGN.CENTER}
local plc_list = ListBox{parent=p_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local plc_elems = {} ---@type graphics_element[]
--#endregion
--#region RTU gateway page
local r_div = Div{parent=panes[3],width=main.get_width()}
local rtu_page = app.new_page(nil, 3)
rtu_page.tasks = { update }
TextBox{parent=r_div,y=1,text="RTU Gateway Devices",alignment=ALIGN.CENTER}
local rtu_list = ListBox{parent=r_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local rtu_elems = {} ---@type graphics_element[]
--#endregion
--#region pocket computer page
local pk_div = Div{parent=panes[4],width=main.get_width()}
local pkt_page = app.new_page(nil, 4)
pkt_page.tasks = { update }
TextBox{parent=pk_div,y=1,text="Pocket Devices",alignment=ALIGN.CENTER}
local pkt_list = ListBox{parent=pk_div,y=3,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local pkt_elems = {} ---@type graphics_element[]
--#endregion
--#region connect/disconnect management
ps.subscribe("comp_connect", function (id)
if id == false then return end
local pfx = "comp_" .. id
local type = ps.get(pfx .. "_type")
if type == DEV_TYPE.PLC then
plc_elems[id] = Div{parent=plc_list,height=7}
local rect = Rectangle{parent=plc_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
local title = TextBox{parent=rect,text="PLC (Unit ?)"}
title.register(ps, pfx .. "_unit", function (unit) title.set_value("PLC (Unit " .. unit .. ")") end)
create_common_indicators(pfx, rect)
elseif type == DEV_TYPE.RTU then
rtu_elems[id] = Div{parent=rtu_list,height=7}
local rect = Rectangle{parent=rtu_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=rect,text="RTU Gateway"}
create_common_indicators(pfx, rect)
elseif type == DEV_TYPE.PKT then
pkt_elems[id] = Div{parent=pkt_list,height=7}
local rect = Rectangle{parent=pkt_elems[id],height=6,x=2,width=20,border=border(1,colors.white,true),thin=true,fg_bg=cpair(colors.white,colors.gray)}
TextBox{parent=rect,text="Pocket Computer"}
create_common_indicators(pfx, rect)
end
end)
ps.subscribe("comp_disconnect", function (id)
if id == false then return end
local type = ps.get("comp_" ..id .. "_type")
if type == DEV_TYPE.PLC then
if plc_elems[id] then plc_elems[id].delete() end
plc_elems[id] = nil
elseif type == DEV_TYPE.RTU then
if rtu_elems[id] then rtu_elems[id].delete() end
rtu_elems[id] = nil
elseif type == DEV_TYPE.PKT then
if pkt_elems[id] then pkt_elems[id].delete() end
pkt_elems[id] = nil
end
end)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " @ ", color = core.cpair(colors.black, colors.blue), callback = main_page.nav_to },
{ label = "PLC", color = core.cpair(colors.black, colors.red), callback = plc_page.nav_to },
{ label = "RTU", color = core.cpair(colors.black, colors.orange), callback = rtu_page.nav_to },
{ label = "PKT", color = core.cpair(colors.black, colors.lightGray), callback = pkt_page.nav_to }
}
app.set_sidebar(list)
-- done, show the app
main_page.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
-- clear the list of connected computers so that connections re-appear on reload of this app
iocontrol.rx.clear_comp_record()
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -1,118 +0,0 @@
--
-- Diagnostic Apps
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local IndicatorLight = require("graphics.elements.indicators.IndicatorLight")
local Checkbox = require("graphics.elements.controls.Checkbox")
local PushButton = require("graphics.elements.controls.PushButton")
local SwitchButton = require("graphics.elements.controls.SwitchButton")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- create diagnostic app pages
---@param root Container parent
local function create_pages(root)
local db = iocontrol.get_db()
------------------------
-- Alarm Testing Page --
------------------------
local alarm_test = Div{parent=root,x=1,y=1}
local alarm_app = db.nav.register_app(APP_ID.ALARMS, alarm_test, nil, true)
local page = alarm_app.new_page(nil, function () end)
page.tasks = { db.diag.tone_test.get_tone_states }
local ttest = db.diag.tone_test
local c_wht_gray = cpair(colors.white, colors.gray)
local c_red_gray = cpair(colors.red, colors.gray)
local c_yel_gray = cpair(colors.yellow, colors.gray)
local c_blue_gray = cpair(colors.blue, colors.gray)
local audio = Div{parent=alarm_test,x=1,y=1}
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",alignment=ALIGN.CENTER}
ttest.ready_warn = TextBox{parent=audio,y=2,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
TextBox{parent=tones,text="Tones",alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local test_btns = {}
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
ttest.tone_buttons = test_btns
local function stop_all_tones()
for i = 1, #test_btns do test_btns[i].set_value(false) end
ttest.stop_tones()
end
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=alarms,text="Alarms (\x13)",alignment=ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
local alarm_btns = {}
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
ttest.alarm_buttons = alarm_btns
local function stop_all_alarms()
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
ttest.stop_alarms()
end
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
TextBox{parent=states,text="States",alignment=ALIGN.CENTER}
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}
local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray}
local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray}
local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray}
local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray}
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
end
return create_pages

View File

@ -1,29 +0,0 @@
--
-- Placeholder App
--
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local APP_ID = pocket.APP_ID
-- create placeholder app page
---@param root Container parent
local function create_pages(root)
local db = iocontrol.get_db()
local main = Div{parent=root,x=1,y=1}
db.nav.register_app(APP_ID.DUMMY, main).new_page(nil, function () end)
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)}
end
return create_pages

View File

@ -9,7 +9,6 @@ local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local docs = require("pocket.ui.docs") local docs = require("pocket.ui.docs")
-- local style = require("pocket.ui.style")
local guide_section = require("pocket.ui.pages.guide_section") local guide_section = require("pocket.ui.pages.guide_section")
@ -31,10 +30,6 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID local APP_ID = pocket.APP_ID
-- local label = style.label
-- local lu_col = style.label_unit_pair
-- local text_fg = style.text_fg
-- new system guide view -- new system guide view
---@param root Container parent ---@param root Container parent
local function new_view(root) local function new_view(root)
@ -47,14 +42,21 @@ local function new_view(root)
local load_div = Div{parent=frame,x=1,y=1} local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1} local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)} WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.cyan,colors._INHERIT)}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
local load_text_1 = TextBox{parent=load_div,y=14,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
local load_text_2 = TextBox{parent=load_div,y=15,text="",alignment=ALIGN.CENTER,fg_bg=cpair(colors.lightGray,colors._INHERIT)}
-- give more detailed information so the user doesn't give up
local function load_text(a, b)
if a then load_text_1.set_value(a) end
load_text_2.set_value(b or "")
end
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}} local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
local btn_fg_bg = cpair(colors.cyan, colors.black) local btn_fg_bg = cpair(colors.cyan, colors.black)
local btn_active = cpair(colors.white, colors.black) local btn_active = cpair(colors.white, colors.black)
local btn_disable = cpair(colors.gray, colors.black)
app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }}) app.set_sidebar({{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home }})
@ -71,7 +73,7 @@ local function new_view(root)
app.set_sidebar(list) app.set_sidebar(list)
page_div = Div{parent=main,y=2} page_div = Div{parent=main,y=2}
local p_width = page_div.get_width() - 2 local p_width = page_div.get_width() - 1
local main_page = app.new_page(nil, 1) local main_page = app.new_page(nil, 1)
local search_page = app.new_page(main_page, 2) local search_page = app.new_page(main_page, 2)
@ -104,6 +106,8 @@ local function new_view(root)
PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to} PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to}
PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to} PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to}
load_text("Search")
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER} TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)} local query_field = TextField{parent=search,x=1,y=3,width=18,fg_bg=cpair(colors.white,colors.gray)}
@ -171,14 +175,29 @@ local function new_view(root)
util.nop() util.nop()
load_text("System Usage")
TextBox{parent=use,y=1,text="System Usage",alignment=ALIGN.CENTER} TextBox{parent=use,y=1,text="System Usage",alignment=ALIGN.CENTER}
PushButton{parent=use,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=use,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
PushButton{parent=use,y=3,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() load_text(false, "Connecting Devices")
PushButton{parent=use,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() local conn_dev_page = guide_section(sect_construct_data, use_page, "Connecting Devs", docs.usage.conn, 110)
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() load_text(false, "Configuring Devices")
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() local config_dev_page = guide_section(sect_construct_data, use_page, "Configuring Devs", docs.usage.config, 350)
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() load_text(false, "Manual Control")
local man_ctrl_page = guide_section(sect_construct_data, use_page, "Manual Control", docs.usage.manual, 100)
load_text(false, "Auto Control")
local auto_ctrl_page = guide_section(sect_construct_data, use_page, "Auto Control", docs.usage.auto, 200)
load_text(false, "Waste Control")
local waste_ctrl_page = guide_section(sect_construct_data, use_page, "Waste Control", docs.usage.waste, 120)
PushButton{parent=use,y=3,text="Connecting Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=conn_dev_page.nav_to}
PushButton{parent=use,text="Configuring Devices >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=config_dev_page.nav_to}
PushButton{parent=use,text="Manual Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=man_ctrl_page.nav_to}
PushButton{parent=use,text="Automatic Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=auto_ctrl_page.nav_to}
PushButton{parent=use,text="Waste Control >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=waste_ctrl_page.nav_to}
load_text("Operator UIs")
TextBox{parent=uis,y=1,text="Operator UIs",alignment=ALIGN.CENTER} TextBox{parent=uis,y=1,text="Operator UIs",alignment=ALIGN.CENTER}
PushButton{parent=uis,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=uis,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
@ -187,51 +206,84 @@ local function new_view(root)
local annunc_div = Div{parent=page_div,x=2} local annunc_div = Div{parent=page_div,x=2}
table.insert(panes, annunc_div) table.insert(panes, annunc_div)
local coord_page = app.new_page(uis_page, #panes + 1)
local coord_div = Div{parent=page_div,x=2}
table.insert(panes, coord_div)
load_text(false, "Alarms")
local alarms_page = guide_section(sect_construct_data, uis_page, "Alarms", docs.alarms, 100) local alarms_page = guide_section(sect_construct_data, uis_page, "Alarms", docs.alarms, 100)
PushButton{parent=uis,y=3,text="Alarms >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=alarms_page.nav_to} PushButton{parent=uis,y=3,text="Alarms >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=alarms_page.nav_to}
PushButton{parent=uis,text="Annunciators >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=annunc_page.nav_to} PushButton{parent=uis,text="Annunciators >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=annunc_page.nav_to}
PushButton{parent=uis,text="Pocket UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=coord_page.nav_to}
PushButton{parent=uis,text="Coordinator UI >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
load_text(false, "Annunciators")
TextBox{parent=annunc_div,y=1,text="Annunciators",alignment=ALIGN.CENTER} TextBox{parent=annunc_div,y=1,text="Annunciators",alignment=ALIGN.CENTER}
PushButton{parent=annunc_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to} PushButton{parent=annunc_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170) local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100) local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170) local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110) PushButton{parent=annunc_div,y=3,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
PushButton{parent=annunc_div,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to} PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
PushButton{parent=annunc_div,text="Unit RCS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rcs_page.nav_to} PushButton{parent=annunc_div,text="Unit RCS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rcs_page.nav_to}
PushButton{parent=annunc_div,text="Facility General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fac_annunc_page.nav_to}
PushButton{parent=annunc_div,text="Waste & Valves >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() load_text(false, "Coordinator UI")
TextBox{parent=coord_div,y=1,text="Coordinator UI",alignment=ALIGN.CENTER}
PushButton{parent=coord_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
load_text(false, "Main Display")
local main_disp_page = guide_section(sect_construct_data, coord_page, "Main Display", docs.c_ui.main, 300)
load_text(false, "Flow Display")
local flow_disp_page = guide_section(sect_construct_data, coord_page, "Flow Display", docs.c_ui.flow, 210)
load_text(false, "Unit Displays")
local unit_disp_page = guide_section(sect_construct_data, coord_page, "Unit Displays", docs.c_ui.unit, 150)
PushButton{parent=coord_div,y=3,text="Main Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_disp_page.nav_to}
PushButton{parent=coord_div,text="Flow Display >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=flow_disp_page.nav_to}
PushButton{parent=coord_div,text="Unit Displays >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_disp_page.nav_to}
load_text("Front Panels")
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER} TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
load_text(false, "Common Items")
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100) local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 180) load_text(false, "Reactor PLC")
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 190)
load_text(false, "RTU Gateway")
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100) local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
load_text(false, "Supervisor")
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160) local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
load_text(false, "Coordinator")
local fp_coordinator_page = guide_section(sect_construct_data, fps_page, "Coordinator", docs.fp.coordinator, 80)
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to} PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to}
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to} PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to}
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to} PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to}
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to} PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to}
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable() PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_coordinator_page.nav_to}
load_text("Glossary")
TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER} TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140) local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140)
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 100) local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 120)
PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to} PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to} PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
load_text("Links")
TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER} TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER}
PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}

View File

@ -32,16 +32,29 @@ local function create_pages(root)
local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}} local root_pane = MultiPane{parent=main,x=1,y=1,panes={conn_sv_wait,conn_api_wait,main_pane}}
root_pane.register(db.ps, "link_state", function (state) local function update()
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then local state = db.ps.get("link_state")
if state == LINK_STATE.UNLINKED then
root_pane.set_value(1) root_pane.set_value(1)
elseif state == LINK_STATE.API_LINK_ONLY then
if not db.loader_require.sv then
root_pane.set_value(3)
db.nav.on_loader_connected()
else root_pane.set_value(1) end
elseif state == LINK_STATE.SV_LINK_ONLY then elseif state == LINK_STATE.SV_LINK_ONLY then
root_pane.set_value(2) if not db.loader_require.api then
root_pane.set_value(3)
db.nav.on_loader_connected()
else root_pane.set_value(2) end
else else
root_pane.set_value(3) root_pane.set_value(3)
db.nav.on_loader_connected() db.nav.on_loader_connected()
end end
end) end
root_pane.register(db.ps, "link_state", update)
root_pane.register(db.ps, "loader_reqs", update)
TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER} TextBox{parent=main_pane,text="Connected!",x=1,y=6,alignment=core.ALIGN.CENTER}
end end

View File

@ -0,0 +1,219 @@
--
-- Radiation Monitor App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local ListBox = require("graphics.elements.ListBox")
local MultiPane = require("graphics.elements.MultiPane")
local Rectangle = require("graphics.elements.Rectangle")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local RadIndicator = require("graphics.elements.indicators.RadIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local border = core.border
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
-- new radiation monitor page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.RADMON, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.yellow,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local f_ps = db.facility.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- create all page divs
for _ = 1, db.facility.num_units + 2 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
local last_update = 0
-- refresh data callback, every 500ms it will re-send the query
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_rad()
last_update = util.time_ms()
end
end
-- create a new radiation monitor list
---@param parent Container
---@param ps psil
local function new_mon_list(parent, ps)
local mon_list = ListBox{parent=parent,y=6,scroll_height=100,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local elem_list = {} ---@type graphics_element[]
mon_list.register(ps, "radiation_monitors", function (data)
local ids = textutils.unserialize(data)
-- delete any disconnected monitors
for id, elem in pairs(elem_list) do
if not util.table_contains(ids, id) then
elem.delete()
elem_list[id] = nil
end
end
-- add newly connected monitors
for _, id in pairs(ids) do
if not elem_list[id] then
elem_list[id] = Div{parent=mon_list,height=5}
local mon_rect = Rectangle{parent=elem_list[id],height=4,x=2,width=20,border=border(1,colors.gray,true),thin=true,fg_bg=cpair(colors.black,colors.lightGray)}
TextBox{parent=mon_rect,text="Env. Detector "..id}
local mon_rad = RadIndicator{parent=mon_rect,x=2,label="",format="%13.3f",lu_colors=cpair(colors.gray,colors.gray),width=18}
mon_rad.register(ps, "radiation@" .. id, mon_rad.update)
end
end
end)
end
--#region unit radiation monitors
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane}
local unit = db.units[i]
local u_ps = unit.unit_ps
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Unit #"..i.." Monitors",alignment=ALIGN.CENTER}
TextBox{parent=u_div,x=2,y=3,text="Max Radiation",fg_bg=label_fg_bg}
local radiation = RadIndicator{parent=u_div,x=2,label="",format="%17.3f",lu_colors=lu_col,width=21}
radiation.register(u_ps, "radiation", radiation.update)
new_mon_list(u_div, u_ps)
end
--#endregion
--#region overview page
local s_pane = panes[db.facility.num_units + 1]
local s_div = Div{parent=s_pane,x=2,width=main.get_width()-2}
local stat_page = app.new_page(nil, db.facility.num_units + 1)
stat_page.tasks = { update }
TextBox{parent=s_div,y=1,text=" Radiation Monitoring",alignment=ALIGN.CENTER}
TextBox{parent=s_div,y=3,text="Max Facility Rad.",fg_bg=label_fg_bg}
local s_f_rad = RadIndicator{parent=s_div,label="",format="%17.3f",lu_colors=lu_col,width=21}
s_f_rad.register(f_ps, "radiation", s_f_rad.update)
for i = 1, db.facility.num_units do
local unit = db.units[i]
local u_ps = unit.unit_ps
s_div.line_break()
TextBox{parent=s_div,text="Max Unit "..i.." Radiation",fg_bg=label_fg_bg}
local s_u_rad = RadIndicator{parent=s_div,label="",format="%17.3f",lu_colors=lu_col,width=21}
s_u_rad.register(u_ps, "radiation", s_u_rad.update)
end
--#endregion
--#region overview page
local f_pane = panes[db.facility.num_units + 2]
local f_div = Div{parent=f_pane,width=main.get_width()}
local fac_page = app.new_page(nil, db.facility.num_units + 2)
fac_page.tasks = { update }
TextBox{parent=f_div,y=1,text="Facility Monitors",alignment=ALIGN.CENTER}
TextBox{parent=f_div,x=2,y=3,text="Max Radiation",fg_bg=label_fg_bg}
local f_rad = RadIndicator{parent=f_div,x=2,label="",format="%17.3f",lu_colors=lu_col,width=21}
f_rad.register(f_ps, "radiation", f_rad.update)
new_mon_list(f_div, f_ps)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = " \x1e ", color = core.cpair(colors.black, colors.blue), callback = stat_page.nav_to },
{ label = "FAC", color = core.cpair(colors.black, colors.yellow), callback = fac_page.nav_to }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
end
app.set_sidebar(list)
-- done, show the app
stat_page.nav_to()
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -312,8 +312,6 @@ local function new_view(root)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update) c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update) c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
-- rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Mismatches",alignment=ALIGN.CENTER,fg_bg=label}
local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s} local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s}
local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s} local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s}
local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s} local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s}
@ -323,7 +321,6 @@ local function new_view(root)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update) c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
rcs_div.line_break() rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Aggregate Checks",alignment=ALIGN.CENTER,fg_bg=label}
if unit.num_boilers > 0 then if unit.num_boilers > 0 then
local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s} local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s}

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

@ -1,3 +1,7 @@
--
-- All the text documentation used in the Guide app is defined in this file.
--
local const = require("scada-common.constants") local const = require("scada-common.constants")
local docs = {} local docs = {}
@ -7,7 +11,9 @@ local DOC_ITEM_TYPE = {
SECTION = 1, SECTION = 1,
SUBSECTION = 2, SUBSECTION = 2,
TEXT = 3, TEXT = 3,
LIST = 4 NOTE = 4,
TIP = 5,
LIST = 6
} }
---@enum DOC_LIST_TYPE ---@enum DOC_LIST_TYPE
@ -51,6 +57,18 @@ local function text(body)
table.insert(target, item) table.insert(target, item)
end end
local function note(body)
---@class pocket_doc_note
local item = { type = DOC_ITEM_TYPE.NOTE, text = body }
table.insert(target, item)
end
local function tip(body)
---@class pocket_doc_tip
local item = { type = DOC_ITEM_TYPE.TIP, text = body }
table.insert(target, item)
end
---@param type DOC_LIST_TYPE ---@param type DOC_LIST_TYPE
---@param items table ---@param items table
---@param colors table|nil colors for indicators or nil for normal lists ---@param colors table|nil colors for indicators or nil for normal lists
@ -60,14 +78,140 @@ local function list(type, items, colors)
table.insert(target, list_def) table.insert(target, list_def)
end end
-- important to note in the future: The PLC should always be in a chunk with the reactor to ensure it can protect it on chunk load if you do not keep it all chunk loaded --#region System Usage
docs.usage = {
conn = {}, config = {}, manual = {}, auto = {}, waste = {}
}
target = docs.usage.conn
sect("Overview")
tip("For the best setup experience, see the Wiki on GitHub or the YouTube channel! This app does not contain all information.")
text("Mekanism devices are connected to ComputerCraft computers that form the SCADA control system.")
sect("Mekanism Conns")
text("Multiblocks and single block devices are both connected directly to a computer by touching it or via wired modems.")
doc("usage_conn_mb", "Multiblocks", "For multiblocks, a logic adapter is used if it exists for that multiblock, otherwise a valve or port block is used.")
text("A wired modem is only connected to the block when you right click it and it gets a red border and you see a message in the chat with the peripheral name.")
tip("Do not connect all peripherals in the system on the same network cable, since Reactor PLCs will grab the first reactor they find and you may accidentally duplicate RTUs.")
sect("Computer Conns")
tip("It helps to be familiar with how ComputerCraft manages peripherals before using this system, though it is not necessary.")
doc("usage_conn_network", "Network", "All computers in the system communicate with each other via wireless or ender modems. Ender modems are preferred due to the unlimited range.")
text("Five different network channels are used and must have the same value for each name across all devices.")
text("For example, the supervisor channel SVR_CHANNEL must be set to the same channel for all devices in your system. Two different named channels should not share the same value (such as SVR_CHANNEL vs CRD_CHANNEL).")
doc("usage_conn_peri", "Peripherals", "ComputerCraft peripherals like monitors and speakers need to touch the computer or be connected via wired modems.")
target = docs.usage.config
sect("Overview")
tip("For the best setup experience, see the Wiki on GitHub or the YouTube channel! This app does not contain all information.")
text("All devices have a configurator program you can launch by running the 'configure' command.")
sect("Networking")
doc("usage_cfg_id", "Computer ID", "A computer ID must NEVER be the identical between devices, which can only happen if you duplicate a computer (such as if you middle-click on it and place it again in creative mode).")
doc("usage_cfg_chan", "Channels", "Channels are used for the computer to computer communication, described in the connection guide section. Channels with the same name must have the same value across all devices in your system and channels with different names cannot overlap.")
doc("usage_cfg_to", "Conn Timeout", "After this period of time the device will close the connection assuming the other device is unresponsive.")
doc("usage_cfg_tr", "Trusted Range", "Devices further than this block distance away will have any network traffic rejected by this device.")
doc("usage_cfg_auth", "Authentication", "To provide a level of security, you can enable facility-wide authentication by setting keys, which must be the same (and set) on all your devices. This adds computation time to each network transmission so you should only do this if you need it on multiplayer.")
sect("Logging")
text("Logs are automatically saved to a log.txt file in the root of the computer. You can change the path to it, if it contains verbose debug messages, and if it is appended to or overwritten each time the program runs.")
text("If you intend to be able to share logs, you should leave it to append.")
doc("usage_cfg_log_upload", "Sharing Logs", "To share logs, you would run 'pastebin put log.txt' where your log file is then share the code.")
sect("Reactor PLC")
text("The Reactor PLC must be connected to a single fission reactor that it will manage. Use the configurator to choose if you would like it to operate as networked or not.")
tip("The Reactor PLC should always be in a chunk with the reactor to ensure it can protect it on server start and/or chunk load.")
doc("usage_cfg_plc_nonet", "Non-Networked", "This lets you use this device as an advanced standalone safety system rather than a basic redstone breaker for easier safety protection.")
doc("usage_cfg_plc_net", "Networked", "This is the most commonly used mode. The Reactor PLC will require a connection to the Supervisor to operate and will allow usage through that for more advanced functionality.")
doc("usage_cfg_plc_unit", "Unit ID", "When networked, you can set any unit ID ranging from 1 to 4. Multiple Reactor PLCs cannot share the same unit ID.")
sect("RTU Gateway")
text("The RTU Gateway allows connecting multiple RTU interfaces to the SCADA system. These interfaces may be external peripherals or redstone.")
text("All devices except for fission reactors must be connected via an RTU Gateway.")
sect("Supervisor")
text("The Supervisor configuration is core to the entire system. If you change things about the system, such as the cooling devices or reactor count, it must be updated here.")
text("This configuration contains many settings that are detailed better in the configurator so they will not be covered here.")
doc("usage_cfg_sv_tanks", "Dynamic Tanks", "Dynamic tanks can be used to provide emergency coolant (and/or auxiliary coolant) to the system. Many layouts are supported by using a mix of facility tanks (connect to 1+ units) and unit tanks (connect to only one unit).")
doc("usage_cfg_sv_aux", "Auxiliary Coolant", "This coolant is enabled at the start of reactors to prevent water levels from dropping in the reactor or boiler while the turbine ramps up. This can be connected to a dynamic tank, a sink, or any other water supply.")
sect("Coordinator")
text("The Coordinator configuration is mainly focused around setting up your displays. This is best to do last after everything else. See the wiki on the GitHub for details on monitor sizing.")
tip("When changing the unit count on the Supervisor, you must also update it on the Coordinator.")
doc("usage_cfg_crd_main", "Main Monitor", "The main monitor contains the main interface and overview. It is always 8 block wide with varying height depending on how many units you have.")
doc("usage_cfg_crd_flow", "Flow Monitor", "The flow monitor contains the waste and coolant flow diagram. It is always 8 block wide with varying height depending on how many units you have.")
doc("usage_cfg_crd_unit", "Unit Monitor", "You need one unit monitor per reactor, and it is always a 4x4 monitor.")
text("Monitors can be connected by direct contact or via wired modems.")
text("Various unit and color options are available to customize the display to your liking. Using energy scales other than RF can impact the precision of your power-related auto control setpoints as RF is always used internally.")
sect("Pocket")
text("You're already here, so not much to mention!")
sect("Self-Check")
text("Most application configurators provide a self-check function that will check the validity of your configuration and the network connection. You should run this if you are having issues with that device.")
sect("Config Changes")
text("When an update adds or removes or otherwise modifies configuration requirements, you will be warned that you need to re-configure. You will not lose any prior data as updates will preserve configurations, you just need to step through the instructions again to add or change any new data.")
target = docs.usage.manual
sect("Overview")
text("Manual reactor control still includes safety checks and monitoring, but the burn rate is not automatically controlled.")
text("A unit is under manual control when the AUTO CTRL option Manual is selected on the unit display.")
note("Specific UIs will not be discussed here. If you need help with the UI, refer to Operator UIs > Coordinator UI > Unit Displays.")
sect("Manual Control")
text("The unit display on the Coordinator is used to run manual control. You may also start/stop and set the burn rate via the Mekanism UI on the Fission Reactor.")
tip("If some controls are grayed out on the unit display, that operation isn't currently available, such as due to the reactor being already started or being under auto control.")
text("Manual control is started by the START button and runs at the commanded burn rate next to it, which can be modified before starting or after having started by selecting a value then pressing SET.")
text("The reactor can be stopped via SCRAM, then the RPS needs to be reset via RESET.")
target = docs.usage.auto
sect("Overview")
text("A main feature of this system is automatic reactor control that supports various managed control modes.")
tip("You should first review the Main Display and Unit Display documentation under Operator UIs > Coordinator before proceeding if you are not familiar with the interfaces.")
sect("Configuration")
note("Configurations cannot be modified while auto control is active.")
doc("usage_auto_assign", "Unit Assignments", "Auto control only applies to units set to a mode other than Manual. To prefer certain units or only use the minimum number necessary, priority groups are used to split up the required burn rate.")
text("Primary units will be used first, followed by secondary, etc. If multiple are assigned to a group, burn rate will be assigned evenly between them.")
text("The next priority group will only be used once the previous one cannot keep up with the total required burn rate for auto control at that moment.")
doc("usage_auto_setpoints", "Setpoints", "Three setpoint spinner inputs are available for the three setpoint-based auto control modes. The system will do its best to meet the requested value, with the current value listed below the input.")
doc("usage_auto_limits", "Unit Limits", "Each unit can be limited to a maximum auto control burn rate to prevent exceeding any safe levels that you know of.")
doc("usage_auto_states", "Unit States", "Any assigned units must be shown as Ready and not Degraded to use auto control. See Operator UIs > Coordinator > Main Display for more.")
sect("Operation Modes")
text("Four auto control modes are available that function based on configurations set on the main display. All modes except Monitored Max Burn will try to only use the primary group until it can't keep up, then the secondary, etc.")
note("No units will be set to a burn rate higher than their limit.")
doc("usage_op_mon_max", "Monitored Max Burn", "This mode runs all units assigned to auto control at their unit limit burn rate regardless of priority group.")
doc("usage_op_com_rate", "Combined Burn Rate", "Assigned units will be commanded to meet the Burn Target setpoint.")
doc("usage_op_chg_level", "Charge Level", "Assigned units will be commanded to bring the induction matrix up to the requested Charge Target.")
doc("usage_op_gen_rate", "Generation Rate", "Assigned units will be commanded to maintain the requested Generation Target.")
note("The rate used is the input rate into the induction matrix, so using other power generation sources may disrupt this control mode.")
sect("Start and Stop")
text("A text box is used to indicate the system status. It will also provide information of why the system has paused control or failed to start.")
text("You cannot start auto control until all assigned units have all their devices connected and functional and the reactor's RPS is not tripped.")
doc("usage_op_save", "SAVE", "SAVE will save the configuration without starting control.")
doc("usage_op_start", "START", "START will attempt to start auto control, which includes first saving the configuration.")
doc("usage_op_stop", "STOP", "STOP will stop all reactors assigned to automatic control.")
target = docs.usage.waste
sect("Overview")
text("When 'valves' are connected for routing waste, this system can manage which waste product(s) are made. The flow monitor shows the diagram of how valves are meant to be connected.")
text("There are three waste products, listed below with the colors generally associated with them.")
list(DOC_LIST_TYPE.LED, { "Pu - Plutonium", "Po - Polonium", "AM - Antimatter" }, { colors.cyan, colors.green, colors.purple })
note("The Po and Pu colors are swapped in older versions of Mekanism.")
sect("Unit Waste")
text("Units can be set to specific waste products via buttons at the bottom right of a unit display.")
note("Refer to Operator UIs > Coordinator UI > Unit Displays for details.")
text("If 'Auto' is selected instead of a waste product, that unit's waste will be processed per the facility waste control.")
sect("Facility Waste")
text("Facility waste control adds additional functionality to waste processing through automatic control.")
text("The waste control interface on the main display lets you set a target waste type along with options that can change that based on circumstances.")
note("Refer to Operator UIs > Coordinator UI > Main Display for information on the display and control interface.")
doc("usage_waste_fallback", "Pu Fallback", "This option switches facility waste control to plutonium when the SNAs cannot keep up, such as at night.")
doc("usage_waste_sps_lc", "Low Charge SPS", "This option prevents the facility waste control from stopping antimatter production at low induction matrix charge (< 10%, resumes after reaching 15%).")
text("With that option enabled, antimatter production will continue. With it disabled, it will switch to polonium if set to antimatter while charge is low.")
note("Pu Fallback takes priority and will switch to plutonium when appropriate regardless of the Low Charge SPS setting.")
--#endregion
--#region Operator UIs
--#region Alarms
docs.alarms = {} docs.alarms = {}
target = docs.alarms target = docs.alarms
doc("ContainmentBreach", "Containment Breach", "Reactor disconnected or indicated unformed while being at or above 100% damage; explosion assumed.") doc("ContainmentBreach", "Containment Breach", "Reactor disconnected or indicated unformed while being at or above 100% damage; explosion assumed.")
doc("ContainmentRadiation", "Containment Radiation", "Environment detector(s) assigned to the unit have observed high levels of radiation.") doc("ContainmentRadiation", "Containment Radiation", "Environment detector(s) assigned to the unit have observed high levels of radiation.")
doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the supervisor.") doc("ReactorLost", "Reactor Lost", "Reactor PLC has stopped communicating with the Supervisor.")
doc("CriticalDamage", "Damage Critical", "Reactor damage has reached or exceeded 100%, so it will explode at any moment.") doc("CriticalDamage", "Damage Critical", "Reactor damage has reached or exceeded 100%, so it will explode at any moment.")
doc("ReactorDamage", "Reactor Damage", "Reactor temperature causing increasing damage to the reactor casing.") doc("ReactorDamage", "Reactor Damage", "Reactor temperature causing increasing damage to the reactor casing.")
doc("ReactorOverTemp", "Reactor Over Temp", "Reactor temperature is at or above maximum safe temperature, so it is now taking damage.") doc("ReactorOverTemp", "Reactor Over Temp", "Reactor temperature is at or above maximum safe temperature, so it is now taking damage.")
@ -78,6 +222,10 @@ doc("RPSTransient", "RPS Transient", "Reactor protection system was activated.")
doc("RCSTransient", "RCS Transient", "Something is wrong with the reactor coolant system, check RCS indicators for details.") doc("RCSTransient", "RCS Transient", "Something is wrong with the reactor coolant system, check RCS indicators for details.")
doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due to having full energy storage. This will prevent cooling, so it needs to be resolved before using that unit.") doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due to having full energy storage. This will prevent cooling, so it needs to be resolved before using that unit.")
--#endregion
--#region Annunciators
docs.annunc = { docs.annunc = {
unit = { unit = {
main_section = {}, rps_section = {}, rcs_section = {} main_section = {}, rps_section = {}, rcs_section = {}
@ -89,8 +237,8 @@ docs.annunc = {
target = docs.annunc.unit.main_section target = docs.annunc.unit.main_section
sect("Unit Status") sect("Unit Status")
doc("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.") doc("PLCOnline", "PLC Online", "Indicates if the fission Reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.") doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the Supervisor has stopped receiving data or a screen has frozen.")
doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.") doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.")
doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.") doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.")
sect("Safety Status") sect("Safety Status")
@ -98,7 +246,7 @@ doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is hol
doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.") doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.")
doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.") doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.")
doc("RadiationWarning", "Radiation Warning", "On if radiation levels are above normal. There is likely a leak somewhere, so that should be identified and fixed. Hazmat suit recommended.") doc("RadiationWarning", "Radiation Warning", "On if radiation levels are above normal. There is likely a leak somewhere, so that should be identified and fixed. Hazmat suit recommended.")
doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekansim. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.") doc("RCPTrip", "RCP Trip", "Reactor coolant pump tripped. This is a technical concept not directly mapping to Mekanism. Here, it indicates if there is either high heated coolant or low cooled coolant that caused an RPS trip. Check the coolant system if this occurs.")
doc("RCSFlowLow", "RCS Flow Low", "Indicates if the reactor coolant system flow is low. This is observed when the cooled coolant level in the reactor is dropping. This can occur while a turbine spins up, but if it persists, check that the cooling system is operating properly. This can occur with smaller boilers or when using pipes and not having enough.") doc("RCSFlowLow", "RCS Flow Low", "Indicates if the reactor coolant system flow is low. This is observed when the cooled coolant level in the reactor is dropping. This can occur while a turbine spins up, but if it persists, check that the cooling system is operating properly. This can occur with smaller boilers or when using pipes and not having enough.")
doc("CoolantLevelLow", "Coolant Level Low", "On if the reactor coolant level is lower than it should be. Check the coolant system.") doc("CoolantLevelLow", "Coolant Level Low", "On if the reactor coolant level is lower than it should be. Check the coolant system.")
doc("ReactorTempHigh", "Reactor Temp. High", "On if the reactor temperature is above expected maximum operating temperature. This is not yet damaging, but should be attended to. Check coolant system.") doc("ReactorTempHigh", "Reactor Temp. High", "On if the reactor temperature is above expected maximum operating temperature. This is not yet damaging, but should be attended to. Check coolant system.")
@ -118,7 +266,7 @@ doc("high_temp", "Temperature High", "Indicates if the RPS tripped due to reachi
doc("low_cool", "Coolant Level Low Low", "Indicates if the RPS tripped due to very low coolant levels that result in the temperature uncontrollably rising. Ensure that the cooling system can provide sufficient cooled coolant flow.") doc("low_cool", "Coolant Level Low Low", "Indicates if the RPS tripped due to very low coolant levels that result in the temperature uncontrollably rising. Ensure that the cooling system can provide sufficient cooled coolant flow.")
doc("no_fuel", "No Fuel", "Indicates if the RPS tripped due to no fuel being available. Check fuel input.") doc("no_fuel", "No Fuel", "Indicates if the RPS tripped due to no fuel being available. Check fuel input.")
doc("fault", "PPM Fault", "Indicates if the RPS tripped due to a peripheral access fault. Something went wrong interfacing with the reactor, try restarting the PLC.") doc("fault", "PPM Fault", "Indicates if the RPS tripped due to a peripheral access fault. Something went wrong interfacing with the reactor, try restarting the PLC.")
doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and supervisor remain chunk loaded.") doc("timeout", "Connection Timeout", "Indicates if the RPS tripped due to losing connection with the supervisory computer. Check that your PLC and Supervisor remain chunk loaded.")
doc("sys_fail", "System Failure", "Indicates if the RPS tripped due to the reactor not being formed. Ensure that the multi-block is formed.") doc("sys_fail", "System Failure", "Indicates if the RPS tripped due to the reactor not being formed. Ensure that the multi-block is formed.")
target = docs.annunc.unit.rcs_section target = docs.annunc.unit.rcs_section
@ -130,7 +278,7 @@ doc("SteamFeedMismatch", "Steam Feed Mismatch", "There is an above tolerance dif
doc("MaxWaterReturnFeed", "Max Water Return Feed", "The turbines are condensing the max rate of water that they can per the structure build. If water return is insufficient, add more saturating condensers to your turbine(s).") doc("MaxWaterReturnFeed", "Max Water Return Feed", "The turbines are condensing the max rate of water that they can per the structure build. If water return is insufficient, add more saturating condensers to your turbine(s).")
doc("WaterLevelLow", "Water Level Low", "The water level in the boiler is low. A larger boiler water tank may help, or you can feed additional water into the boiler from elsewhere.") doc("WaterLevelLow", "Water Level Low", "The water level in the boiler is low. A larger boiler water tank may help, or you can feed additional water into the boiler from elsewhere.")
doc("HeatingRateLow", "Heating Rate Low", "The boiler is not hot enough to boil water, but it is receiving heated coolant. This is almost never a safety concern.") doc("HeatingRateLow", "Heating Rate Low", "The boiler is not hot enough to boil water, but it is receiving heated coolant. This is almost never a safety concern.")
doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.") doc("SteamDumpOpen", "Steam Relief Valve Open", "This turns yellow if the turbine is set to dumping excess and red if it is set to dumping [all]. 'Relief Valve' in this case is that setting allowing the venting of steam. You should never have this set to dumping [all]. Emergency coolant activation from the Supervisor will automatically set it to dumping excess to ensure there is no backup of steam as water is added.")
doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity, but not tripped. You may need more turbines if they can't keep up.") doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity, but not tripped. You may need more turbines if they can't keep up.")
doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.") doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.")
doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.") doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.")
@ -154,28 +302,144 @@ doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to cri
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.") doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
doc("as_gen_fault", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.") doc("as_gen_fault", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
docs.fp = { --#endregion
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}
--#region Coordinator UI
docs.c_ui = {
main = {}, flow = {}, unit = {}
} }
--comp id "This must never be the identical between devices, and that can only happen if you duplicate a computer (such as middle-click on it and place it elsewhere in creative mode)." target = docs.c_ui.main
sect("Facility Diagram")
text("The facility overview diagram is made up of unit diagrams showing the reactor, boiler(s) if present, and turbine(s). This includes values of various key statistics such as temperatures along with bars showing the fill percentage of the tanks in each multiblock.")
text("Boilers are shown under the reactor, listed in order of index (#1 then #2 below). Turbines are shown to the right, also listed in order of index (indexes are per unit and set in the RTU Gateway configuration).")
text("Pipe connections are visualized with color-coded lines, which are primarily to indicate connections, as not all facilities may use pipes.")
note("If a component you have is not showing up, ensure the Supervisor is configured for your actual cooling configuration.")
sect("Facility Status")
note("The annunciator here is described in Operator UIs > Annunciators.")
doc("ui_fac_scram", "FAC SCRAM", "This SCRAMs all units in the facility.")
doc("ui_fac_ack", "ACK \x13", "This acknowledges (mutes) all alarms for all units in the facility.")
doc("ui_fac_rad", "Radiation", "The facility radiation, which is the current maximum of all connected facility radiation monitors (excludes unit monitors).")
doc("ui_fac_linked", "Linked RTUs", "The number of RTU Gateways connected.")
sect("Automatic Control")
text("This interface is used for managing automatic facility control, which only applies to units set via the unit display to be under auto control. This includes setpoints, status, configuration, and control.")
doc("ui_fac_auto_bt", "Burn Target", "When set to Combined Burn Rate mode, assigned units will ramp up to meet this combined target.")
doc("ui_fac_auto_ct", "Charge Target", "When set to Charge Level mode, assigned units will run to reach and maintain this induction matrix charge level.")
doc("ui_fac_auto_gt", "Gen. Target", "When set to Generation Rate mode, assigned units will run to reach and maintain this continuous power output, using the induction matrix input rate.")
doc("ui_fac_save", "SAVE", "This saves your configuration without starting control.")
doc("ui_fac_start", "START", "This starts the configured automatic control.")
tip("START also includes the SAVE operation.")
doc("ui_fac_stop", "STOP", "This terminates automatic control, stopping assigned units.")
text("There are four automatic control modes, detailed further in System Usage > Automatic Control")
doc("ui_fac_auto_mmb", "Monitored Max Burn", "This runs all assigned units at the maximum configured rate.")
doc("ui_fac_auto_cbr", "Combined Burn Rate", "This runs assigned units to meet the target combined rate.")
doc("ui_fac_auto_cl", "Charge Level", "This runs assigned units to maintain an induction matrix charge level.")
doc("ui_fac_auto_gr", "Generation Rate", "This runs assigned units to meet a target induction matrix power input rate.")
doc("ui_fac_auto_lim", "Unit Limit", "Each unit can have a limit set that auto control will never exceed.")
doc("ui_fac_unit_ready", "Unit Status Ready", "A unit is only ready for auto control if all multiblocks are formed, online with data received, and there is no RPS trip.")
doc("ui_fac_unit_degraded", "Unit Status Degraded", "A unit is degraded if the reactor, boiler(s), and/or turbine(s) are faulted or not connected.")
sect("Waste Control")
text("Above unit statuses are the unit waste statuses, showing which are set to the auto waste mode and the actual current waste production of that unit.")
text("The facility automatic waste control interface is surrounded by a brown border and lets you configure that system, starting with the requested waste product.")
doc("ui_fac_waste_pu_fall_act", "Fallback Active", "When the system is falling back to plutonium production while SNAs cannot keep up.")
doc("ui_fac_waste_sps_lc_act", "SPS Disabled LC", "When the system is falling back to polonium production to prevent draining all power with the SPS while the induction matrix charge has dropped below 10% and not yet reached 15%.")
doc("ui_fac_waste_pu_fall", "Pu Fallback", "Switch from Po or Antimatter when the SNAs can't keep up (like at night).")
doc("ui_fac_waste_sps_lc", "Low Charge SPS", "Continue running antimatter production even at low induction matrix charge levels (<10%).")
sect("Induction Matrix")
text("The induction matrix statistics are shown at the bottom right, including fill bars for the FILL, I (input rate), and O (output rate).")
text("Averages are computed by the system while other data is directly from the device.")
doc("ui_fac_im_charge", "Charging", "Charge is increasing (more input than output).")
doc("ui_fac_im_charge", "Discharging", "Charge is draining (more output than input).")
doc("ui_fac_im_charge", "Max I/O Rate", "The induction providers are at their maximum rate.")
doc("ui_fac_eta", "ETA", "The ETA is based off a longer average so it may take a minute to stabilize, but will give a rough estimate of time to charge/discharge.")
target = docs.c_ui.flow
sect("Flow Diagram")
text("The coolant and waste flow monitor is one large P&ID (process and instrumentation diagram) showing an overview of those flows.")
text("Color-coded pipes are used to show the connections, and valve symbols \x10\x11 are used to show valves (redstone controlled pipes).")
doc("ui_flow_rates", "Flow Rates", "Flow rates are always shown below their respective pipes and sourced from devices when possible. The waste flow is based on the reactor burn rate, then everything downstream of the SNAs are based on the SNA production rate.")
doc("ui_flow_valves", "Standard Valves", "Valve naming (PV00-XX) is based on P&ID naming conventions. These count up across the whole facility, and use tags at the end to add clarity.")
note("The indicator next to the label turns on when the associated redstone RTU is connected.")
list(DOC_LIST_TYPE.BULLET, { "PU: Plutonium", "PO: Polonium", "PL: Po Pellets", "AM: Antimatter", "EMC: Emer. Coolant", "AUX: Aux. Coolant" })
doc("ui_flow_valve_open", "OPEN", "This indicates if the respective valve is commanded open.")
doc("ui_flow_prv", "PRVs", "Pressure Relief Valves (PRVs) are used to show the turbine steam dumping states of each turbine.")
list(DOC_LIST_TYPE.LED, { "Not Dumping", "Dumping Excess", "Dumping" }, { colors.gray, colors.yellow, colors.red })
sect("SNAs")
text("Solar Neutron Activators are shown on the flow diagram as a combined block due to the large variable count supported.")
tip("SNAs consume 10x the waste as they produce in antimatter, so take that into account before connecting too many SNAs.")
doc("ui_flow_sna_act", "ACTIVE", "The SNAs have a non-zero total flow.")
doc("ui_flow_sna_cnt", "CNT", "The count of SNAs assigned to the unit.")
doc("ui_flow_sna_peak_o", "PEAK\x1a", "The combined theoretical peak output the SNAs can achieve under full sunlight.")
doc("ui_flow_sna_max_o", "MAX \x1a", "The current combined maximum output rate of the SNAs (based on current sunlight).")
doc("ui_flow_sna_max_i", "\x1aMAX", "The computed combined maximum input rate (10x the output rate).")
doc("ui_flow_sna_in", "\x1aIN", "The current input rate into the SNAs.")
sect("Dynamic Tanks")
text("Dynamic tanks configured for the system are listed to the left. The title may start with U for unit tanks or F for facility tanks.")
text("The fill information and water level are shown below the status label.")
doc("ui_flow_dyn_fill", "FILL", "If filling is enabled by the tank mode (via Mekanism UI).")
doc("ui_flow_dyn_empty", "EMPTY", "If emptying is enabled by the tank mode (via Mekanism UI).")
sect("SPS")
doc("ui_flow_sps_in", "Input Rate", "The rate of polonium into the SPS.")
doc("ui_flow_sps_prod", "Production Rate", "The rate of antimatter produced by the SPS.")
sect("Statistics")
text("The sum of all unit's waste rate statistics are shown under the SPS block. These are combined current rates, not long-term sums.")
doc("ui_flow_stat_raw", "RAW WASTE", "The combined rate of raw waste generated by the reactors before processing.")
doc("ui_flow_stat_proc", "PROC. WASTE", "The combined rates of different waste product production. Pu is plutonium, Po is polonium, and PoPl is polonium pellets. Antimatter is shown in the SPS block.")
doc("ui_flow_stat_spent", "SPENT WASTE", "The combined rate of spent waste generated after processing.")
sect("Other Blocks")
text("Other blocks, such as CENTRIFUGE, correspond to devices that are not intended to be connected and/or serve as labels.")
target = docs.c_ui.unit
sect("Data Display")
text("The unit monitor contains extensive data information, including annunciator and alarm displays described in the associated sections in the Operator UIs section.")
doc("ui_unit_core", "Core Map", "A core map diagram is shown at the top right, colored by core temperature. The layout is based off of the multiblock dimensions.")
list(DOC_LIST_TYPE.BULLET, { "Gray <= 300\xb0C", "Blue <= 350\xb0C", "Green < 600\xb0C", "Yellow < 100\xb0C", "Orange < 1200\xb0C", "Red < 1300\xb0C", "Pink >= 1300\xb0C" })
text("Internal tanks (fuel, cooled coolant, heated coolant, and waste) are displayed below the core map, labeled F, C, H, and W, respectively.")
doc("ui_unit_rad", "Radiation", "The unit radiation, which is the current maximum of all connected radiation monitors assigned to this unit.")
text("Multiple other data values are shown but should be self-explanatory.")
sect("Controls")
text("A set of buttons and the burn rate input are used for manual reactor control. When in auto mode, unavailable controls are disabled. The burn rate is only applied after SET is pressed.")
doc("ui_unit_start", "START", "This starts the reactor at the requested burn rate.")
doc("ui_unit_scram", "SCRAM", "This SCRAMs the reactor.")
doc("ui_unit_ack", "ACK \x13", "This acknowledges alarms on this unit.")
doc("ui_unit_reset", "RESET", "This resets the RPS for this unit.")
sect("Auto Control")
text("To put this unit under auto control, select an option other than Manual. You must press SET to apply this, but cannot change this while auto control is active. The priorities available are described in System Usage > Automatic Control.")
doc("ui_unit_prio", "Prio. Group", "This displays the unit's auto control priority group.")
doc("ui_unit_ready", "READY", "This indicates if the unit is ready for auto control. A unit is only ready for auto control if all multiblocks are formed, online with data received, and there is no RPS trip.")
doc("ui_unit_standby", "STANDBY", "This indicates if the unit is set to auto control and that is active, but the auto control does not currently need this reactor to run at the moment, so it is idle.")
sect("Waste Processing")
text("The unit's waste output configuration can be set via these buttons. Auto will put this unit under control of the facility waste control, otherwise the system will always command the requested option for this unit.")
--#endregion
--#endregion
--#region Front Panels
docs.fp = {
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}, coordinator = {}
}
target = docs.fp.common target = docs.fp.common
sect("Core Status") sect("Core Status")
doc("fp_status", "STATUS", "This is always lit, except on the Reactor PLC (see Reactor PLC section).") doc("fp_status", "STATUS", "This is always lit, except on the Reactor PLC (see Reactor PLC section).")
doc("fp_heartbeat", "HEARTBEAT", "This alternates between lit and unlit as the main loop on the device runs. If this freezes, something is wrong and the logs will indicate why.") doc("fp_heartbeat", "HEARTBEAT", "This alternates between lit and unlit as the main loop on the device runs. If this freezes, something is wrong and the logs will indicate why.")
sect("Hardware & Network") sect("Hardware & Network")
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the supervisor's connection lists.") doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the Supervisor's connection lists.")
doc("fp_modem", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.") doc("fp_modem", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.")
list(DOC_LIST_TYPE.LED, { "not linked", "linked", "link denied", "bad comms version", "duplicate PLC" }, { colors.gray, colors.green, colors.red, colors.orange, colors.yellow }) list(DOC_LIST_TYPE.LED, { "not linked", "linked", "link denied", "bad comms version", "duplicate PLC" }, { colors.gray, colors.green, colors.red, colors.orange, colors.yellow })
text("You can fix \"bad comms version\" by ensuring all devices are up-to-date, as this indicates a communications protocol version mismatch. Note that yellow is Reactor PLC-specific, indicating duplicate unit IDs in use.") text("You can fix \"bad comms version\" by ensuring all devices are up-to-date, as this indicates a communications protocol version mismatch. Note that yellow is Reactor PLC-specific, indicating duplicate unit IDs in use.")
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the supervisor.") doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the Supervisor.")
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the supervisor and this device do not match. Make sure everything is up-to-date.") doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the Supervisor and this device do not match. Make sure everything is up-to-date.")
sect("Versions") sect("Versions")
doc("fp_fw", "FW", "Firmware application version of this device.") doc("fp_fw", "FW", "Firmware application version of this device.")
doc("fp_nt", "NT", "Network (comms) version this device has. These must match between devices in order for them to connect.") doc("fp_nt", "NT", "Network (comms) version this device has. These must match between devices in order for them to connect.")
target = docs.fp.r_plc target = docs.fp.r_plc
sect("Overview")
text("Documentation for Reactor PLC-specific front panel items are below. Refer to 'Common Items' for the items not covered in this section.")
sect("Core Status") sect("Core Status")
doc("fp_status", "STATUS", "This is green once the PLC is initialized and OK (has all its peripherals) and red if something is wrong, in which case you should refer to the other indicator lights (REACTOR & MODEM).") doc("fp_status", "STATUS", "This is green once the PLC is initialized and OK (has all its peripherals) and red if something is wrong, in which case you should refer to the other indicator lights (REACTOR & MODEM).")
sect("Hardware & Network") sect("Hardware & Network")
@ -194,8 +458,8 @@ doc("fp_emer_cool", "EMER COOLANT", "This is only present if PLC-controlled emer
doc("fp_rps_trip", "RPS TRIP", "Flashes when the RPS has SCRAM'd the reactor due to a safety trip.") doc("fp_rps_trip", "RPS TRIP", "Flashes when the RPS has SCRAM'd the reactor due to a safety trip.")
sect("RPS Conditions") sect("RPS Conditions")
doc("fp_rps_man", "MANUAL", "The RPS was tripped manually (SCRAM by user, not via the Mekanism Reactor UI).") doc("fp_rps_man", "MANUAL", "The RPS was tripped manually (SCRAM by user, not via the Mekanism Reactor UI).")
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the supervisor automatically.") doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the Supervisor automatically.")
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the supervisor connection.") doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the Supervisor connection.")
doc("fp_rps_pflt", "PLC FAULT", "The RPS tripped due to a peripheral error.") doc("fp_rps_pflt", "PLC FAULT", "The RPS tripped due to a peripheral error.")
doc("fp_rps_rflt", "RCT FAULT", "The RPS tripped due to the reactor not being formed.") doc("fp_rps_rflt", "RCT FAULT", "The RPS tripped due to the reactor not being formed.")
doc("fp_rps_temp", "HI DAMAGE", "The RPS tripped due to being >=" .. const.RPS_LIMITS.MAX_DAMAGE_PERCENT .. "% damaged.") doc("fp_rps_temp", "HI DAMAGE", "The RPS tripped due to being >=" .. const.RPS_LIMITS.MAX_DAMAGE_PERCENT .. "% damaged.")
@ -206,6 +470,9 @@ doc("fp_rps_ccool", "LO CCOOLANT", "The RPS tripped due to having low levels of
doc("fp_rps_ccool", "HI HCOOLANT", "The RPS tripped due to having high levels of heated coolant (>" .. (const.RPS_LIMITS.MAX_HEATED_COLLANT_FILL * 100) .. "%).") doc("fp_rps_ccool", "HI HCOOLANT", "The RPS tripped due to having high levels of heated coolant (>" .. (const.RPS_LIMITS.MAX_HEATED_COLLANT_FILL * 100) .. "%).")
target = docs.fp.rtu_gw target = docs.fp.rtu_gw
sect("Overview")
text("Documentation for RTU Gateway-specific front panel items are below. Refer to 'Common Items' for the items not covered in this section.")
doc("fp_rtu_spkr", "SPEAKERS", "This is the count of speaker peripherals connected to this RTU Gateway.")
sect("Co-Routine States") sect("Co-Routine States")
doc("fp_rtu_rt_main", "RT MAIN", "This indicates if the device's main loop co-routine is running.") doc("fp_rtu_rt_main", "RT MAIN", "This indicates if the device's main loop co-routine is running.")
doc("fp_rtu_rt_comms", "RT COMMS", "This indicates if the communications handler co-routine is running.") doc("fp_rtu_rt_comms", "RT COMMS", "This indicates if the communications handler co-routine is running.")
@ -218,30 +485,49 @@ doc("fp_rtu_rt", "Device Assignment", "In each RTU entry row, the device identif
target = docs.fp.supervisor target = docs.fp.supervisor
sect("Round Trip Times") sect("Round Trip Times")
doc("fp_sv_fw", "RTT", "Each connection has a round trip time, or RTT. Since the supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.") doc("fp_sv_rtt", "RTT", "Each connection has a round trip time, or RTT. Since the Supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=300ms", "yellow: <=500ms ", "red: >500ms" }) list(DOC_LIST_TYPE.BULLET, { "green: <=300ms", "yellow: <=500ms ", "red: >500ms" })
sect("SVR Tab") sect("SVR Tab")
text("This tab includes information about the supervisor, covered by 'Common Items'.") text("This tab includes information about the Supervisor, covered by 'Common Items'.")
sect("PLC Tab") sect("PLC Tab")
text("This tab lists the expected PLC connections based on the number of configured units. Status information about each connection is shown when linked.") text("This tab lists the expected PLC connections based on the number of configured units. Status information about each connection is shown when linked.")
doc("fp_sv_link", "LINK", "This indicates if the reactor PLC is linked.") doc("fp_sv_link", "LINK", "This indicates if the Reactor PLC is linked.")
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the reactor PLC, or --- if disconnected.") doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the Reactor PLC, or --- if disconnected.")
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the reactor PLC.") doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the Reactor PLC.")
sect("RTU Tab") sect("RTU Tab")
text("As RTU gateways connect to the supervisor, they will show up here along with some information.") text("As RTU gateways connect to the Supervisor, they will show up here along with some information.")
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU gateway.") doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU Gateway.")
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU gateway (each line on the RTU gateway's front panel).") doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU Gateway (each line on the RTU Gateway's front panel).")
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU gateway.") doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU Gateway.")
sect("PKT Tab") sect("PKT Tab")
text("As pocket computers connect to the supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.") text("As pocket computers connect to the Supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
sect("DEV Tab") sect("DEV Tab")
text("If nothing is connected, this will list all the expected RTU devices that aren't found. This page should be blank if everything is connected and configured correctly. If not, it will list certain types of detectable problems.") text("If nothing is connected, this will list all the expected RTU devices that aren't found. This page should be blank if everything is connected and configured correctly. If not, it will list certain types of detectable problems.")
doc("fp_sv_d_miss", "MISSING", "These items list missing devices, with the details that should be used in the RTU's configuration.") doc("fp_sv_d_miss", "MISSING", "These items list missing devices, with the details that should be used in the RTU's configuration.")
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.") doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the Supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
doc("fp_sv_d_dupe", "DUPLICATE", "If a device tries to connect that is configured the same as another, it will be rejected and show up here. If you try to connect two #1 turbines for a unit, that would fail and one would appear here.") doc("fp_sv_d_dupe", "DUPLICATE", "If a device tries to connect that is configured the same as another, it will be rejected and show up here. If you try to connect two #1 turbines for a unit, that would fail and one would appear here.")
sect("INF Tab") sect("INF Tab")
text("This tab gives information about the other tabs, along with extra details on the DEV tab.") text("This tab gives information about the other tabs, along with extra details on the DEV tab.")
target = docs.fp.coordinator
sect("Round Trip Times")
doc("fp_crd_rtt", "RTT", "Each connection has a round trip time, or RTT. Since the Coordinator updates at a rate of 500ms, RTTs ~500ms - ~1000ms are typical. Higher RTTs indicate lag, which results in performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=1000ms", "yellow: <=1500ms ", "red: >1500ms" })
sect("CRD Tab")
text("This tab includes information about the Coordinator, partially covered by 'Common Items'.")
doc("fp_crd_spkr", "SPEAKER", "This indicates if the speaker is connected.")
doc("fp_crd_rt_main", "RT MAIN", "This indicates that the device's main loop co-routine is running.")
doc("fp_crd_rt_render", "RT RENDER", "This indicates that the Coordinator graphics renderer co-routine is running.")
doc("fp_crd_mon_main", "MAIN MONITOR", "The connection status of the main display monitor.")
doc("fp_crd_mon_flow", "FLOW MONITOR", "The connection status of the coolant and waste flow display monitor.")
doc("fp_crd_mon_unit", "UNIT X MONITOR", "The connection status of the monitor associated with a given unit.")
sect("API Tab")
text("This tab lists connected pocket computers. Refer to the Supervisor PKT tab documentation for details on fields.")
--#endregion
--#region Glossary
docs.glossary = { docs.glossary = {
abbvs = {}, terms = {} abbvs = {}, terms = {}
} }
@ -249,8 +535,7 @@ docs.glossary = {
target = docs.glossary.abbvs target = docs.glossary.abbvs
doc("G_ACK", "ACK", "Alarm ACKnowledge. Pressing this acknowledges that you understand an alarm occurred and would like to stop the audio tone(s).") doc("G_ACK", "ACK", "Alarm ACKnowledge. Pressing this acknowledges that you understand an alarm occurred and would like to stop the audio tone(s).")
doc("G_Auto", "Auto", "Automatic.") doc("G_Auto", "Auto", "Automatic.")
doc("G_CRD", "CRD", "Coordinator. Abbreviation for the coordinator computer.") doc("G_CRD", "CRD", "Coordinator. Abbreviation for the Coordinator computer.")
doc("G_DBG", "DBG", "Debug. Abbreviation for the debugging sessions from pocket computers found on the supervisor's front panel.")
doc("G_FP", "FP", "Front Panel. See Terminology.") doc("G_FP", "FP", "Front Panel. See Terminology.")
doc("G_Hi", "Hi", "High.") doc("G_Hi", "Hi", "High.")
doc("G_Lo", "Lo", "Low.") doc("G_Lo", "Lo", "Low.")
@ -260,7 +545,7 @@ doc("G_PLC", "PLC", "Programmable Logic Controller. A device that not only repor
doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer created for this project that prevents peripheral calls from crashing applications.") doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer created for this project that prevents peripheral calls from crashing applications.")
doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.") doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.")
doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).") doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.") doc("G_RPS", "RPS", "Reactor Protection System. A component of the Reactor PLC responsible for keeping the reactor safe.")
doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.") doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.")
doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.") doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.")
doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.") doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.")
@ -269,6 +554,8 @@ doc("G_UI", "UI", "User Interface.")
target = docs.glossary.terms target = docs.glossary.terms
doc("G_AssignedUnit", "Assigned Unit", "A unit that is assigned to an automatic control group (not assigned to Manual).") doc("G_AssignedUnit", "Assigned Unit", "A unit that is assigned to an automatic control group (not assigned to Manual).")
doc("G_AuxCoolant", "Auxiliary Coolant", "A separate water input to the reactor or boiler to supplement return water from a turbine during initial ramp-up.")
doc("G_EmerCoolant", "Emergency Coolant", "A dynamic tank or other water supply used when a reactor or boiler does not have enough water to stop a runaway reactor overheat.")
doc("G_Fault", "Fault", "Something has gone wrong and/or failed to function.") doc("G_Fault", "Fault", "Something has gone wrong and/or failed to function.")
doc("G_FrontPanel", "Front Panel", "A basic interface on the front of a device for viewing and sometimes modifying its state. This is what you see when looking at a computer running one of the SCADA applications.") doc("G_FrontPanel", "Front Panel", "A basic interface on the front of a device for viewing and sometimes modifying its state. This is what you see when looking at a computer running one of the SCADA applications.")
doc("G_HighHigh", "High High", "Very High.") doc("G_HighHigh", "High High", "Very High.")
@ -282,4 +569,6 @@ doc("G_Tripped", "Tripped", "An alarm condition has been met, and is still met."
doc("G_Tripping", "Tripping", "Alarm condition(s) is/are met, but has/have not reached the minimum time before the condition(s) is/are deemed a problem.") doc("G_Tripping", "Tripping", "Alarm condition(s) is/are met, but has/have not reached the minimum time before the condition(s) is/are deemed a problem.")
doc("G_TurbineTrip", "Turbine Trip", "The turbine stopped, which prevents heated coolant from being cooled. In Mekanism, this would occur when a turbine cannot generate any more energy due to filling its buffer and having no output with any remaining energy capacity.") doc("G_TurbineTrip", "Turbine Trip", "The turbine stopped, which prevents heated coolant from being cooled. In Mekanism, this would occur when a turbine cannot generate any more energy due to filling its buffer and having no output with any remaining energy capacity.")
--#endregion
return docs return docs

View File

@ -7,14 +7,15 @@ local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local about_app = require("pocket.ui.apps.about")
local alarm_app = require("pocket.ui.apps.alarm")
local comps_app = require("pocket.ui.apps.comps")
local control_app = require("pocket.ui.apps.control") local control_app = require("pocket.ui.apps.control")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local facil_app = require("pocket.ui.apps.facility") local facil_app = require("pocket.ui.apps.facility")
local guide_app = require("pocket.ui.apps.guide") local guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader") local loader_app = require("pocket.ui.apps.loader")
local process_app = require("pocket.ui.apps.process") local process_app = require("pocket.ui.apps.process")
local sys_apps = require("pocket.ui.apps.sys_apps") local rad_app = require("pocket.ui.apps.radiation")
local unit_app = require("pocket.ui.apps.unit") local unit_app = require("pocket.ui.apps.unit")
local waste_app = require("pocket.ui.apps.waste") local waste_app = require("pocket.ui.apps.waste")
@ -71,10 +72,11 @@ local function init(main)
process_app(page_div) process_app(page_div)
waste_app(page_div) waste_app(page_div)
guide_app(page_div) guide_app(page_div)
rad_app(page_div)
loader_app(page_div) loader_app(page_div)
sys_apps(page_div) about_app(page_div)
diag_apps(page_div) alarm_app(page_div)
dummy_app(page_div) comps_app(page_div)
-- verify all apps were created -- verify all apps were created
assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered") assert(util.table_len(db.nav.get_containers()) == APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")

View File

@ -1,3 +1,7 @@
--
-- Dynamic Tank View
--
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")

View File

@ -1,3 +1,7 @@
--
-- Induction Matrix View
--
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")

View File

@ -1,3 +1,7 @@
--
-- SPS View
--
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style") local style = require("pocket.ui.style")

View File

@ -1,3 +1,7 @@
--
-- A Guide App Subsection
--
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -34,13 +38,13 @@ return function (data, base_page, title, items, scroll_height)
local section_div = Div{parent=page_div,x=2} local section_div = Div{parent=page_div,x=2}
table.insert(panes, section_div) table.insert(panes, section_div)
TextBox{parent=section_div,y=1,text=title,alignment=ALIGN.CENTER} TextBox{parent=section_div,y=1,text=title,alignment=ALIGN.CENTER}
PushButton{parent=section_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=base_page.nav_to} PushButton{parent=section_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=base_page.nav_to}
local view_page = app.new_page(section_page, #panes + 1) local view_page = app.new_page(section_page, #panes + 1)
local section_view_div = Div{parent=page_div,x=2} local section_view_div = Div{parent=page_div,x=2}
table.insert(panes, section_view_div) table.insert(panes, section_view_div)
TextBox{parent=section_view_div,y=1,text=title,alignment=ALIGN.CENTER} TextBox{parent=section_view_div,y=1,text=title,alignment=ALIGN.CENTER}
PushButton{parent=section_view_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to} PushButton{parent=section_view_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to}
local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=60,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=60,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local def_list = ListBox{parent=section_view_div,x=1,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)} local def_list = ListBox{parent=section_view_div,x=1,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
@ -49,7 +53,7 @@ return function (data, base_page, title, items, scroll_height)
local page_end local page_end
for i = 1, #items do for i = 1, #items do
local item = items[i] ---@type pocket_doc_sect|pocket_doc_subsect|pocket_doc_text|pocket_doc_list local item = items[i] ---@type pocket_doc_sect|pocket_doc_subsect|pocket_doc_text|pocket_doc_note|pocket_doc_tip|pocket_doc_list
if item.type == DOC_TYPE.SECTION then if item.type == DOC_TYPE.SECTION then
---@cast item pocket_doc_sect ---@cast item pocket_doc_sect
@ -73,6 +77,8 @@ return function (data, base_page, title, items, scroll_height)
local _ = Div{parent=name_list,height=1} local _ = Div{parent=name_list,height=1}
end end
table.insert(search_db, { string.lower(item.name), item.name, title, view })
local name_title = Div{parent=name_list,height=1} local name_title = Div{parent=name_list,height=1}
TextBox{parent=name_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)} TextBox{parent=name_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
PushButton{parent=name_title,x=title_offs,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.green,colors.black),active_fg_bg=btn_active,callback=view} PushButton{parent=name_title,x=title_offs,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.green,colors.black),active_fg_bg=btn_active,callback=view}
@ -108,6 +114,19 @@ return function (data, base_page, title, items, scroll_height)
TextBox{parent=def_list,text=item.text} TextBox{parent=def_list,text=item.text}
page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.NOTE then
---@cast item pocket_doc_note
TextBox{parent=def_list,text=item.text,fg_bg=cpair(colors.gray,colors._INHERIT)}
page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.TIP then
---@cast item pocket_doc_tip
TextBox{parent=def_list,text="TIP!",fg_bg=cpair(colors.orange,colors._INHERIT)}
TextBox{parent=def_list,text=item.text}
page_end = Div{parent=def_list,height=1,can_focus=true} page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.LIST then elseif item.type == DOC_TYPE.LIST then
---@cast item pocket_doc_list ---@cast item pocket_doc_list

View File

@ -9,11 +9,9 @@ local core = require("graphics.core")
local AppMultiPane = require("graphics.elements.AppMultiPane") local AppMultiPane = require("graphics.elements.AppMultiPane")
local Div = require("graphics.elements.Div") local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local App = require("graphics.elements.controls.App") local App = require("graphics.elements.controls.App")
local ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local APP_ID = pocket.APP_ID local APP_ID = pocket.APP_ID
@ -50,15 +48,12 @@ local function new_view(root)
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg} App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.PROCESS)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg} App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.PROCESS)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.WASTE)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg} App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.WASTE)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=7,text="\x08",title="Devices",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg} App{parent=apps_1,x=16,y=7,text="\xb6",title="Guide",callback=function()open(APP_ID.GUIDE)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=12,text="\xb6",title="Guide",callback=function()open(APP_ID.GUIDE)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg} App{parent=apps_1,x=2,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}
TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,alignment=ALIGN.CENTER} App{parent=apps_2,x=2,y=2,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=9,y=2,text="@",title="Comps",callback=function()open(APP_ID.COMPS)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=2,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg} App{parent=apps_2,x=16,y=2,text="\x1e",title="Rad",callback=function()open(APP_ID.RADMON)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=9,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=16,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
return main return main
end end

View File

@ -1,3 +1,7 @@
--
-- Unit Boiler View
--
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -13,8 +17,8 @@ local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton") local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator") local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar") local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local ALIGN = core.ALIGN local ALIGN = core.ALIGN

View File

@ -1,3 +1,7 @@
--
-- Unit Reactor View
--
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -13,8 +17,8 @@ local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton") local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator") local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator") local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local VerticalBar = require("graphics.elements.indicators.VerticalBar") local VerticalBar = require("graphics.elements.indicators.VerticalBar")
local ALIGN = core.ALIGN local ALIGN = core.ALIGN

View File

@ -1,3 +1,7 @@
--
-- Unit Turbine View
--
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")

View File

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

@ -82,8 +82,9 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49} local plc_c_2 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49} local plc_c_3 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49} local plc_c_4 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_c_5 = Div{parent=plc_cfg,x=2,y=4,width=49}
local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4}} local plc_pane = MultiPane{parent=plc_cfg,x=1,y=4,panes={plc_c_1,plc_c_2,plc_c_3,plc_c_4,plc_c_5}}
TextBox{parent=plc_cfg,x=1,y=2,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)} TextBox{parent=plc_cfg,x=1,y=2,text=" PLC Configuration",fg_bg=cpair(colors.black,colors.orange)}
@ -152,13 +153,21 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
function self.bundled_emcool(en) if en then color.enable() else color.disable() end end function self.bundled_emcool(en) if en then color.enable() else color.disable() end end
TextBox{parent=plc_c_5,x=1,y=1,height=5,text="Advanced Options"}
local invert = Checkbox{parent=plc_c_5,x=1,y=3,label="Invert",default=ini_cfg.EmerCoolInvert,box_fg_bg=cpair(colors.orange,colors.black),callback=function()end}
TextBox{parent=plc_c_5,x=10,y=3,text="new!",fg_bg=cpair(colors.red,colors._INHERIT)} ---@todo remove NEW tag on next revision
TextBox{parent=plc_c_5,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=plc_c_5,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function submit_emcool() local function submit_emcool()
tmp_cfg.EmerCoolSide = side_options_map[side.get_value()] tmp_cfg.EmerCoolSide = side_options_map[side.get_value()]
tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil) tmp_cfg.EmerCoolColor = util.trinary(bundled.get_value(), color_options_map[color.get_value()], nil)
tmp_cfg.EmerCoolInvert = invert.get_value()
next_from_plc() next_from_plc()
end end
PushButton{parent=plc_c_4,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_4,x=1,y=14,text="\x1b Back",callback=function()plc_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=plc_c_4,x=33,y=14,min_width=10,text="Advanced",callback=function()plc_pane.set_value(5)end,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=btn_act_fg_bg,dis_fg_bg=btn_dis_fg_bg}
PushButton{parent=plc_c_4,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} PushButton{parent=plc_c_4,x=44,y=14,text="Next \x1a",callback=submit_emcool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion --#endregion
@ -461,6 +470,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
try_set(side, side_to_idx(ini_cfg.EmerCoolSide)) try_set(side, side_to_idx(ini_cfg.EmerCoolSide))
try_set(bundled, ini_cfg.EmerCoolColor ~= nil) try_set(bundled, ini_cfg.EmerCoolColor ~= nil)
if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end if ini_cfg.EmerCoolColor ~= nil then try_set(color, color_to_idx(ini_cfg.EmerCoolColor)) end
try_set(invert, ini_cfg.EmerCoolInvert)
try_set(svr_chan, ini_cfg.SVR_Channel) try_set(svr_chan, ini_cfg.SVR_Channel)
try_set(plc_chan, ini_cfg.PLC_Channel) try_set(plc_chan, ini_cfg.PLC_Channel)
try_set(timeout, ini_cfg.ConnTimeout) try_set(timeout, ini_cfg.ConnTimeout)
@ -533,9 +543,11 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
if tmp_cfg.EmerCoolEnable then if tmp_cfg.EmerCoolEnable then
tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side tmp_cfg.EmerCoolSide = config.EMERGENCY_COOL.side
tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color tmp_cfg.EmerCoolColor = config.EMERGENCY_COOL.color
tmp_cfg.EmerCoolInvert = false
else else
tmp_cfg.EmerCoolSide = nil tmp_cfg.EmerCoolSide = nil
tmp_cfg.EmerCoolColor = nil tmp_cfg.EmerCoolColor = nil
tmp_cfg.EmerCoolInvert = false
end end
tmp_cfg.SVR_Channel = config.SVR_CHANNEL tmp_cfg.SVR_Channel = config.SVR_CHANNEL

View File

@ -32,7 +32,8 @@ local changes = {
{ "v1.6.2", { "AuthKey minimum length is now 8 (if set)" } }, { "v1.6.2", { "AuthKey minimum length is now 8 (if set)" } },
{ "v1.6.8", { "ConnTimeout can now have a fractional part" } }, { "v1.6.8", { "ConnTimeout can now have a fractional part" } },
{ "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } }, { "v1.6.15", { "Added front panel UI theme", "Added color accessibility modes" } },
{ "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } } { "v1.7.3", { "Added standard with black off state color mode", "Added blue indicator color modes" } },
{ "v1.8.21", { "Added option to invert emergency coolant redstone control" } }
} }
---@class plc_configurator ---@class plc_configurator
@ -76,6 +77,7 @@ local tmp_cfg = {
EmerCoolEnable = false, EmerCoolEnable = false,
EmerCoolSide = nil, ---@type string|nil EmerCoolSide = nil, ---@type string|nil
EmerCoolColor = nil, ---@type color|nil EmerCoolColor = nil, ---@type color|nil
EmerCoolInvert = false, ---@type boolean
SVR_Channel = nil, ---@type integer SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer PLC_Channel = nil, ---@type integer
ConnTimeout = nil, ---@type number ConnTimeout = nil, ---@type number
@ -100,6 +102,7 @@ local fields = {
{ "EmerCoolEnable", "Emergency Coolant", false }, { "EmerCoolEnable", "Emergency Coolant", false },
{ "EmerCoolSide", "Emergency Coolant Side", nil }, { "EmerCoolSide", "Emergency Coolant Side", nil },
{ "EmerCoolColor", "Emergency Coolant Color", nil }, { "EmerCoolColor", "Emergency Coolant Color", nil },
{ "EmerCoolInvert", "Emergency Coolant Invert", false },
{ "SVR_Channel", "SVR Channel", 16240 }, { "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 }, { "PLC_Channel", "PLC Channel", 16241 },
{ "ConnTimeout", "Connection Timeout", 5 }, { "ConnTimeout", "Connection Timeout", 5 },

View File

@ -43,6 +43,7 @@ function plc.load_config()
config.EmerCoolEnable = settings.get("EmerCoolEnable") config.EmerCoolEnable = settings.get("EmerCoolEnable")
config.EmerCoolSide = settings.get("EmerCoolSide") config.EmerCoolSide = settings.get("EmerCoolSide")
config.EmerCoolColor = settings.get("EmerCoolColor") config.EmerCoolColor = settings.get("EmerCoolColor")
config.EmerCoolInvert = settings.get("EmerCoolInvert")
config.SVR_Channel = settings.get("SVR_Channel") config.SVR_Channel = settings.get("SVR_Channel")
config.PLC_Channel = settings.get("PLC_Channel") config.PLC_Channel = settings.get("PLC_Channel")
@ -98,6 +99,7 @@ function plc.validate_config(cfg)
if cfg.EmerCoolEnable then if cfg.EmerCoolEnable then
cfv.assert_eq(rsio.is_valid_side(cfg.EmerCoolSide), true) cfv.assert_eq(rsio.is_valid_side(cfg.EmerCoolSide), true)
cfv.assert_eq(cfg.EmerCoolColor == nil or rsio.is_color(cfg.EmerCoolColor), true) cfv.assert_eq(cfg.EmerCoolColor == nil or rsio.is_color(cfg.EmerCoolColor), true)
cfv.assert_type_bool(cfg.EmerCoolInvert)
end end
return cfv.valid() return cfv.valid()
@ -166,7 +168,8 @@ function plc.rps_init(reactor, is_formed)
local function _set_emer_cool(state) local function _set_emer_cool(state)
-- check if this was configured: if it's a table, fields have already been validated. -- check if this was configured: if it's a table, fields have already been validated.
if config.EmerCoolEnable then if config.EmerCoolEnable then
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, state) -- use ~= as XOR for simple inversion
local level = rsio.digital_write_active(rsio.IO.U_EMER_COOL, config.EmerCoolInvert ~= state)
if level ~= false then if level ~= false then
if rsio.is_color(config.EmerCoolColor) then if rsio.is_color(config.EmerCoolColor) then

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.19" local R_PLC_VERSION = "v1.8.22"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

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" or p_type == "environment_detector" 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 == "reinforcedInductionPort" 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

@ -43,8 +43,8 @@ local self = {
local peripherals = {} local peripherals = {}
local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "spsPort", "solarNeutronActivator", "environmentDetector" } local RTU_DEV_TYPES = { "boilerValve", "turbineValve", "dynamicValve", "inductionPort", "reinforcedInductionPort", "spsPort", "solarNeutronActivator", "environmentDetector", "environment_detector" }
local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector" } local NEEDS_UNIT = { "boilerValve", "turbineValve", "dynamicValve", "solarNeutronActivator", "environmentDetector", "environment_detector" }
-- create the peripherals configuration view -- create the peripherals configuration view
---@param tool_ctl _rtu_cfg_tool_ctl ---@param tool_ctl _rtu_cfg_tool_ctl
@ -165,14 +165,14 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
end end
self.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.") self.p_desc.set_value("Each reactor unit can have at most 1 tank and the facility can have at most 4. Each facility tank must have a unique # 1 through 4, regardless of where it is connected. Only a total of 4 tanks can be displayed on the flow monitor.")
elseif type == "environmentDetector" then elseif type == "environmentDetector" or type == "environment_detector" then
reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8) reposition("This is the below system's # env. detector.", 29, 99, 17, 6, 8)
self.p_assign_btn.show() self.p_assign_btn.show()
self.p_assign_btn.redraw() self.p_assign_btn.redraw()
if self.p_assign_btn.get_value() == 1 then self.p_unit.disable() else self.p_unit.enable() end if self.p_assign_btn.get_value() == 1 then self.p_unit.disable() else self.p_unit.enable() end
self.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.") self.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.")
elseif type == "inductionPort" or type == "spsPort" then elseif type == "inductionPort" or type == "reinforcedInductionPort" or type == "spsPort" then
local dev = tri(type == "inductionPort", "induction matrix", "SPS") local dev = tri(type == "inductionPort" or type == "reinforcedInductionPort", "induction matrix", "SPS")
self.p_idx.hide(true) self.p_idx.hide(true)
self.p_unit.hide(true) self.p_unit.hide(true)
self.p_prompt.set_value("This is the " .. dev .. " for the facility.") self.p_prompt.set_value("This is the " .. dev .. " for the facility.")
@ -212,10 +212,10 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
tool_ctl.update_peri_list() tool_ctl.update_peri_list()
TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you are clicking this just because your device is not shown, follow the connection instructions in 'I don't see my device!'."} TextBox{parent=peri_c_3,x=1,y=1,height=4,text="This feature is intended for advanced users. If you just can't see your device, click 'I don't see my device!' instead."}
TextBox{parent=peri_c_3,x=1,y=6,height=4,text="Peripheral Name"} TextBox{parent=peri_c_3,x=1,y=5,height=4,text="Peripheral Name"}
local p_name = TextField{parent=peri_c_3,x=1,y=7,width=49,height=1,max_len=128,fg_bg=bw_fg_bg} local p_name = TextField{parent=peri_c_3,x=1,y=6,width=49,height=1,max_len=128,fg_bg=bw_fg_bg}
local p_type = Radio2D{parent=peri_c_3,x=1,y=9,rows=4,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple} local p_type = Radio2D{parent=peri_c_3,x=1,y=8,rows=5,columns=2,default=1,options=RTU_DEV_TYPES,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.purple}
local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true} local man_p_err = TextBox{parent=peri_c_3,x=8,y=14,width=35,text="Please enter a peripheral name.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
man_p_err.hide(true) man_p_err.hide(true)
@ -281,7 +281,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
local idx = tonumber(self.p_idx.get_value()) local idx = tonumber(self.p_idx.get_value())
if util.table_contains(NEEDS_UNIT, peri_type) then if util.table_contains(NEEDS_UNIT, peri_type) then
if (peri_type == "dynamicValve" or peri_type == "environmentDetector") and for_facility then if (peri_type == "dynamicValve" or peri_type == "environmentDetector" or peri_type == "environment_detector") and for_facility then
-- skip -- skip
elseif not (util.is_int(u) and u > 0 and u < 5) then elseif not (util.is_int(u) and u > 0 and u < 5) then
self.p_err.set_value("Unit ID must be within 1 to 4.") self.p_err.set_value("Unit ID must be within 1 to 4.")
@ -310,7 +310,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
else index = idx end else index = idx end
elseif peri_type == "dynamicValve" then elseif peri_type == "dynamicValve" then
index = 1 index = 1
elseif peri_type == "environmentDetector" then elseif peri_type == "environmentDetector" or peri_type == "environment_detector" then
if not (util.is_int(idx) and idx > 0) then if not (util.is_int(idx) and idx > 0) then
self.p_err.set_value("Index must be greater than 0.") self.p_err.set_value("Index must be greater than 0.")
self.p_err.show() self.p_err.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
@ -105,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
@ -132,13 +164,82 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
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)
@ -146,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
@ -191,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)
@ -205,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
@ -233,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
@ -254,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())
@ -302,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
@ -319,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
@ -374,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
@ -391,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)
@ -401,22 +549,29 @@ 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]
if def.relay == self.rs_cfg_phy then
local name = rsio.to_string(def.port) local name = rsio.to_string(def.port)
local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b") local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_c = tri(rsio.is_digital(def.port), colors.blue, colors.purple)
local conn = def.side local conn = def.side
local unit = util.strval(def.unit or "F") 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)}
@ -427,7 +582,8 @@ function redstone.create(tool_ctl, main_pane, cfg_sys, rs_cfg, style)
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

@ -506,7 +506,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local u, idx = def.unit, def.index local u, idx = def.unit, def.index
if util.table_contains(NEEDS_UNIT, mount.type) then if util.table_contains(NEEDS_UNIT, mount.type) then
if (mount.type == "dynamicValve" or mount.type == "environmentDetector") and for_facility then if (mount.type == "dynamicValve" or mount.type == "environmentDetector" or mount.type == "environment_detector") and for_facility then
-- skip -- skip
elseif not (util.is_int(u) and u > 0 and u < 5) then elseif not (util.is_int(u) and u > 0 and u < 5) then
err = true err = true
@ -527,7 +527,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
else index = idx end else index = idx end
elseif mount.type == "dynamicValve" then elseif mount.type == "dynamicValve" then
index = 1 index = 1
elseif mount.type == "environmentDetector" then elseif mount.type == "environmentDetector" or mount.type == "environment_detector" then
if not (util.is_int(idx) and idx > 0) then if not (util.is_int(idx) and idx > 0) then
err = true err = true
else index = idx end else index = idx 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
---@return integer count count of digital inputs
function public.link_di(side, color, invert)
local f_read ---@type function 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
---@return integer count count of digital outputs
function public.link_do(side, color, invert)
local f_read ---@type function local f_read ---@type function
local f_write ---@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)
-- inverted conditions
if digital_write(level) then
output = colors.subtract(output, color)
else output = colors.combine(output, color) end
phy.setBundledOutput(side, output)
end
end
else
f_read = function () return digital_read(colors.test(phy.getBundledOutput(side), color)) end
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 if digital_write(level) then
output = colors.combine(output, color) output = colors.combine(output, color)
else else output = colors.subtract(output, color) end
output = colors.subtract(output, color)
end
rs.setBundledOutput(side, output) 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

@ -20,6 +20,7 @@ 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
@ -129,31 +130,46 @@ local function init(panel, units)
-- 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=term_h-3,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)
if unit.reactor then
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor) 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} TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
end end
end end
end
return init return init

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.6" local RTU_VERSION = "v1.12.3"
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
@ -374,8 +440,8 @@ local function main()
println_ts(util.c("sys_config> failed to check if '", name, "' is formed")) println_ts(util.c("sys_config> failed to check if '", name, "' is formed"))
log.warning(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock")) log.warning(util.c("sys_config> failed to check if '", name, "' is a formed dynamic tank multiblock"))
end end
elseif type == "inductionPort" then elseif type == "inductionPort" or type == "reinforcedInductionPort" then
-- induction matrix multiblock -- induction matrix multiblock (normal or reinforced)
if not validate_assign(true) then return false end if not validate_assign(true) then return false end
rtu_type = RTU_UNIT_TYPE.IMATRIX rtu_type = RTU_UNIT_TYPE.IMATRIX
@ -406,7 +472,7 @@ local function main()
rtu_type = RTU_UNIT_TYPE.SNA rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface, faulted = sna_rtu.new(device) rtu_iface, faulted = sna_rtu.new(device)
elseif type == "environmentDetector" then elseif type == "environmentDetector" or type == "environment_detector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
if not validate_index(1) then return false end if not validate_index(1) then return false end
if not validate_assign(entry.unit == nil) then return false end if not validate_assign(entry.unit == nil) then return false end
@ -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

@ -74,7 +74,7 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
end end
unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE unit.type = RTU_UNIT_TYPE.DYNAMIC_VALVE
elseif type == "inductionPort" then elseif type == "inductionPort" or type == "reinforcedInductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end if unit.reactor ~= 0 then fail(util.c("induction matrix '", unit.name, "' cannot init, not assigned to facility")) end
@ -89,7 +89,7 @@ local function handle_unit_mount(smem, println_ts, iface, type, device, unit)
if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end if unit.reactor < 1 or unit.reactor > 4 then fail(util.c("SNA '", unit.name, "' cannot init, not assigned to a valid unit")) end
unit.type = RTU_UNIT_TYPE.SNA unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then elseif type == "environmentDetector" or type == "environment_detector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end if unit.reactor < 0 or unit.reactor > 4 then fail(util.c("environment detector '", unit.name, "' cannot init, no valid assignment provided")) end
if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end if (unit.index == false) or unit.index < 1 then fail(util.c("environment detector '", unit.name, "' cannot init, invalid index provided")) end
@ -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,8 +17,8 @@ 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.5" comms.version = "3.0.8"
comms.api_version = "0.0.9" comms.api_version = "0.0.10"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@ -52,9 +52,10 @@ local MGMT_TYPE = {
RTU_ADVERT = 3, -- RTU capability advertisement RTU_ADVERT = 3, -- RTU capability advertisement
RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones RTU_TONE_ALARM = 5, -- instruct RTUs to play specified alarm tones
DIAG_TONE_GET = 6, -- diagnostic: get alarm tones DIAG_TONE_GET = 6, -- (API) diagnostic: get alarm tones
DIAG_TONE_SET = 7, -- diagnostic: set alarm tones DIAG_TONE_SET = 7, -- (API) diagnostic: set alarm tones
DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for DIAG_ALARM_SET = 8, -- (API) diagnostic: set alarm to simulate audio for
INFO_LIST_CMP = 9 -- (API) info: list all computers on the network
} }
---@enum CRDN_TYPE ---@enum CRDN_TYPE
@ -72,7 +73,8 @@ local CRDN_TYPE = {
API_GET_UNIT = 10, -- API: get reactor unit data API_GET_UNIT = 10, -- API: get reactor unit data
API_GET_CTRL = 11, -- API: get data for the control app API_GET_CTRL = 11, -- API: get data for the control app
API_GET_PROC = 12, -- API: get data for the process app API_GET_PROC = 12, -- API: get data for the process app
API_GET_WASTE = 13 -- API: get data for the waste app API_GET_WASTE = 13, -- API: get data for the waste app
API_GET_RAD = 14 -- API: get data for the radiation monitor app
} }
---@enum ESTABLISH_ACK ---@enum ESTABLISH_ACK

View File

@ -88,6 +88,7 @@ constants.FLOW_STABILITY_DELAY_MS = 10000
-- - background radiation 0.0000001 Sv/h (99.99 nSv/h) -- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h) -- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
-- - damaging radiation 0.00006 Sv/h (60 uSv/h) -- - damaging radiation 0.00006 Sv/h (60 uSv/h)
constants.LOW_RADIATION = 0.00001 constants.LOW_RADIATION = 0.00001
constants.HAZARD_RADIATION = 0.00006 constants.HAZARD_RADIATION = 0.00006
constants.HIGH_RADIATION = 0.001 constants.HIGH_RADIATION = 0.001
@ -95,6 +96,11 @@ constants.VERY_HIGH_RADIATION = 0.1
constants.SEVERE_RADIATION = 8.0 constants.SEVERE_RADIATION = 8.0
constants.EXTREME_RADIATION = 100.0 constants.EXTREME_RADIATION = 100.0
-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick
constants.WARN_RTT = 300 -- 2x as long as expected w/ 0 ping
constants.HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping
--#endregion --#endregion
--#region Mekanism Configuration Constants --#region Mekanism Configuration Constants

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()
-- default text failure message
if not handled then
util.println("fatal error occured in main application:") util.println("fatal error occured in main application:")
error(err, 0) error(err, 0)
end end
end
return crash return crash

View File

@ -453,7 +453,7 @@ function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAda
---@return Modem|nil modem function table ---@return Modem|nil modem function table
function ppm.get_wireless_modem() function ppm.get_wireless_modem()
local w_modem = nil local w_modem = nil
local emulated_env = periphemu ~= nil local emulated_env = true
for _, device in pairs(ppm_sys.mounts) do for _, device in pairs(ppm_sys.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then if device.type == "modem" and (emulated_env or device.dev.isWireless()) then

View File

@ -53,12 +53,12 @@ function psil.create()
if ic[key] == nil then alloc(key) end if ic[key] == nil then alloc(key) end
if ic[key].value ~= value then if ic[key].value ~= value then
ic[key].value = value
for i = 1, #ic[key].subscribers do for i = 1, #ic[key].subscribers do
ic[key].subscribers[i].notify(value) ic[key].subscribers[i].notify(value)
end end
end end
ic[key].value = value
end end
-- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed<br> -- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed<br>

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.12" util.version = "1.5.4"
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

@ -2,15 +2,12 @@
-- Data Bus - Central Communication Linking for Supervisor Front Panel -- Data Bus - Central Communication Linking for Supervisor Front Panel
-- --
local const = require("scada-common.constants")
local psil = require("scada-common.psil") local psil = require("scada-common.psil")
local util = require("scada-common.util") local util = require("scada-common.util")
local pgi = require("supervisor.panel.pgi") local pgi = require("supervisor.panel.pgi")
-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick
local WARN_RTT = 300 -- 2x as long as expected w/ 0 ping
local HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping
local databus = {} local databus = {}
-- databus PSIL -- databus PSIL
@ -59,9 +56,9 @@ end
function databus.tx_plc_rtt(reactor_id, rtt) function databus.tx_plc_rtt(reactor_id, rtt)
databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt) databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt)
if rtt > HIGH_RTT then if rtt > const.HIGH_RTT then
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then elseif rtt > const.WARN_RTT then
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green_hc) databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green_hc)
@ -90,9 +87,9 @@ end
function databus.tx_rtu_rtt(session_id, rtt) function databus.tx_rtu_rtt(session_id, rtt)
databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt) databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then if rtt > const.HIGH_RTT then
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red) databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then elseif rtt > const.WARN_RTT then
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc) databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green_hc) databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green_hc)
@ -129,9 +126,9 @@ end
function databus.tx_crd_rtt(rtt) function databus.tx_crd_rtt(rtt)
databus.ps.publish("crd_rtt", rtt) databus.ps.publish("crd_rtt", rtt)
if rtt > HIGH_RTT then if rtt > const.HIGH_RTT then
databus.ps.publish("crd_rtt_color", colors.red) databus.ps.publish("crd_rtt_color", colors.red)
elseif rtt > WARN_RTT then elseif rtt > const.WARN_RTT then
databus.ps.publish("crd_rtt_color", colors.yellow_hc) databus.ps.publish("crd_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("crd_rtt_color", colors.green_hc) databus.ps.publish("crd_rtt_color", colors.green_hc)
@ -160,9 +157,9 @@ end
function databus.tx_pdg_rtt(session_id, rtt) function databus.tx_pdg_rtt(session_id, rtt)
databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt) databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt)
if rtt > HIGH_RTT then if rtt > const.HIGH_RTT then
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red) databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red)
elseif rtt > WARN_RTT then elseif rtt > const.WARN_RTT then
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc) databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc)
else else
databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green_hc) databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green_hc)

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
@ -138,7 +144,17 @@ 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 --#region SETUP
@ -157,7 +173,7 @@ function facility.new(config)
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
@ -335,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
@ -404,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

View File

@ -5,6 +5,8 @@ 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 alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
@ -643,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
@ -714,11 +716,17 @@ function update.post_auto()
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
@ -734,8 +742,11 @@ function update.alarm_audio()
end end
end end
if not self.test_tone_reset then -- record facility alarms
alarms[ALARM.FacilityRadiation] = self.alarm_states[ALARM.FacilityRadiation] == ALARM_STATE.TRIPPED
-- clear testing alarms if we aren't using them -- clear testing alarms if we aren't using them
if not self.test_tone_reset then
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
@ -774,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

View File

@ -2,10 +2,12 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
local pocket = {} local pocket = {}
local DEV_TYPE = comms.DEVICE_TYPE
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
@ -34,9 +36,10 @@ 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 sessions svsessions_list list of computer sessions, read-only
---@param facility facility facility data table ---@param facility facility facility data table
---@param fp_ok boolean if the front panel UI is running ---@param fp_ok boolean if the front panel UI is running
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, facility, fp_ok) function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout, sessions, facility, 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
@ -182,6 +185,47 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout,
end end
if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end if not valid then _send_mgmt(MGMT_TYPE.DIAG_ALARM_SET, { false }) end
elseif pkt.type == MGMT_TYPE.INFO_LIST_CMP then
local get = databus.ps.get
---@diagnostic disable-next-line: undefined-field
local devices = { { DEV_TYPE.SVR, os.getComputerID(), get("version"), 0 } }
-- add the coordinator if connected
if get("crd_conn") then
table.insert(devices, { DEV_TYPE.CRD, get("crd_addr"), get("crd_fw"), get("crd_rtt") })
end
-- add the PLCs if connected
for i = 1, #facility.get_units() do
local tag = "plc_" .. i
local addr = -1
for _, s in ipairs(sessions.plc) do
if s.reactor == i then
addr = s.s_addr
break
end
end
if get(tag .. "_conn") then
table.insert(devices, { DEV_TYPE.PLC, addr, get(tag .. "_fw"), get(tag .. "_rtt"), i })
end
end
-- add connected RTUs
for i = 1, #sessions.rtu do
local s = sessions.rtu[i]
table.insert(devices, { DEV_TYPE.RTU, s.s_addr, s.version, get("rtu_" .. s.instance.get_id() .. "_rtt") })
end
-- add connected pocket computers
for i = 1, #sessions.pdg do
local s = sessions.pdg[i]
table.insert(devices, { DEV_TYPE.PKT, s.s_addr, s.version, get("pdg_" .. s.instance.get_id() .. "_rtt") })
end
_send_mgmt(MGMT_TYPE.INFO_LIST_CMP, devices)
else else
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end

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
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_min(unit_advert.reactor, 0)
advert_validator.assert_max(unit_advert.reactor, #self.fac_units) 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,106 +105,104 @@ 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 end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
elseif mode == IO_MODE.ANALOG_IN then elseif mode == IO_MODE.ANALOG_IN then
self.has_ai = true self.has_ai = true
table.insert(self.io_list.analog_in, port) table.insert(self.io_map.analog_in, io_entry)
self.phy_io.analog_in[port] = { phy = 0, req = 0 } self.phy_io.analog_in[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io ---@class rs_db_ana_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
---@return integer ---@return integer
read = function () return self.phy_io.analog_in[port].phy end, read = function () return self.phy_io.analog_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.ANALOG_OUT then elseif mode == IO_MODE.ANALOG_OUT then
self.has_ao = true self.has_ao = true
table.insert(self.io_list.analog_out, port) table.insert(self.io_map.analog_out, io_entry)
self.phy_io.analog_out[port] = { phy = 0, req = 0 } self.phy_io.analog_out[bank][port] = { phy = 0, req = 0 }
---@class rs_db_ana_io ---@class rs_db_ana_io
local io_f = { local io_f = {
---@nodiscard ---@nodiscard
---@return integer ---@return integer
read = function () return self.phy_io.analog_out[port].phy end, read = function () return self.phy_io.analog_out[bank][port].phy end,
---@param value integer ---@param value integer
write = function (value) write = function (value)
if value >= 0 and value <= 15 then if value >= 0 and value <= 15 then
self.phy_io.analog_out[port].req = value self.phy_io.analog_out[bank][port].req = value
end end
end end
} }
self.db.io[port] = io_f self.db.io[bank][port] = io_f
else else
-- should be unreachable code, we already validated ports -- should be unreachable code, we already validated ports
log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", port, ")"), true) log.error(util.c(log_tag, "failed to identify advertisement port IO mode (", bank, ":", port, ")"), true)
return nil return nil
end end
else else
log.error(util.c(log_tag, "invalid advertisement port (", port, ")"), true) log.error(util.c(log_tag, "invalid advertisement port (", bank, ":", port, ")"), true)
return nil return nil
end end
end end
end
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- 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
local changed = false
for _, entry in pairs(self.phy_io.digital_out[bank]) do
if entry.phy ~= entry.req then 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
local changed = false
for _, entry in pairs(self.phy_io.analog_out[bank]) do
if entry.phy ~= entry.req then 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

@ -47,12 +47,13 @@ local self = {
facility = nil, ---@type facility|nil facility = nil, ---@type facility|nil
plc_ini_reset = {}, plc_ini_reset = {},
-- lists of connected sessions -- lists of connected sessions
---@class svsessions_list
---@diagnostic disable: missing-fields ---@diagnostic disable: missing-fields
sessions = { sessions = {
rtu = {}, ---@type rtu_session_struct rtu = {}, ---@type rtu_session_struct[]
plc = {}, ---@type plc_session_struct plc = {}, ---@type plc_session_struct[]
crd = {}, ---@type crd_session_struct crd = {}, ---@type crd_session_struct[]
pdg = {} ---@type pdg_session_struct pdg = {} ---@type pdg_session_struct[]
}, },
---@diagnostic enable: missing-fields ---@diagnostic enable: missing-fields
-- next session IDs -- next session IDs
@ -621,7 +622,7 @@ function svsessions.establish_pdg_session(source_addr, i_seq_num, version)
local id = self.next_ids.pdg local id = self.next_ids.pdg
pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.facility, self.fp_ok) pdg_s.instance = pocket.new_session(id, source_addr, i_seq_num, pdg_s.in_queue, pdg_s.out_queue, self.config.PKT_Timeout, self.sessions, self.facility, self.fp_ok)
table.insert(self.sessions.pdg, pdg_s) table.insert(self.sessions.pdg, pdg_s)
local mt = { local mt = {

View File

@ -23,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.8" local SUPERVISOR_VERSION = "v1.7.1"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts

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
@ -81,7 +67,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
num_boilers = num_boilers, num_boilers = num_boilers,
num_turbines = num_turbines, num_turbines = num_turbines,
aux_coolant = aux_coolant, aux_coolant = aux_coolant,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, 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[]
@ -258,7 +244,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
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
@ -597,20 +583,20 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
_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.em_cool_opened = false self.em_cool_opened = false
end end
@ -775,10 +761,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
-- 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

View File

@ -4,10 +4,14 @@ 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 alarm_ctl = require("supervisor.alarm_ctl")
local plc = require("supervisor.session.plc") local plc = require("supervisor.session.plc")
local qtypes = require("supervisor.session.rtu.qtypes") 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
local CONTAINER_MODE = types.CONTAINER_MODE local CONTAINER_MODE = types.CONTAINER_MODE
@ -22,19 +26,9 @@ local IO = rsio.IO
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
"ACKED",
"RING_BACK",
"RING_BACK_TRIPPING"
}
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
local ALARM_LIMS = const.ALARM_LIMITS local ALARM_LIMS = const.ALARM_LIMITS
local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local RS_THRESH = const.RS_THRESHOLDS local RS_THRESH = const.RS_THRESHOLDS
---@class unit_logic_extension ---@class unit_logic_extension
@ -179,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
@ -426,97 +416,16 @@ function logic.update_annunciator(self)
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
@ -629,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
@ -660,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)
@ -824,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
@ -906,7 +811,7 @@ function logic.handle_redstone(self)
if enable_emer_cool and not self.em_cool_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], "]"))