Compare commits

..

17 Commits
devel ... main

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
6eb9ac5845
Merge pull request #626 from MikaylaFischler/devel
2025.06.29 Release
2025-06-29 16:48:45 -04:00
Mikayla
919ca6f0af
Merge pull request #620 from MikaylaFischler/devel
2025.05.10 Release
2025-05-10 17:45:19 -04:00
Mikayla
b1ad2084f2
Merge pull request #610 from MikaylaFischler/devel
2025.02.26 Release
2025-02-26 18:52:56 -05:00
Mikayla
cf9e26ac8f
Merge pull request #599 from MikaylaFischler/devel
Pocket Beta Release
2025-01-27 12:52:32 -05:00
Mikayla
451232ce91
Merge pull request #586 from MikaylaFischler/devel
2024.12.21 Release
2024-12-21 12:30:33 -05:00
Mikayla
c6343e5956
Merge pull request #579 from MikaylaFischler/devel
2024.11.21 Release
2024-11-21 18:40:52 -05:00
Mikayla
7130176781
Merge pull request #563 from MikaylaFischler/devel
2024.10.18 Release
2024-10-18 20:34:14 -04:00
Mikayla
8e19418701
Merge pull request #547 from MikaylaFischler/devel
2024.09.08 Release
2024-09-11 21:29:36 -04:00
Mikayla
07406ca5fc
Merge pull request #542 from MikaylaFischler/devel
2024.08.25 Release
2024-08-25 22:50:18 -04:00
Mikayla
b0342654e7
added off-line installation to installation options 2024-08-12 09:55:19 -04:00
Mikayla
f725eb0eef
Merge pull request #533 from MikaylaFischler/devel
2024.07.28 Release
2024-07-28 17:21:26 -04:00
13 changed files with 539 additions and 477 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

@ -45,6 +45,7 @@ v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v
You can install this on a ComputerCraft computer using either: You can install this on a ComputerCraft computer using either:
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua` * `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
* `pastebin get sqUN6VUb ccmsi.lua` * `pastebin get sqUN6VUb ccmsi.lua`
* Off-line (when HTTP is disabled) installation via [release bundles](https://github.com/MikaylaFischler/cc-mek-scada/wiki/Alternative-Installation-Strategies#release-bundles)
## Contributing ## Contributing

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

@ -53,6 +53,7 @@ function databus.tx_hw_status(plc_state)
databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2))) databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
databus.ps.publish("has_modem", not plc_state.no_modem) databus.ps.publish("has_modem", not plc_state.no_modem)
databus.ps.publish("degraded", plc_state.degraded) databus.ps.publish("degraded", plc_state.degraded)
databus.ps.publish("init_ok", plc_state.init_ok)
end end
-- transmit thread (routine) statuses -- transmit thread (routine) statuses

View File

@ -51,11 +51,11 @@ local function init(panel)
local system = Div{parent=panel,width=14,height=18,x=2,y=3} local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local degraded = LED{parent=system,label="STATUS",colors=cpair(colors.red,colors.green)} local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn} local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
system.line_break() system.line_break()
degraded.register(databus.ps, "degraded", degraded.update) init_ok.register(databus.ps, "init_ok", init_ok.update)
heartbeat.register(databus.ps, "heartbeat", heartbeat.update) heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green} local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}

View File

@ -118,7 +118,7 @@ function plc.rps_init(reactor, is_formed)
reactor_enabled = false, reactor_enabled = false,
enabled_at = 0, enabled_at = 0,
emer_cool_active = nil, ---@type boolean emer_cool_active = nil, ---@type boolean
formed = is_formed, ---@type boolean|nil formed = is_formed,
force_disabled = false, force_disabled = false,
tripped = false, tripped = false,
trip_cause = "ok" ---@type rps_trip_cause trip_cause = "ok" ---@type rps_trip_cause
@ -364,35 +364,29 @@ function plc.rps_init(reactor, is_formed)
return public.activate() return public.activate()
end end
-- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions -- check all safety conditions
---@nodiscard ---@nodiscard
---@param has_reactor boolean if the PLC state indicates we have a reactor
---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip ---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip
function public.check(has_reactor) function public.check()
local status = RPS_TRIP_CAUSE.OK local status = RPS_TRIP_CAUSE.OK
local was_tripped = self.tripped local was_tripped = self.tripped
local first_trip = false local first_trip = false
if has_reactor then if self.formed then
if self.formed then -- update state
-- update state parallel.waitForAll(
parallel.waitForAll( _is_formed,
_is_formed, _is_force_disabled,
_is_force_disabled, _high_damage,
_high_damage, _high_temp,
_high_temp, _low_coolant,
_low_coolant, _excess_waste,
_excess_waste, _excess_heated_coolant,
_excess_heated_coolant, _insufficient_fuel
_insufficient_fuel )
)
else
-- check to see if its now formed
_is_formed()
end
else else
self.formed = nil -- check to see if its now formed
self.state[CHK.SYS_FAIL] = true _is_formed()
end end
-- check system states in order of severity -- check system states in order of severity
@ -480,7 +474,6 @@ function plc.rps_init(reactor, is_formed)
---@nodiscard ---@nodiscard
function public.is_active() return self.reactor_enabled end function public.is_active() return self.reactor_enabled end
---@nodiscard ---@nodiscard
---@return boolean|nil formed true if formed, false if not, nil if unknown
function public.is_formed() return self.formed end function public.is_formed() return self.formed end
---@nodiscard ---@nodiscard
function public.is_force_disabled() return self.force_disabled end function public.is_force_disabled() return self.force_disabled end
@ -502,14 +495,14 @@ function plc.rps_init(reactor, is_formed)
end end
-- partial RPS reset that only clears fault and sys_fail -- partial RPS reset that only clears fault and sys_fail
function public.reset_reattach() function public.reset_formed()
self.tripped = false self.tripped = false
self.trip_cause = RPS_TRIP_CAUSE.OK self.trip_cause = RPS_TRIP_CAUSE.OK
self.state[CHK.FAULT] = false self.state[CHK.FAULT] = false
self.state[CHK.SYS_FAIL] = false self.state[CHK.SYS_FAIL] = false
log.info("RPS: partial reset on connected or formed") log.info("RPS: partial reset on formed")
end end
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
@ -591,7 +584,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- dynamic reactor status information, excluding heating rate -- dynamic reactor status information, excluding heating rate
---@return table data_table, boolean faulted ---@return table data_table, boolean faulted
local function _get_reactor_status() local function _get_reactor_status()
local fuel, waste, coolant, hcoolant = nil, nil, nil, nil local fuel = nil
local waste = nil
local coolant = nil
local hcoolant = nil
local data_table = {} local data_table = {}
reactor.__p_disable_afc() reactor.__p_disable_afc()
@ -710,112 +707,6 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
reactor.__p_enable_afc() reactor.__p_enable_afc()
end end
-- handle a burn rate command
---@param packet rplc_frame
---@param setpoints plc_setpoints
--- EVENT_CONSUMER: this function consumes events
local function _handle_burn_rate(packet, setpoints)
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
reactor.setBurnRate(burn_rate)
success = reactor.__p_is_ok()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
end
-- handle an auto burn rate command
---@param packet rplc_frame
---@param setpoints plc_setpoints
--- EVENT_CONSUMER: this function consumes events
local function _handle_auto_burn_rate(packet, setpoints)
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 100) / 100
local ramp = packet.data[2]
self.auto_ack_token = packet.data[3]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.01 then
if rps.is_active() then
-- auto scram to disable
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
else
log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
log.debug("AUTO: activating the reactor")
reactor.setBurnRate(0.01)
if reactor.__p_is_faulted() then
log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
log.warning("AUTO: automatic reactor activation failed")
end
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
reactor.setBurnRate(burn_rate)
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class plc_comms ---@class plc_comms
@ -857,8 +748,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
---@param formed boolean reactor formed (from PLC state) ---@param formed boolean reactor formed (from PLC state)
function public.send_status(no_reactor, formed) function public.send_status(no_reactor, formed)
if self.linked then if self.linked then
local mek_data = nil ---@type table local mek_data = nil ---@type table
local heating_rate = 0.0 ---@type number local heating_rate = 0.0 ---@type number
if (not no_reactor) and rps.is_formed() then if (not no_reactor) and rps.is_formed() then
if _update_status_cache() then mek_data = self.status_cache end if _update_status_cache() then mek_data = self.status_cache end
@ -912,11 +803,15 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- get as RPLC packet -- get as RPLC packet
if s_pkt.protocol() == PROTOCOL.RPLC then if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get()
end
-- get as SCADA management packet -- get as SCADA management packet
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then pkt = mgmt_pkt.get() end if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
else else
log.debug("unsupported packet type " .. s_pkt.protocol(), true) log.debug("unsupported packet type " .. s_pkt.protocol(), true)
end end
@ -928,13 +823,16 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
-- handle RPLC and MGMT packets -- handle RPLC and MGMT packets
---@param packet rplc_frame|mgmt_frame packet frame ---@param packet rplc_frame|mgmt_frame packet frame
---@param plc_state plc_state PLC state ---@param plc_state plc_state PLC state
---@param setpoints plc_setpoints setpoint control table ---@param setpoints setpoints setpoint control table
---@param println_ts function console print, when UI isn't running function public.handle_packet(packet, plc_state, setpoints)
function public.handle_packet(packet, plc_state, setpoints, println_ts) -- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end
local protocol = packet.scada_frame.protocol() local protocol = packet.scada_frame.protocol()
local l_chan = packet.scada_frame.local_channel() local l_chan = packet.scada_frame.local_channel()
local src_addr = packet.scada_frame.src_addr() local src_addr = packet.scada_frame.src_addr()
-- handle packets now that we have prints setup
if l_chan == config.PLC_Channel then if l_chan == config.PLC_Channel then
-- check sequence number -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
@ -969,7 +867,36 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
log.debug("sent out structure again, did supervisor miss it?") log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
-- set the burn rate -- set the burn rate
_handle_burn_rate(packet, setpoints) if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate > 0 and burn_rate <= self.max_burn_rate then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
success = true
else
reactor.setBurnRate(burn_rate)
success = reactor.__p_is_ok()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
elseif packet.type == RPLC_TYPE.RPS_ENABLE then elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor -- enable the reactor
self.scrammed = false self.scrammed = false
@ -998,7 +925,68 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
_send_ack(packet.type, true) _send_ack(packet.type, true)
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
-- automatic control requested a new burn rate -- automatic control requested a new burn rate
_handle_auto_burn_rate(packet, setpoints) if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 100) / 100
local ramp = packet.data[2]
self.auto_ack_token = packet.data[3]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.01 then
if rps.is_active() then
-- auto scram to disable
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
else
log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
log.debug("AUTO: activating the reactor")
reactor.setBurnRate(0.01)
if reactor.__p_is_faulted() then
log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
log.warning("AUTO: automatic reactor activation failed")
end
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
log.debug(util.c("AUTO: setting burn rate ramp to ", burn_rate))
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
reactor.setBurnRate(burn_rate)
ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(util.c(burn_rate, " rate outside of 0 < x <= ", self.max_burn_rate))
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
else else
log.debug("received unknown RPLC packet type " .. packet.type) log.debug("received unknown RPLC packet type " .. packet.type)
end end

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.9.0" 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
@ -87,6 +87,7 @@ local function main()
-- PLC system state flags -- PLC system state flags
---@class plc_state ---@class plc_state
plc_state = { plc_state = {
init_ok = true,
fp_ok = false, fp_ok = false,
shutdown = false, shutdown = false,
degraded = true, degraded = true,
@ -96,22 +97,19 @@ local function main()
}, },
-- control setpoints -- control setpoints
---@class plc_setpoints ---@class setpoints
setpoints = { setpoints = {
burn_rate_en = false, burn_rate_en = false,
burn_rate = 0.0 burn_rate = 0.0
}, },
-- core PLC devices -- core PLC devices
---@class plc_dev
plc_dev = { plc_dev = {
---@diagnostic disable-next-line: assign-type-mismatch reactor = ppm.get_fission_reactor(),
reactor = ppm.get_fission_reactor(), ---@type table
modem = ppm.get_wireless_modem() modem = ppm.get_wireless_modem()
}, },
-- system objects -- system objects
---@class plc_sys
plc_sys = { plc_sys = {
rps = nil, ---@type rps rps = nil, ---@type rps
nic = nil, ---@type nic nic = nil, ---@type nic
@ -138,20 +136,14 @@ local function main()
-- we need a reactor, can at least do some things even if it isn't formed though -- we need a reactor, can at least do some things even if it isn't formed though
if plc_state.no_reactor then if plc_state.no_reactor then
println("startup> fission reactor not found") println("init> fission reactor not found")
log.warning("startup> no reactor on startup") log.warning("init> no reactor on startup")
plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
plc_state.reactor_formed = false
-- mount a virtual peripheral to init the RPS with
local _, dev = ppm.mount_virtual()
smem_dev.reactor = dev
log.info("startup> mounted virtual device as reactor")
elseif not smem_dev.reactor.isFormed() then elseif not smem_dev.reactor.isFormed() then
println("startup> fission reactor is not formed") println("init> fission reactor is not formed")
log.warning("startup> reactor logic adapter present, but reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true plc_state.degraded = true
plc_state.reactor_formed = false plc_state.reactor_formed = false
@ -159,74 +151,89 @@ local function main()
-- modem is required if networked -- modem is required if networked
if __shared_memory.networked and plc_state.no_modem then if __shared_memory.networked and plc_state.no_modem then
println("startup> wireless modem not found") println("init> wireless modem not found")
log.warning("startup> no wireless modem on startup") log.warning("init> no wireless modem on startup")
-- scram reactor if present and enabled -- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram() smem_dev.reactor.scram()
end end
plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
end end
-- scram on boot if networked, otherwise leave the reactor be
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
log.debug("startup> power-on SCRAM")
smem_dev.reactor.scram()
end
-- setup front panel
local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-- ...or not
if not plc_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
-- 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_no_fp(msg) if not plc_state.fp_ok then println(msg) end end local function _println_no_fp(message) if not plc_state.fp_ok then println(message) end end
-- PLC init<br>
--- EVENT_CONSUMER: this function consumes events
local function init()
-- scram on boot if networked, otherwise leave the reactor be
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram()
end
-- setup front panel
if not renderer.ui_ready() then
local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)
-- ...or not
if not plc_state.fp_ok then
println_ts(util.c("UI error: ", message))
println("init> running without front panel")
log.error(util.c("front panel GUI render failed with error ", message))
log.info("init> running in headless mode without front panel")
end
end
if plc_state.init_ok then
-- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, plc_state.reactor_formed)
log.debug("init> rps init")
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("init> conn watchdog started")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init")
else
_println_no_fp("init> starting in offline mode")
log.info("init> running without networking")
end
-- notify user of emergency coolant configuration status
if config.EmerCoolEnable then
println("init> emergency coolant control ready")
log.info("init> running with emergency coolant control available")
end
util.push_event("clock_start")
_println_no_fp("init> completed")
log.info("init> startup completed")
else
_println_no_fp("init> system in degraded state, awaiting devices...")
log.warning("init> started in a degraded state, awaiting peripheral connections...")
end
databus.tx_hw_status(plc_state)
end
---------------------------------------- ----------------------------------------
-- start system
----------------------------------------
-- initialize PLC -- initialize PLC
---------------------------------------- init()
-- init reactor protection system
smem_sys.rps = plc.rps_init(smem_dev.reactor, util.trinary(plc_state.no_reactor, nil, plc_state.reactor_formed))
log.debug("startup> rps init")
-- notify user of emergency coolant configuration status
if config.EmerCoolEnable then
_println_no_fp("startup> emergency coolant control ready")
log.info("startup> emergency coolant control available")
end
-- conditionally init comms
if __shared_memory.networked then
-- comms watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.ConnTimeout)
log.debug("startup> conn watchdog started")
-- create network interface then setup comms
smem_sys.nic = network.nic(smem_dev.modem)
smem_sys.plc_comms = plc.comms(R_PLC_VERSION, smem_sys.nic, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("startup> comms init")
else
_println_no_fp("startup> starting in non-networked mode")
log.info("startup> starting without networking")
end
databus.tx_hw_status(plc_state)
_println_no_fp("startup> completed")
log.info("startup> completed")
-- init threads -- init threads
local main_thread = threads.thread__main(__shared_memory) local main_thread = threads.thread__main(__shared_memory, init)
local rps_thread = threads.thread__rps(__shared_memory) local rps_thread = threads.thread__rps(__shared_memory)
if __shared_memory.networked then if __shared_memory.networked then
@ -240,12 +247,14 @@ local function main()
-- run threads -- run threads
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec) parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec)
-- send status one last time after RPS shutdown if plc_state.init_ok then
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed) -- send status one last time after RPS shutdown
smem_sys.plc_comms.send_rps_status() smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
smem_sys.plc_comms.send_rps_status()
-- close connection -- close connection
smem_sys.plc_comms.close() smem_sys.plc_comms.close()
end
else else
-- run threads, excluding comms -- run threads, excluding comms
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)

View File

@ -31,7 +31,8 @@ local MQ__COMM_CMD = {
-- main thread -- main thread
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__main(smem) ---@param init function
function threads.thread__main(smem, init)
-- 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_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
@ -41,7 +42,7 @@ function threads.thread__main(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("main", true) databus.tx_rt_status("main", true)
log.debug("OS: main thread start") log.debug("main thread init, clock inactive")
-- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- send status updates at 2Hz (every 10 server ticks) (every loop tick)
-- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks) -- send link requests at 0.5Hz (every 40 server ticks) (every 8 loop ticks)
@ -54,9 +55,6 @@ function threads.thread__main(smem)
local plc_state = smem.plc_state local plc_state = smem.plc_state
local plc_dev = smem.plc_dev local plc_dev = smem.plc_dev
-- start clock
loop_clock.start()
-- event loop -- event loop
while true do while true do
-- get plc_sys fields (may have been set late due to degraded boot) -- get plc_sys fields (may have been set late due to degraded boot)
@ -69,6 +67,7 @@ function threads.thread__main(smem)
-- handle event -- handle event
if event == "timer" and loop_clock.is_clock(param1) then if event == "timer" and loop_clock.is_clock(param1) then
-- note: loop clock is only running if init_ok = true
-- blink heartbeat indicator -- blink heartbeat indicator
databus.heartbeat() databus.heartbeat()
@ -94,7 +93,7 @@ function threads.thread__main(smem)
-- reactor now formed -- reactor now formed
plc_state.reactor_formed = true plc_state.reactor_formed = true
println_ts("reactor is now formed") println_ts("reactor is now formed.")
log.info("reactor is now formed") log.info("reactor is now formed")
-- SCRAM newly formed reactor -- SCRAM newly formed reactor
@ -107,10 +106,10 @@ function threads.thread__main(smem)
-- partial reset of RPS, specific to becoming formed -- partial reset of RPS, specific to becoming formed
-- without this, auto control can't resume on chunk load -- without this, auto control can't resume on chunk load
rps.reset_reattach() rps.reset_formed()
elseif plc_state.reactor_formed and (rps.is_formed() == false) then elseif plc_state.reactor_formed and not rps.is_formed() then
-- reactor no longer formed -- reactor no longer formed
println_ts("reactor is no longer formed") println_ts("reactor is no longer formed.")
log.info("reactor is no longer formed") log.info("reactor is no longer formed")
plc_state.reactor_formed = false plc_state.reactor_formed = false
@ -119,14 +118,14 @@ function threads.thread__main(smem)
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
elseif event == "modem_message" and networked and nic.is_connected() then elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then
-- got a packet -- got a packet
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
if packet ~= nil then if packet ~= nil then
-- pass the packet onto the comms message queue -- pass the packet onto the comms message queue
smem.q.mq_comms_rx.push_packet(packet) smem.q.mq_comms_rx.push_packet(packet)
end end
elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then
-- haven't heard from server recently? close connection and shutdown reactor -- haven't heard from server recently? close connection and shutdown reactor
plc_comms.close() plc_comms.close()
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
@ -147,7 +146,8 @@ function threads.thread__main(smem)
elseif networked and type == "modem" then elseif networked and type == "modem" then
---@cast device Modem ---@cast device Modem
-- we only care if this is our wireless modem -- we only care if this is our wireless modem
if nic.is_modem(device) then -- note, check init_ok first since nic will be nil if it is false
if plc_state.init_ok and nic.is_modem(device) then
nic.disconnect() nic.disconnect()
println_ts("comms modem disconnected!") println_ts("comms modem disconnected!")
@ -161,8 +161,10 @@ function threads.thread__main(smem)
plc_state.no_modem = true plc_state.no_modem = true
plc_state.degraded = true plc_state.degraded = true
-- try to scram reactor if it is still connected if plc_state.init_ok then
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) -- try to scram reactor if it is still connected
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
end
end end
else else
log.warning("a modem was disconnected") log.warning("a modem was disconnected")
@ -182,7 +184,7 @@ function threads.thread__main(smem)
plc_dev.reactor = device plc_dev.reactor = device
plc_state.no_reactor = false plc_state.no_reactor = false
println_ts("reactor reconnected") println_ts("reactor reconnected.")
log.info("reactor reconnected") log.info("reactor reconnected")
-- we need to assume formed here as we cannot check in this main loop -- we need to assume formed here as we cannot check in this main loop
@ -194,52 +196,64 @@ function threads.thread__main(smem)
plc_state.degraded = false plc_state.degraded = false
end end
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) if plc_state.init_ok then
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
rps.reconnect_reactor(plc_dev.reactor) rps.reconnect_reactor(plc_dev.reactor)
if networked then if networked then
plc_comms.reconnect_reactor(plc_dev.reactor) plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_formed()
end end
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_reattach()
elseif networked and type == "modem" then elseif networked and type == "modem" then
---@cast device Modem ---@cast device Modem
-- note, check init_ok first since nic will be nil if it is false -- note, check init_ok first since nic will be nil if it is false
if device.isWireless() and not nic.is_connected() then if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
-- reconnected modem -- reconnected modem
plc_dev.modem = device plc_dev.modem = device
plc_state.no_modem = false plc_state.no_modem = false
nic.connect(device) if plc_state.init_ok then nic.connect(device) end
println_ts("comms modem reconnected") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected") log.info("comms modem reconnected")
-- determine if we are still in a degraded state -- determine if we are still in a degraded state
if plc_state.reactor_formed and not plc_state.no_reactor then if not plc_state.no_reactor then
plc_state.degraded = false plc_state.degraded = false
end end
elseif device.isWireless() then elseif device.isWireless() then
log.info("unused wireless modem connected") log.info("unused wireless modem reconnected")
else else
log.info("wired modem connected") log.info("wired modem reconnected")
end end
end end
end end
-- if not init'd and no longer degraded, proceed to init
if not plc_state.init_ok and not plc_state.degraded then
plc_state.init_ok = true
init()
end
-- update indicators -- update indicators
databus.tx_hw_status(plc_state) databus.tx_hw_status(plc_state)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
event == "double_click" then event == "double_click" then
-- handle a mouse event -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "clock_start" then
-- start loop clock
loop_clock.start()
log.debug("main thread clock started")
end end
-- check for termination request -- check for termination request
if event == "terminate" or ppm.should_terminate() then if event == "terminate" or ppm.should_terminate() then
log.info("OS: terminate requested, main thread exiting") log.info("terminate requested, main thread exiting")
-- rps handles reactor shutdown -- rps handles reactor shutdown
plc_state.shutdown = true plc_state.shutdown = true
break break
@ -263,7 +277,8 @@ function threads.thread__main(smem)
-- if not, we need to restart the clock -- if not, we need to restart the clock
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("OS: main thread restarting now...") log.info("main thread restarting now...")
util.push_event("clock_start")
end end
end end
end end
@ -284,7 +299,7 @@ function threads.thread__rps(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("rps", true) databus.tx_rt_status("rps", true)
log.debug("OS: rps thread start") log.debug("rps thread start")
-- load in from shared memory -- load in from shared memory
local networked = smem.networked local networked = smem.networked
@ -301,36 +316,49 @@ function threads.thread__rps(smem)
-- get plc_sys fields (may have been set late due to degraded boot) -- get plc_sys fields (may have been set late due to degraded boot)
local rps = smem.plc_sys.rps local rps = smem.plc_sys.rps
local plc_comms = smem.plc_sys.plc_comms local plc_comms = smem.plc_sys.plc_comms
-- get reactor, it may have changed due to a disconnect/reconnect -- get reactor, may have changed do to disconnect/reconnect
local reactor = plc_dev.reactor local reactor = plc_dev.reactor
-- SCRAM if no open connection -- RPS checks
if networked and not plc_comms.is_linked() then if plc_state.init_ok then
if was_linked then -- SCRAM if no open connection
was_linked = false if networked and not plc_comms.is_linked() then
rps.trip_timeout() if was_linked then
was_linked = false
rps.trip_timeout()
end
else
was_linked = true
end end
else was_linked = true end
-- check reactor status if (not plc_state.no_reactor) and rps.is_formed() then
if (not plc_state.no_reactor) and rps.is_formed() then -- check reactor status
local reactor_status = reactor.getStatus() ---@diagnostic disable-next-line: need-check-nil
databus.tx_reactor_state(reactor_status) local reactor_status = reactor.getStatus()
databus.tx_reactor_state(reactor_status)
-- if we tried to SCRAM but failed, keep trying -- if we tried to SCRAM but failed, keep trying
-- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check)
if rps.is_tripped() and reactor_status then rps.scram() end if rps.is_tripped() and reactor_status then
end rps.scram()
end
end
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS -- if we are in standalone mode and the front panel isn't working, continuously reset RPS
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
-- check safety (SCRAM occurs if tripped) -- check safety (SCRAM occurs if tripped)
local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor) if not plc_state.no_reactor then
if rps_tripped and rps_first then local rps_tripped, rps_status_string, rps_first = rps.check()
println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")")
if networked then plc_comms.send_rps_alarm(rps_status_string) end if rps_tripped and rps_first then
println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string)
if networked and not plc_state.no_modem then
plc_comms.send_rps_alarm(rps_status_string)
end
end
end
end end
-- check for messages in the message queue -- check for messages in the message queue
@ -340,19 +368,19 @@ function threads.thread__rps(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__RPS_CMD.SCRAM then if plc_state.init_ok then
-- SCRAM if msg.message == MQ__RPS_CMD.SCRAM then
log.info("RPS: OS requested SCRAM") -- SCRAM
rps.scram() rps.scram()
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
-- lost peripheral(s) -- lost peripheral(s)
log.info("RPS: received PLC degraded alert") rps.trip_fault()
rps.trip_fault() elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then -- watchdog tripped
-- watchdog tripped rps.trip_timeout()
println_ts("RPS: supervisor timeout") println_ts("server timeout")
log.warning("RPS: received supervisor timeout alert") log.warning("server timeout")
rps.trip_timeout() end
end end
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
-- received data -- received data
@ -368,17 +396,17 @@ function threads.thread__rps(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
-- safe exit -- safe exit
log.info("OS: rps thread shutdown initiated") log.info("rps thread shutdown initiated")
if plc_state.init_ok then
if rps.scram() then if rps.scram() then
println_ts("exiting, reactor disabled") println_ts("reactor disabled")
log.info("OS: rps thread reactor SCRAM OK on exit") log.info("rps thread reactor SCRAM OK")
else else
println_ts("exiting, reactor failed to disable") println_ts("exiting, reactor failed to disable")
log.error("OS: rps thread failed to SCRAM reactor on exit") log.error("rps thread failed to SCRAM reactor on exit")
end
end end
log.info("rps thread exiting")
log.info("OS: rps thread exiting")
break break
end end
@ -400,8 +428,8 @@ function threads.thread__rps(smem)
databus.tx_rt_status("rps", false) databus.tx_rt_status("rps", false)
if not plc_state.shutdown then if not plc_state.shutdown then
smem.plc_sys.rps.scram() if plc_state.init_ok then smem.plc_sys.rps.scram() end
log.info("OS: rps thread restarting in 5 seconds...") log.info("rps thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -420,7 +448,7 @@ function threads.thread__comms_tx(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_tx", true) databus.tx_rt_status("comms_tx", true)
log.debug("OS: comms tx thread start") log.debug("comms tx thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state local plc_state = smem.plc_state
@ -437,7 +465,7 @@ function threads.thread__comms_tx(smem)
while comms_queue.ready() and not plc_state.shutdown do while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop() local msg = comms_queue.pop()
if msg ~= nil then if msg ~= nil and plc_state.init_ok 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__COMM_CMD.SEND_STATUS then if msg.message == MQ__COMM_CMD.SEND_STATUS then
@ -458,7 +486,7 @@ function threads.thread__comms_tx(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("OS: comms tx thread exiting") log.info("comms tx thread exiting")
break break
end end
@ -480,7 +508,7 @@ function threads.thread__comms_tx(smem)
databus.tx_rt_status("comms_tx", false) databus.tx_rt_status("comms_tx", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("OS: comms tx thread restarting in 5 seconds...") log.info("comms tx thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -493,16 +521,13 @@ end
---@nodiscard ---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__comms_rx(smem) function threads.thread__comms_rx(smem)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not smem.plc_state.fp_ok then util.println_ts(message) end end
---@class parallel_thread ---@class parallel_thread
local public = {} local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("comms_rx", true) databus.tx_rt_status("comms_rx", true)
log.debug("OS: comms rx thread start") log.debug("comms rx thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state local plc_state = smem.plc_state
@ -521,7 +546,7 @@ function threads.thread__comms_rx(smem)
while comms_queue.ready() and not plc_state.shutdown do while comms_queue.ready() and not plc_state.shutdown do
local msg = comms_queue.pop() local msg = comms_queue.pop()
if msg ~= nil then if msg ~= nil and plc_state.init_ok then
if msg.qtype == mqueue.TYPE.COMMAND then if msg.qtype == mqueue.TYPE.COMMAND then
-- received a command -- received a command
elseif msg.qtype == mqueue.TYPE.DATA then elseif msg.qtype == mqueue.TYPE.DATA then
@ -530,7 +555,7 @@ function threads.thread__comms_rx(smem)
-- received a packet -- received a packet
-- handle the packet (setpoints passed to update burn rate setpoint) -- handle the packet (setpoints passed to update burn rate setpoint)
-- (plc_state passed to check if degraded) -- (plc_state passed to check if degraded)
plc_comms.handle_packet(msg.message, plc_state, setpoints, println_ts) plc_comms.handle_packet(msg.message, plc_state, setpoints)
end end
end end
@ -540,7 +565,7 @@ function threads.thread__comms_rx(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("OS: comms rx thread exiting") log.info("comms rx thread exiting")
break break
end end
@ -562,7 +587,7 @@ function threads.thread__comms_rx(smem)
databus.tx_rt_status("comms_rx", false) databus.tx_rt_status("comms_rx", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("OS: comms rx thread restarting in 5 seconds...") log.info("comms rx thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end
@ -581,7 +606,7 @@ function threads.thread__setpoint_control(smem)
-- execute thread -- execute thread
function public.exec() function public.exec()
databus.tx_rt_status("spctl", true) databus.tx_rt_status("spctl", true)
log.debug("OS: setpoint control thread start") log.debug("setpoint control thread start")
-- load in from shared memory -- load in from shared memory
local plc_state = smem.plc_state local plc_state = smem.plc_state
@ -604,7 +629,9 @@ function threads.thread__setpoint_control(smem)
-- get reactor, may have changed do to disconnect/reconnect -- get reactor, may have changed do to disconnect/reconnect
local reactor = plc_dev.reactor local reactor = plc_dev.reactor
if not plc_state.no_reactor then if plc_state.init_ok and (not plc_state.no_reactor) then
---@cast reactor table won't be nil
-- check if we should start ramping -- check if we should start ramping
if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then
local cur_burn_rate = reactor.getBurnRate() local cur_burn_rate = reactor.getBurnRate()
@ -671,7 +698,7 @@ function threads.thread__setpoint_control(smem)
-- check for termination request -- check for termination request
if plc_state.shutdown then if plc_state.shutdown then
log.info("OS: setpoint control thread exiting") log.info("setpoint control thread exiting")
break break
end end
@ -693,7 +720,7 @@ function threads.thread__setpoint_control(smem)
databus.tx_rt_status("spctl", false) databus.tx_rt_status("spctl", false)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("OS: setpoint control thread restarting in 5 seconds...") log.info("setpoint control thread restarting in 5 seconds...")
util.psleep(5) util.psleep(5)
end end
end end

View File

@ -205,7 +205,7 @@ function comms.scada_packet()
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("COMMS: comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 5 then if #self.raw == 5 then
@ -326,7 +326,7 @@ function comms.authd_packet()
if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then if (type(max_distance) == "number") and ((type(distance) ~= "number") or (distance > max_distance)) then
-- outside of maximum allowable transmission distance -- outside of maximum allowable transmission distance
-- log.debug("COMMS: comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)") -- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
else else
if type(self.raw) == "table" then if type(self.raw) == "table" then
if #self.raw == 4 then if #self.raw == 4 then
@ -412,7 +412,7 @@ function comms.modbus_packet()
self.raw = { self.txn_id, self.unit_id, self.func_code } self.raw = { self.txn_id, self.unit_id, self.func_code }
for i = 1, self.length do insert(self.raw, data[i]) end for i = 1, self.length do insert(self.raw, data[i]) end
else else
log.error("COMMS: modbus_packet.make(): data not a table") log.error("comms.modbus_packet.make(): data not a table")
end end
end end
@ -435,11 +435,11 @@ function comms.modbus_packet()
return size_ok and valid return size_ok and valid
else else
log.debug("COMMS: attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("COMMS: nil frame encountered", true) log.debug("nil frame encountered", true)
return false return false
end end
end end
@ -498,7 +498,7 @@ function comms.rplc_packet()
self.raw = { self.id, self.type } self.raw = { self.id, self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("COMMS: rplc_packet.make(): data not a table") log.error("comms.rplc_packet.make(): data not a table")
end end
end end
@ -521,11 +521,11 @@ function comms.rplc_packet()
return ok return ok
else else
log.debug("COMMS: attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("COMMS: nil frame encountered", true) log.debug("nil frame encountered", true)
return false return false
end end
end end
@ -580,7 +580,7 @@ function comms.mgmt_packet()
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("COMMS: mgmt_packet.make(): data not a table") log.error("comms.mgmt_packet.make(): data not a table")
end end
end end
@ -601,11 +601,11 @@ function comms.mgmt_packet()
return ok return ok
else else
log.debug("COMMS: attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("COMMS: nil frame encountered", true) log.debug("nil frame encountered", true)
return false return false
end end
end end
@ -659,7 +659,7 @@ function comms.crdn_packet()
self.raw = { self.type } self.raw = { self.type }
for i = 1, #data do insert(self.raw, data[i]) end for i = 1, #data do insert(self.raw, data[i]) end
else else
log.error("COMMS: crdn_packet.make(): data not a table") log.error("comms.crdn_packet.make(): data not a table")
end end
end end
@ -680,11 +680,11 @@ function comms.crdn_packet()
return ok return ok
else else
log.debug("COMMS: attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true) log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
return false return false
end end
else else
log.debug("COMMS: nil frame encountered", true) log.debug("nil frame encountered", true)
return false return false
end end
end end

View File

@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE log.MODE = MODE
local _log = { local logger = {
not_ready = true, not_ready = true,
path = "/log.txt", path = "/log.txt",
mode = MODE.APPEND, mode = MODE.APPEND,
@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace
---@param err_msg string|nil error message ---@param err_msg string|nil error message
---@return boolean out_of_space ---@return boolean out_of_space
local function check_out_of_space(err_msg) local function check_out_of_space(err_msg)
return (free_space(_log.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil)) return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
end end
-- private log write function -- private log write function
---@param msg_bits any[] ---@param msg_bits any[]
local function write_log(msg_bits) local function _log(msg_bits)
if _log.not_ready then return end if logger.not_ready then return end
local time_stamp = os.date(TIME_FMT) local time_stamp = os.date(TIME_FMT)
local stamped = util.c(time_stamp, table.unpack(msg_bits)) local stamped = util.c(time_stamp, table.unpack(msg_bits))
-- attempt to write log -- attempt to write log
local status, result = pcall(function () local status, result = pcall(function ()
_log.file.writeLine(stamped) logger.file.writeLine(stamped)
_log.file.flush() logger.file.flush()
end) end)
-- if we don't have space, we need to create a new log file -- if we don't have space, we need to create a new log file
if check_out_of_space() then if check_out_of_space() then
-- delete the old log file before opening a new one -- delete the old log file before opening a new one
_log.file.close() logger.file.close()
fs.delete(_log.path) fs.delete(logger.path)
-- re-init logger and pass dmesg_out so that it doesn't change -- re-init logger and pass dmesg_out so that it doesn't change
log.init(_log.path, _log.mode, _log.debug, _log.dmesg_out) log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
-- log the message and recycle warning -- log the message and recycle warning
_log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file") logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
_log.file.writeLine(stamped) logger.file.writeLine(stamped)
_log.file.flush() logger.file.flush()
elseif (not status) and (result ~= nil) then elseif (not status) and (result ~= nil) then
util.println("unexpected error writing to the log file: " .. result) util.println("unexpected error writing to the log file: " .. result)
end end
@ -89,45 +89,45 @@ end
function log.init(path, write_mode, include_debug, dmesg_redirect) function log.init(path, write_mode, include_debug, dmesg_redirect)
local err_msg local err_msg
_log.path = path logger.path = path
_log.mode = write_mode logger.mode = write_mode
_log.debug = include_debug logger.debug = include_debug
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w")) logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
if dmesg_redirect then if dmesg_redirect then
_log.dmesg_out = dmesg_redirect logger.dmesg_out = dmesg_redirect
else else
_log.dmesg_out = term.current() logger.dmesg_out = term.current()
end end
-- check for space issues -- check for space issues
local out_of_space = check_out_of_space(err_msg) local out_of_space = check_out_of_space(err_msg)
-- try to handle problems -- try to handle problems
if _log.file == nil or out_of_space then if logger.file == nil or out_of_space then
if out_of_space then if out_of_space then
if fs.exists(_log.path) then if fs.exists(logger.path) then
fs.delete(_log.path) fs.delete(logger.path)
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w")) logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
if _log.file then if logger.file then
_log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file") logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
_log.file.flush() logger.file.flush()
else error("failed to setup the log file: " .. err_msg) end else error("failed to setup the log file: " .. err_msg) end
else error("failed to make space for the log file, please delete unused files") end else error("failed to make space for the log file, please delete unused files") end
else error("unexpected error setting up the log file: " .. err_msg) end else error("unexpected error setting up the log file: " .. err_msg) end
end end
_log.not_ready = false logger.not_ready = false
end end
-- close the log file handle -- close the log file handle
function log.close() _log.file.close() end function log.close() logger.file.close() end
-- direct dmesg output to a monitor/window -- direct dmesg output to a monitor/window
---@param window Window window or terminal reference ---@param window Window window or terminal reference
function log.direct_dmesg(window) _log.dmesg_out = window end function log.direct_dmesg(window) logger.dmesg_out = window end
-- dmesg style logging for boot because I like linux-y things -- dmesg style logging for boot because I like linux-y things
---@param msg any message ---@param msg any message
@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color)
tag = util.strval(tag or "") tag = util.strval(tag or "")
local t_stamp = string.format("%12.2f", os.clock()) local t_stamp = string.format("%12.2f", os.clock())
local out = _log.dmesg_out local out = logger.dmesg_out
if out ~= nil then if out ~= nil then
local out_w, out_h = out.getSize() local out_w, out_h = out.getSize()
@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1 logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color)
if cur_y == out_h then if cur_y == out_h then
out.scroll(1) out.scroll(1)
out.setCursorPos(1, cur_y) out.setCursorPos(1, cur_y)
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1 logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
else else
out.setCursorPos(1, cur_y + 1) out.setCursorPos(1, cur_y + 1)
end end
@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color)
out.write(lines[i]) out.write(lines[i])
end end
_log.dmesg_restore_coord = { out.getCursorPos() } logger.dmesg_restore_coord = { out.getCursorPos() }
write_log{"[", t_stamp, "] [", tag, "] ", msg} _log{"[", t_stamp, "] [", tag, "] ", msg}
end end
return ts_coord return ts_coord
@ -241,9 +241,9 @@ end
---@return function update, function done ---@return function update, function done
function log.dmesg_working(msg, tag, tag_color) function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color) local ts_coord = log.dmesg(msg, tag, tag_color)
local initial_scroll = _log.dmesg_scroll_count local initial_scroll = logger.dmesg_scroll_count
local out = _log.dmesg_out local out = logger.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1 local width = (ts_coord.x2 - ts_coord.x1) + 1
if out ~= nil then if out ~= nil then
@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color)
local counter = 0 local counter = 0
local function update(sec_remaining) local function update(sec_remaining)
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll) local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end if new_y < 1 then return end
local time = util.sprintf("%ds", sec_remaining) local time = util.sprintf("%ds", sec_remaining)
@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color)
counter = counter + 1 counter = counter + 1
out.setCursorPos(table.unpack(_log.dmesg_restore_coord)) out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
end end
local function done(ok) local function done(ok)
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll) local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
if new_y < 1 then return end if new_y < 1 then return end
out.setCursorPos(ts_coord.x1, new_y) out.setCursorPos(ts_coord.x1, new_y)
@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color)
out.setTextColor(initial_color) out.setTextColor(initial_color)
out.setCursorPos(table.unpack(_log.dmesg_restore_coord)) out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
end end
return update, done return update, done
@ -312,28 +312,28 @@ end
---@param msg any message ---@param msg any message
---@param trace? boolean include file trace ---@param trace? boolean include file trace
function log.debug(msg, trace) function log.debug(msg, trace)
if _log.debug then if logger.debug then
if trace then if trace then
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.name ~= nil then if info.name ~= nil then
write_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} _log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
else else
write_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg} _log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
end end
else else
write_log{DBG_TAG, msg} _log{DBG_TAG, msg}
end end
end end
end end
-- log info messages -- log info messages
---@param msg any message ---@param msg any message
function log.info(msg) write_log{INF_TAG, msg} end function log.info(msg) _log{INF_TAG, msg} end
-- log warning messages -- log warning messages
---@param msg any message ---@param msg any message
function log.warning(msg) write_log{WRN_TAG, msg} end function log.warning(msg) _log{WRN_TAG, msg} end
-- log error messages -- log error messages
---@param msg any message ---@param msg any message
@ -343,17 +343,17 @@ function log.error(msg, trace)
local info = debug.getinfo(2) local info = debug.getinfo(2)
if info.name ~= nil then if info.name ~= nil then
write_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg} _log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
else else
write_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg} _log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
end end
else else
write_log{ERR_TAG, msg} _log{ERR_TAG, msg}
end end
end end
-- log fatal errors -- log fatal errors
---@param msg any message ---@param msg any message
function log.fatal(msg) write_log{FTL_TAG, msg} end function log.fatal(msg) _log{FTL_TAG, msg} end
return log return log

View File

@ -1,10 +1,9 @@
-- --
-- Network Communications and Message Authentication -- Network Communications
-- --
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 ppm = require("scada-common.ppm")
local util = require("scada-common.util") local util = require("scada-common.util")
local md5 = require("lockbox.digest.md5") local md5 = require("lockbox.digest.md5")
@ -18,7 +17,7 @@ local array = require("lockbox.util.array")
local network = {} local network = {}
-- cryptography engine -- cryptography engine
local _crypt = { local c_eng = {
key = nil, key = nil,
hmac = nil hmac = nil
} }
@ -40,23 +39,23 @@ function network.init_mac(passkey)
key_deriv.setPassword(passkey) key_deriv.setPassword(passkey)
key_deriv.finish() key_deriv.finish()
_crypt.key = array.fromHex(key_deriv.asHex()) c_eng.key = array.fromHex(key_deriv.asHex())
-- initialize HMAC -- initialize HMAC
_crypt.hmac = hmac() c_eng.hmac = hmac()
_crypt.hmac.setBlockSize(64) c_eng.hmac.setBlockSize(64)
_crypt.hmac.setDigest(md5) c_eng.hmac.setDigest(md5)
_crypt.hmac.setKey(_crypt.key) c_eng.hmac.setKey(c_eng.key)
local init_time = util.time_ms() - start local init_time = util.time_ms() - start
log.info("NET: network.init_mac completed in " .. init_time .. "ms") log.info("network.init_mac completed in " .. init_time .. "ms")
return init_time return init_time
end end
-- de-initialize message authentication system -- de-initialize message authentication system
function network.deinit_mac() function network.deinit_mac()
_crypt.key, _crypt.hmac = nil, nil c_eng.key, c_eng.hmac = nil, nil
end end
-- generate HMAC of message -- generate HMAC of message
@ -65,41 +64,29 @@ end
local function compute_hmac(message) local function compute_hmac(message)
-- local start = util.time_ms() -- local start = util.time_ms()
_crypt.hmac.init() c_eng.hmac.init()
_crypt.hmac.update(stream.fromString(message)) c_eng.hmac.update(stream.fromString(message))
_crypt.hmac.finish() c_eng.hmac.finish()
local hash = _crypt.hmac.asHex() local hash = c_eng.hmac.asHex()
-- log.debug("NET: compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)") -- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
return hash return hash
end end
-- NIC: Network Interface Controller<br> -- NIC: Network Interface Controller<br>
-- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless -- utilizes HMAC-MD5 for message authentication, if enabled
---@param modem Modem|nil modem to use ---@param modem Modem modem to use
function network.nic(modem) function network.nic(modem)
local self = { local self = {
-- modem interface name connected = true, -- used to avoid costly MAC calculations if modem isn't even present
iface = "?",
-- phy name
name = "?",
-- used to quickly return out of tx/rx functions if there is nothing to do
connected = false,
-- used to avoid costly MAC calculations if not required
use_hash = false,
-- open channels
channels = {} channels = {}
} }
---@class nic:Modem ---@class nic:Modem
local public = {} local public = {}
-- get the phy name
---@nodiscard
function public.phy_name() return self.name end
-- check if this NIC has a connected modem -- check if this NIC has a connected modem
---@nodiscard ---@nodiscard
function public.is_connected() return self.connected end function public.is_connected() return self.connected end
@ -108,14 +95,9 @@ function network.nic(modem)
---@param reconnected_modem Modem ---@param reconnected_modem Modem
function public.connect(reconnected_modem) function public.connect(reconnected_modem)
modem = reconnected_modem modem = reconnected_modem
self.iface = ppm.get_iface(modem)
self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}")
self.connected = true self.connected = true
self.use_hash = _crypt.hmac and modem.isWireless()
-- open only previously opened channels -- open previously opened channels
modem.closeAll()
for _, channel in ipairs(self.channels) do for _, channel in ipairs(self.channels) do
modem.open(channel) modem.open(channel)
end end
@ -135,13 +117,13 @@ function network.nic(modem)
function public.is_modem(device) return device == modem end function public.is_modem(device) return device == modem end
-- wrap modem functions, then create custom functions -- wrap modem functions, then create custom functions
if modem then public.connect(modem) end public.connect(modem)
-- open a channel on the modem<br> -- open a channel on the modem<br>
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection -- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
---@param channel integer ---@param channel integer
function public.open(channel) function public.open(channel)
if modem then modem.open(channel) end modem.open(channel)
local already_open = false local already_open = false
for i = 1, #self.channels do for i = 1, #self.channels do
@ -159,7 +141,7 @@ function network.nic(modem)
-- close a channel on the modem -- close a channel on the modem
---@param channel integer ---@param channel integer
function public.close(channel) function public.close(channel)
if modem then modem.close(channel) end modem.close(channel)
for i = 1, #self.channels do for i = 1, #self.channels do
if self.channels[i] == channel then if self.channels[i] == channel then
@ -171,7 +153,7 @@ function network.nic(modem)
-- close all channels on the modem -- close all channels on the modem
function public.closeAll() function public.closeAll()
if modem then modem.closeAll() end modem.closeAll()
self.channels = {} self.channels = {}
end end
@ -183,20 +165,17 @@ function network.nic(modem)
if self.connected then if self.connected then
local tx_packet = packet ---@type authd_packet|scada_packet local tx_packet = packet ---@type authd_packet|scada_packet
if self.use_hash then if c_eng.hmac ~= nil then
-- local start = util.time_ms() -- local start = util.time_ms()
tx_packet = comms.authd_packet() tx_packet = comms.authd_packet()
---@cast tx_packet authd_packet ---@cast tx_packet authd_packet
tx_packet.make(packet, compute_hmac) tx_packet.make(packet, compute_hmac)
-- log.debug("NET: network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") -- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
end end
---@diagnostic disable-next-line: need-check-nil
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable()) modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
else
log.debug("NET: network.transmit tx dropped, link is down")
end end
end end
@ -211,10 +190,10 @@ function network.nic(modem)
function public.receive(side, sender, reply_to, message, distance) function public.receive(side, sender, reply_to, message, distance)
local packet = nil local packet = nil
if self.connected and side == self.iface then if self.connected then
local s_packet = comms.scada_packet() local s_packet = comms.scada_packet()
if self.use_hash then if c_eng.hmac ~= nil then
-- parse packet as an authenticated SCADA packet -- parse packet as an authenticated SCADA packet
local a_packet = comms.authd_packet() local a_packet = comms.authd_packet()
a_packet.receive(side, sender, reply_to, message, distance) a_packet.receive(side, sender, reply_to, message, distance)
@ -227,10 +206,10 @@ function network.nic(modem)
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true })) local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
if a_packet.mac() == computed_hmac then if a_packet.mac() == computed_hmac then
-- log.debug("NET: network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") -- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
s_packet.stamp_authenticated() s_packet.stamp_authenticated()
else else
-- log.debug("NET: network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") -- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end end
end end
end end

View File

@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
local REPORT_FREQUENCY = 20 -- log every 20 faults per function local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local _ppm = { local ppm_sys = {
mounts = {}, ---@type { [string]: ppm_entry } mounts = {}, ---@type { [string]: ppm_entry }
next_vid = 0, next_vid = 0,
auto_cf = false, auto_cf = false,
@ -66,7 +66,7 @@ local function peri_init(iface)
if status then if status then
-- auto fault clear -- auto fault clear
if self.auto_cf then self.faulted = false end if self.auto_cf then self.faulted = false end
if _ppm.auto_cf then _ppm.faulted = false end if ppm_sys.auto_cf then ppm_sys.faulted = false end
self.fault_counts[key] = 0 self.fault_counts[key] = 0
@ -78,10 +78,10 @@ local function peri_init(iface)
self.faulted = true self.faulted = true
self.last_fault = result self.last_fault = result
_ppm.faulted = true ppm_sys.faulted = true
_ppm.last_fault = result ppm_sys.last_fault = result
if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = "" local count_str = ""
if self.fault_counts[key] > 0 then if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total faults]" count_str = " [" .. self.fault_counts[key] .. " total faults]"
@ -92,7 +92,7 @@ local function peri_init(iface)
self.fault_counts[key] = self.fault_counts[key] + 1 self.fault_counts[key] = self.fault_counts[key] + 1
if result == "Terminated" then _ppm.terminate = true end if result == "Terminated" then ppm_sys.terminate = true end
return ACCESS_FAULT, result return ACCESS_FAULT, result
end end
@ -159,10 +159,10 @@ local function peri_init(iface)
self.faulted = true self.faulted = true
self.last_fault = UNDEFINED_FIELD self.last_fault = UNDEFINED_FIELD
_ppm.faulted = true ppm_sys.faulted = true
_ppm.last_fault = UNDEFINED_FIELD ppm_sys.last_fault = UNDEFINED_FIELD
if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = "" local count_str = ""
if self.fault_counts[key] > 0 then if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total calls]" count_str = " [" .. self.fault_counts[key] .. " total calls]"
@ -193,35 +193,35 @@ end
-- REPORTING -- -- REPORTING --
-- silence error prints -- silence error prints
function ppm.disable_reporting() _ppm.mute = true end function ppm.disable_reporting() ppm_sys.mute = true end
-- allow error prints -- allow error prints
function ppm.enable_reporting() _ppm.mute = false end function ppm.enable_reporting() ppm_sys.mute = false end
-- FAULT MEMORY -- -- FAULT MEMORY --
-- enable automatically clearing fault flag -- enable automatically clearing fault flag
function ppm.enable_afc() _ppm.auto_cf = true end function ppm.enable_afc() ppm_sys.auto_cf = true end
-- disable automatically clearing fault flag -- disable automatically clearing fault flag
function ppm.disable_afc() _ppm.auto_cf = false end function ppm.disable_afc() ppm_sys.auto_cf = false end
-- clear fault flag -- clear fault flag
function ppm.clear_fault() _ppm.faulted = false end function ppm.clear_fault() ppm_sys.faulted = false end
-- check fault flag -- check fault flag
---@nodiscard ---@nodiscard
function ppm.is_faulted() return _ppm.faulted end function ppm.is_faulted() return ppm_sys.faulted end
-- get the last fault message -- get the last fault message
---@nodiscard ---@nodiscard
function ppm.get_last_fault() return _ppm.last_fault end function ppm.get_last_fault() return ppm_sys.last_fault end
-- TERMINATION -- -- TERMINATION --
-- if a caught error was a termination request -- if a caught error was a termination request
---@nodiscard ---@nodiscard
function ppm.should_terminate() return _ppm.terminate end function ppm.should_terminate() return ppm_sys.terminate end
-- MOUNTING -- -- MOUNTING --
@ -229,12 +229,12 @@ function ppm.should_terminate() return _ppm.terminate end
function ppm.mount_all() function ppm.mount_all()
local ifaces = peripheral.getNames() local ifaces = peripheral.getNames()
_ppm.mounts = {} ppm_sys.mounts = {}
for i = 1, #ifaces do for i = 1, #ifaces do
_ppm.mounts[ifaces[i]] = peri_init(ifaces[i]) ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
log.info(util.c("PPM: found a ", _ppm.mounts[ifaces[i]].type, " (", ifaces[i], ")")) log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
end end
if #ifaces == 0 then if #ifaces == 0 then
@ -253,10 +253,10 @@ function ppm.mount(iface)
for i = 1, #ifaces do for i = 1, #ifaces do
if iface == ifaces[i] then if iface == ifaces[i] then
_ppm.mounts[iface] = peri_init(iface) ppm_sys.mounts[iface] = peri_init(iface)
pm_type = _ppm.mounts[iface].type pm_type = ppm_sys.mounts[iface].type
pm_dev = _ppm.mounts[iface].dev pm_dev = ppm_sys.mounts[iface].dev
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type)) log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
break break
@ -278,12 +278,12 @@ function ppm.remount(iface)
for i = 1, #ifaces do for i = 1, #ifaces do
if iface == ifaces[i] then if iface == ifaces[i] then
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type)) log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type))
ppm.unmount(_ppm.mounts[iface].dev) ppm.unmount(ppm_sys.mounts[iface].dev)
_ppm.mounts[iface] = peri_init(iface) ppm_sys.mounts[iface] = peri_init(iface)
pm_type = _ppm.mounts[iface].type pm_type = ppm_sys.mounts[iface].type
pm_dev = _ppm.mounts[iface].dev pm_dev = ppm_sys.mounts[iface].dev
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type)) log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
break break
@ -297,24 +297,24 @@ end
---@nodiscard ---@nodiscard
---@return string type, table device ---@return string type, table device
function ppm.mount_virtual() function ppm.mount_virtual()
local iface = "ppm_vdev_" .. _ppm.next_vid local iface = "ppm_vdev_" .. ppm_sys.next_vid
_ppm.mounts[iface] = peri_init("__virtual__") ppm_sys.mounts[iface] = peri_init("__virtual__")
_ppm.next_vid = _ppm.next_vid + 1 ppm_sys.next_vid = ppm_sys.next_vid + 1
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface)) log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
return _ppm.mounts[iface].type, _ppm.mounts[iface].dev return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev
end end
-- manually unmount a peripheral from the PPM -- manually unmount a peripheral from the PPM
---@param device table device table ---@param device table device table
function ppm.unmount(device) function ppm.unmount(device)
if device then if device then
for iface, data in pairs(_ppm.mounts) do for iface, data in pairs(ppm_sys.mounts) do
if data.dev == device then if data.dev == device then
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface)) log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
_ppm.mounts[iface] = nil ppm_sys.mounts[iface] = nil
break break
end end
end end
@ -330,7 +330,7 @@ function ppm.handle_unmount(iface)
local pm_type = nil local pm_type = nil
-- what got disconnected? -- what got disconnected?
local lost_dev = _ppm.mounts[iface] local lost_dev = ppm_sys.mounts[iface]
if lost_dev then if lost_dev then
pm_type = lost_dev.type pm_type = lost_dev.type
@ -341,18 +341,18 @@ function ppm.handle_unmount(iface)
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface)) log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
end end
_ppm.mounts[iface] = nil ppm_sys.mounts[iface] = nil
return pm_type, pm_dev return pm_type, pm_dev
end end
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready -- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
function ppm.log_mounts() function ppm.log_mounts()
for iface, mount in pairs(_ppm.mounts) do for iface, mount in pairs(ppm_sys.mounts) do
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")")) log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
end end
if util.table_len(_ppm.mounts) == 0 then if util.table_len(ppm_sys.mounts) == 0 then
log.warning("PPM: no devices had been found") log.warning("PPM: no devices had been found")
end end
end end
@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end
---@return { [string]: ppm_entry } mounts ---@return { [string]: ppm_entry } mounts
function ppm.list_mounts() function ppm.list_mounts()
local list = {} local list = {}
for k, v in pairs(_ppm.mounts) do list[k] = v end for k, v in pairs(ppm_sys.mounts) do list[k] = v end
return list return list
end end
@ -379,7 +379,7 @@ end
---@return string|nil iface CC peripheral interface ---@return string|nil iface CC peripheral interface
function ppm.get_iface(device) function ppm.get_iface(device)
if device then if device then
for iface, data in pairs(_ppm.mounts) do for iface, data in pairs(ppm_sys.mounts) do
if data.dev == device then return iface end if data.dev == device then return iface end
end end
end end
@ -392,8 +392,8 @@ end
---@param iface string CC peripheral interface ---@param iface string CC peripheral interface
---@return { [string]: function }|nil device function table ---@return { [string]: function }|nil device function table
function ppm.get_periph(iface) function ppm.get_periph(iface)
if _ppm.mounts[iface] then if ppm_sys.mounts[iface] then
return _ppm.mounts[iface].dev return ppm_sys.mounts[iface].dev
else return nil end else return nil end
end end
@ -402,8 +402,8 @@ end
---@param iface string CC peripheral interface ---@param iface string CC peripheral interface
---@return string|nil type ---@return string|nil type
function ppm.get_type(iface) function ppm.get_type(iface)
if _ppm.mounts[iface] then if ppm_sys.mounts[iface] then
return _ppm.mounts[iface].type return ppm_sys.mounts[iface].type
else return nil end else return nil end
end end
@ -414,7 +414,7 @@ end
function ppm.get_all_devices(name) function ppm.get_all_devices(name)
local devices = {} local devices = {}
for _, data in pairs(_ppm.mounts) do for _, data in pairs(ppm_sys.mounts) do
if data.type == name then if data.type == name then
table.insert(devices, data.dev) table.insert(devices, data.dev)
end end
@ -430,7 +430,7 @@ end
function ppm.get_device(name) function ppm.get_device(name)
local device = nil local device = nil
for _, data in pairs(_ppm.mounts) do for _, data in pairs(ppm_sys.mounts) do
if data.type == name then if data.type == name then
device = data.dev device = data.dev
break break
@ -453,9 +453,9 @@ 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.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
w_modem = device.dev w_modem = device.dev
break break
@ -471,7 +471,7 @@ end
function ppm.get_monitor_list() function ppm.get_monitor_list()
local list = {} local list = {}
for iface, device in pairs(_ppm.mounts) do for iface, device in pairs(ppm_sys.mounts) do
if device.type == "monitor" then list[iface] = device end if device.type == "monitor" then list[iface] = device end
end end

View File

@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {} local util = {}
-- scada-common version -- scada-common version
util.version = "1.5.5" 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