Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7cb9eaa8 | ||
|
|
3139dc2176 | ||
|
|
25fc0050c3 | ||
|
|
390cf98b0a | ||
|
|
7ddd6f32c5 | ||
|
|
18a488f1b9 | ||
|
|
4c7ad0c539 | ||
|
|
a083f8983b |
@ -1,57 +0,0 @@
|
||||
# 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.
|
||||
@ -45,7 +45,6 @@ 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:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/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
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ local CCMSI_VERSION = "v1.21"
|
||||
|
||||
local install_dir = "/.install-cache"
|
||||
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
|
||||
local repo_path = "http://git.befatorinc.de/TheHomecraft/cc-mek-scada/raw/"
|
||||
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
|
||||
|
||||
---@diagnostic disable-next-line: undefined-global
|
||||
local _is_pkt_env = pocket -- luacheck: ignore pocket
|
||||
|
||||
@ -53,7 +53,6 @@ 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("has_modem", not plc_state.no_modem)
|
||||
databus.ps.publish("degraded", plc_state.degraded)
|
||||
databus.ps.publish("init_ok", plc_state.init_ok)
|
||||
end
|
||||
|
||||
-- transmit thread (routine) statuses
|
||||
|
||||
@ -51,11 +51,11 @@ local function init(panel)
|
||||
|
||||
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
|
||||
|
||||
local init_ok = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local degraded = LED{parent=system,label="STATUS",colors=cpair(colors.red,colors.green)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
|
||||
system.line_break()
|
||||
|
||||
init_ok.register(databus.ps, "init_ok", init_ok.update)
|
||||
degraded.register(databus.ps, "degraded", degraded.update)
|
||||
heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
|
||||
|
||||
@ -118,7 +118,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
reactor_enabled = false,
|
||||
enabled_at = 0,
|
||||
emer_cool_active = nil, ---@type boolean
|
||||
formed = is_formed,
|
||||
formed = is_formed, ---@type boolean|nil
|
||||
force_disabled = false,
|
||||
tripped = false,
|
||||
trip_cause = "ok" ---@type rps_trip_cause
|
||||
@ -364,29 +364,35 @@ function plc.rps_init(reactor, is_formed)
|
||||
return public.activate()
|
||||
end
|
||||
|
||||
-- check all safety conditions
|
||||
-- check all safety conditions if we have a formed reactor, otherwise handle a subset of conditions
|
||||
---@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
|
||||
function public.check()
|
||||
function public.check(has_reactor)
|
||||
local status = RPS_TRIP_CAUSE.OK
|
||||
local was_tripped = self.tripped
|
||||
local first_trip = false
|
||||
|
||||
if self.formed then
|
||||
-- update state
|
||||
parallel.waitForAll(
|
||||
_is_formed,
|
||||
_is_force_disabled,
|
||||
_high_damage,
|
||||
_high_temp,
|
||||
_low_coolant,
|
||||
_excess_waste,
|
||||
_excess_heated_coolant,
|
||||
_insufficient_fuel
|
||||
)
|
||||
if has_reactor then
|
||||
if self.formed then
|
||||
-- update state
|
||||
parallel.waitForAll(
|
||||
_is_formed,
|
||||
_is_force_disabled,
|
||||
_high_damage,
|
||||
_high_temp,
|
||||
_low_coolant,
|
||||
_excess_waste,
|
||||
_excess_heated_coolant,
|
||||
_insufficient_fuel
|
||||
)
|
||||
else
|
||||
-- check to see if its now formed
|
||||
_is_formed()
|
||||
end
|
||||
else
|
||||
-- check to see if its now formed
|
||||
_is_formed()
|
||||
self.formed = nil
|
||||
self.state[CHK.SYS_FAIL] = true
|
||||
end
|
||||
|
||||
-- check system states in order of severity
|
||||
@ -474,6 +480,7 @@ function plc.rps_init(reactor, is_formed)
|
||||
---@nodiscard
|
||||
function public.is_active() return self.reactor_enabled end
|
||||
---@nodiscard
|
||||
---@return boolean|nil formed true if formed, false if not, nil if unknown
|
||||
function public.is_formed() return self.formed end
|
||||
---@nodiscard
|
||||
function public.is_force_disabled() return self.force_disabled end
|
||||
@ -495,14 +502,14 @@ function plc.rps_init(reactor, is_formed)
|
||||
end
|
||||
|
||||
-- partial RPS reset that only clears fault and sys_fail
|
||||
function public.reset_formed()
|
||||
function public.reset_reattach()
|
||||
self.tripped = false
|
||||
self.trip_cause = RPS_TRIP_CAUSE.OK
|
||||
|
||||
self.state[CHK.FAULT] = false
|
||||
self.state[CHK.SYS_FAIL] = false
|
||||
|
||||
log.info("RPS: partial reset on formed")
|
||||
log.info("RPS: partial reset on connected or formed")
|
||||
end
|
||||
|
||||
-- reset the automatic and timeout trip flags, then clear trip if that was the trip cause
|
||||
@ -584,11 +591,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
-- dynamic reactor status information, excluding heating rate
|
||||
---@return table data_table, boolean faulted
|
||||
local function _get_reactor_status()
|
||||
local fuel = nil
|
||||
local waste = nil
|
||||
local coolant = nil
|
||||
local hcoolant = nil
|
||||
|
||||
local fuel, waste, coolant, hcoolant = nil, nil, nil, nil
|
||||
local data_table = {}
|
||||
|
||||
reactor.__p_disable_afc()
|
||||
@ -707,6 +710,112 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
reactor.__p_enable_afc()
|
||||
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 --
|
||||
|
||||
---@class plc_comms
|
||||
@ -748,8 +857,8 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
---@param formed boolean reactor formed (from PLC state)
|
||||
function public.send_status(no_reactor, formed)
|
||||
if self.linked then
|
||||
local mek_data = nil ---@type table
|
||||
local heating_rate = 0.0 ---@type number
|
||||
local mek_data = nil ---@type table
|
||||
local heating_rate = 0.0 ---@type number
|
||||
|
||||
if (not no_reactor) and rps.is_formed() then
|
||||
if _update_status_cache() then mek_data = self.status_cache end
|
||||
@ -803,15 +912,11 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
-- get as RPLC packet
|
||||
if s_pkt.protocol() == PROTOCOL.RPLC then
|
||||
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
|
||||
elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
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
|
||||
log.debug("unsupported packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
@ -823,16 +928,13 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
-- handle RPLC and MGMT packets
|
||||
---@param packet rplc_frame|mgmt_frame packet frame
|
||||
---@param plc_state plc_state PLC state
|
||||
---@param setpoints setpoints setpoint control table
|
||||
function public.handle_packet(packet, plc_state, setpoints)
|
||||
-- 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
|
||||
|
||||
---@param setpoints plc_setpoints setpoint control table
|
||||
---@param println_ts function console print, when UI isn't running
|
||||
function public.handle_packet(packet, plc_state, setpoints, println_ts)
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
|
||||
-- handle packets now that we have prints setup
|
||||
if l_chan == config.PLC_Channel then
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
@ -867,36 +969,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
log.debug("sent out structure again, did supervisor miss it?")
|
||||
elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
|
||||
-- set the burn rate
|
||||
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
|
||||
_handle_burn_rate(packet, setpoints)
|
||||
elseif packet.type == RPLC_TYPE.RPS_ENABLE then
|
||||
-- enable the reactor
|
||||
self.scrammed = false
|
||||
@ -925,68 +998,7 @@ function plc.comms(version, nic, reactor, rps, conn_watchdog)
|
||||
_send_ack(packet.type, true)
|
||||
elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
|
||||
-- automatic control requested a new burn rate
|
||||
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
|
||||
_handle_auto_burn_rate(packet, setpoints)
|
||||
else
|
||||
log.debug("received unknown RPLC packet type " .. packet.type)
|
||||
end
|
||||
|
||||
@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
|
||||
local renderer = require("reactor-plc.renderer")
|
||||
local threads = require("reactor-plc.threads")
|
||||
|
||||
local R_PLC_VERSION = "v1.8.22"
|
||||
local R_PLC_VERSION = "v1.9.0"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@ -87,7 +87,6 @@ local function main()
|
||||
-- PLC system state flags
|
||||
---@class plc_state
|
||||
plc_state = {
|
||||
init_ok = true,
|
||||
fp_ok = false,
|
||||
shutdown = false,
|
||||
degraded = true,
|
||||
@ -97,19 +96,22 @@ local function main()
|
||||
},
|
||||
|
||||
-- control setpoints
|
||||
---@class setpoints
|
||||
---@class plc_setpoints
|
||||
setpoints = {
|
||||
burn_rate_en = false,
|
||||
burn_rate = 0.0
|
||||
},
|
||||
|
||||
-- core PLC devices
|
||||
---@class plc_dev
|
||||
plc_dev = {
|
||||
reactor = ppm.get_fission_reactor(),
|
||||
---@diagnostic disable-next-line: assign-type-mismatch
|
||||
reactor = ppm.get_fission_reactor(), ---@type table
|
||||
modem = ppm.get_wireless_modem()
|
||||
},
|
||||
|
||||
-- system objects
|
||||
---@class plc_sys
|
||||
plc_sys = {
|
||||
rps = nil, ---@type rps
|
||||
nic = nil, ---@type nic
|
||||
@ -136,14 +138,20 @@ local function main()
|
||||
|
||||
-- we need a reactor, can at least do some things even if it isn't formed though
|
||||
if plc_state.no_reactor then
|
||||
println("init> fission reactor not found")
|
||||
log.warning("init> no reactor on startup")
|
||||
println("startup> fission reactor not found")
|
||||
log.warning("startup> no reactor on startup")
|
||||
|
||||
plc_state.init_ok = false
|
||||
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
|
||||
println("init> fission reactor is not formed")
|
||||
log.warning("init> reactor logic adapter present, but reactor is not formed")
|
||||
println("startup> fission reactor is not formed")
|
||||
log.warning("startup> reactor logic adapter present, but reactor is not formed")
|
||||
|
||||
plc_state.degraded = true
|
||||
plc_state.reactor_formed = false
|
||||
@ -151,89 +159,74 @@ local function main()
|
||||
|
||||
-- modem is required if networked
|
||||
if __shared_memory.networked and plc_state.no_modem then
|
||||
println("init> wireless modem not found")
|
||||
log.warning("init> no wireless modem on startup")
|
||||
println("startup> wireless modem not found")
|
||||
log.warning("startup> no wireless modem on startup")
|
||||
|
||||
-- scram reactor if present and enabled
|
||||
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
|
||||
smem_dev.reactor.scram()
|
||||
end
|
||||
|
||||
plc_state.init_ok = false
|
||||
plc_state.degraded = true
|
||||
end
|
||||
|
||||
-- print a log message to the terminal as long as the UI isn't running
|
||||
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)
|
||||
-- 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
|
||||
local function _println_no_fp(msg) if not plc_state.fp_ok then println(msg) end 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
|
||||
local main_thread = threads.thread__main(__shared_memory, init)
|
||||
local main_thread = threads.thread__main(__shared_memory)
|
||||
local rps_thread = threads.thread__rps(__shared_memory)
|
||||
|
||||
if __shared_memory.networked then
|
||||
@ -247,14 +240,12 @@ local function main()
|
||||
-- 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)
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- send status one last time after RPS shutdown
|
||||
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
smem_sys.plc_comms.send_rps_status()
|
||||
-- send status one last time after RPS shutdown
|
||||
smem_sys.plc_comms.send_status(plc_state.no_reactor, plc_state.reactor_formed)
|
||||
smem_sys.plc_comms.send_rps_status()
|
||||
|
||||
-- close connection
|
||||
smem_sys.plc_comms.close()
|
||||
end
|
||||
-- close connection
|
||||
smem_sys.plc_comms.close()
|
||||
else
|
||||
-- run threads, excluding comms
|
||||
parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec)
|
||||
|
||||
@ -31,8 +31,7 @@ local MQ__COMM_CMD = {
|
||||
-- main thread
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
---@param init function
|
||||
function threads.thread__main(smem, init)
|
||||
function threads.thread__main(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
|
||||
|
||||
@ -42,7 +41,7 @@ function threads.thread__main(smem, init)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("main", true)
|
||||
log.debug("main thread init, clock inactive")
|
||||
log.debug("OS: main thread start")
|
||||
|
||||
-- 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)
|
||||
@ -55,6 +54,9 @@ function threads.thread__main(smem, init)
|
||||
local plc_state = smem.plc_state
|
||||
local plc_dev = smem.plc_dev
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
-- event loop
|
||||
while true do
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
@ -67,7 +69,6 @@ function threads.thread__main(smem, init)
|
||||
|
||||
-- handle event
|
||||
if event == "timer" and loop_clock.is_clock(param1) then
|
||||
-- note: loop clock is only running if init_ok = true
|
||||
-- blink heartbeat indicator
|
||||
databus.heartbeat()
|
||||
|
||||
@ -93,7 +94,7 @@ function threads.thread__main(smem, init)
|
||||
-- reactor now formed
|
||||
plc_state.reactor_formed = true
|
||||
|
||||
println_ts("reactor is now formed.")
|
||||
println_ts("reactor is now formed")
|
||||
log.info("reactor is now formed")
|
||||
|
||||
-- SCRAM newly formed reactor
|
||||
@ -106,10 +107,10 @@ function threads.thread__main(smem, init)
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
elseif plc_state.reactor_formed and not rps.is_formed() then
|
||||
rps.reset_reattach()
|
||||
elseif plc_state.reactor_formed and (rps.is_formed() == false) then
|
||||
-- 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")
|
||||
|
||||
plc_state.reactor_formed = false
|
||||
@ -118,14 +119,14 @@ function threads.thread__main(smem, init)
|
||||
|
||||
-- update indicators
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "modem_message" and networked and plc_state.init_ok and nic.is_connected() then
|
||||
elseif event == "modem_message" and networked and nic.is_connected() then
|
||||
-- got a packet
|
||||
local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
if packet ~= nil then
|
||||
-- pass the packet onto the comms message queue
|
||||
smem.q.mq_comms_rx.push_packet(packet)
|
||||
end
|
||||
elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then
|
||||
elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then
|
||||
-- haven't heard from server recently? close connection and shutdown reactor
|
||||
plc_comms.close()
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
|
||||
@ -146,8 +147,7 @@ function threads.thread__main(smem, init)
|
||||
elseif networked and type == "modem" then
|
||||
---@cast device Modem
|
||||
-- we only care if this is our wireless modem
|
||||
-- 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
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
|
||||
println_ts("comms modem disconnected!")
|
||||
@ -161,10 +161,8 @@ function threads.thread__main(smem, init)
|
||||
plc_state.no_modem = true
|
||||
plc_state.degraded = true
|
||||
|
||||
if plc_state.init_ok then
|
||||
-- try to scram reactor if it is still connected
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
end
|
||||
-- try to scram reactor if it is still connected
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM)
|
||||
end
|
||||
else
|
||||
log.warning("a modem was disconnected")
|
||||
@ -184,7 +182,7 @@ function threads.thread__main(smem, init)
|
||||
plc_dev.reactor = device
|
||||
plc_state.no_reactor = false
|
||||
|
||||
println_ts("reactor reconnected.")
|
||||
println_ts("reactor reconnected")
|
||||
log.info("reactor reconnected")
|
||||
|
||||
-- we need to assume formed here as we cannot check in this main loop
|
||||
@ -196,64 +194,52 @@ function threads.thread__main(smem, init)
|
||||
plc_state.degraded = false
|
||||
end
|
||||
|
||||
if plc_state.init_ok then
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
|
||||
|
||||
rps.reconnect_reactor(plc_dev.reactor)
|
||||
if networked then
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed/reconnected
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_formed()
|
||||
rps.reconnect_reactor(plc_dev.reactor)
|
||||
if networked then
|
||||
plc_comms.reconnect_reactor(plc_dev.reactor)
|
||||
end
|
||||
|
||||
-- partial reset of RPS, specific to becoming formed/reconnected
|
||||
-- without this, auto control can't resume on chunk load
|
||||
rps.reset_reattach()
|
||||
elseif networked and type == "modem" then
|
||||
---@cast device Modem
|
||||
-- note, check init_ok first since nic will be nil if it is false
|
||||
if device.isWireless() and not (plc_state.init_ok and nic.is_connected()) then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
plc_dev.modem = device
|
||||
plc_state.no_modem = false
|
||||
|
||||
if plc_state.init_ok then nic.connect(device) end
|
||||
nic.connect(device)
|
||||
|
||||
println_ts("wireless modem reconnected.")
|
||||
println_ts("comms modem reconnected")
|
||||
log.info("comms modem reconnected")
|
||||
|
||||
-- determine if we are still in a degraded state
|
||||
if not plc_state.no_reactor then
|
||||
if plc_state.reactor_formed and not plc_state.no_reactor then
|
||||
plc_state.degraded = false
|
||||
end
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
log.info("unused wireless modem connected")
|
||||
else
|
||||
log.info("wired modem reconnected")
|
||||
log.info("wired modem connected")
|
||||
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
|
||||
databus.tx_hw_status(plc_state)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or
|
||||
event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "clock_start" then
|
||||
-- start loop clock
|
||||
loop_clock.start()
|
||||
log.debug("main thread clock started")
|
||||
end
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
log.info("terminate requested, main thread exiting")
|
||||
log.info("OS: terminate requested, main thread exiting")
|
||||
-- rps handles reactor shutdown
|
||||
plc_state.shutdown = true
|
||||
break
|
||||
@ -277,8 +263,7 @@ function threads.thread__main(smem, init)
|
||||
-- if not, we need to restart the clock
|
||||
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
|
||||
if not plc_state.shutdown then
|
||||
log.info("main thread restarting now...")
|
||||
util.push_event("clock_start")
|
||||
log.info("OS: main thread restarting now...")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -299,7 +284,7 @@ function threads.thread__rps(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("rps", true)
|
||||
log.debug("rps thread start")
|
||||
log.debug("OS: rps thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local networked = smem.networked
|
||||
@ -316,49 +301,36 @@ function threads.thread__rps(smem)
|
||||
-- get plc_sys fields (may have been set late due to degraded boot)
|
||||
local rps = smem.plc_sys.rps
|
||||
local plc_comms = smem.plc_sys.plc_comms
|
||||
-- get reactor, may have changed do to disconnect/reconnect
|
||||
-- get reactor, it may have changed due to a disconnect/reconnect
|
||||
local reactor = plc_dev.reactor
|
||||
|
||||
-- RPS checks
|
||||
if plc_state.init_ok then
|
||||
-- SCRAM if no open connection
|
||||
if networked and not plc_comms.is_linked() then
|
||||
if was_linked then
|
||||
was_linked = false
|
||||
rps.trip_timeout()
|
||||
end
|
||||
else
|
||||
was_linked = true
|
||||
-- SCRAM if no open connection
|
||||
if networked and not plc_comms.is_linked() then
|
||||
if was_linked then
|
||||
was_linked = false
|
||||
rps.trip_timeout()
|
||||
end
|
||||
else was_linked = true end
|
||||
|
||||
if (not plc_state.no_reactor) and rps.is_formed() then
|
||||
-- check reactor status
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local reactor_status = reactor.getStatus()
|
||||
databus.tx_reactor_state(reactor_status)
|
||||
-- check reactor status
|
||||
if (not plc_state.no_reactor) and rps.is_formed() then
|
||||
local reactor_status = reactor.getStatus()
|
||||
databus.tx_reactor_state(reactor_status)
|
||||
|
||||
-- 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)
|
||||
if rps.is_tripped() and reactor_status then
|
||||
rps.scram()
|
||||
end
|
||||
end
|
||||
-- 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)
|
||||
if rps.is_tripped() and reactor_status then rps.scram() end
|
||||
end
|
||||
|
||||
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS
|
||||
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
|
||||
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
|
||||
-- if we are in standalone mode and the front panel isn't working, continuously reset RPS
|
||||
-- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable
|
||||
if not (networked or smem.plc_state.fp_ok) then rps.reset(true) end
|
||||
|
||||
-- check safety (SCRAM occurs if tripped)
|
||||
if not plc_state.no_reactor then
|
||||
local rps_tripped, rps_status_string, rps_first = rps.check()
|
||||
|
||||
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
|
||||
-- check safety (SCRAM occurs if tripped)
|
||||
local rps_tripped, rps_status_string, rps_first = rps.check(not plc_state.no_reactor)
|
||||
if rps_tripped and rps_first then
|
||||
println_ts("RPS: SCRAM on safety trip (" .. rps_status_string .. ")")
|
||||
if networked then plc_comms.send_rps_alarm(rps_status_string) end
|
||||
end
|
||||
|
||||
-- check for messages in the message queue
|
||||
@ -368,19 +340,19 @@ function threads.thread__rps(smem)
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if plc_state.init_ok then
|
||||
if msg.message == MQ__RPS_CMD.SCRAM then
|
||||
-- SCRAM
|
||||
rps.scram()
|
||||
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
|
||||
-- lost peripheral(s)
|
||||
rps.trip_fault()
|
||||
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
|
||||
-- watchdog tripped
|
||||
rps.trip_timeout()
|
||||
println_ts("server timeout")
|
||||
log.warning("server timeout")
|
||||
end
|
||||
if msg.message == MQ__RPS_CMD.SCRAM then
|
||||
-- SCRAM
|
||||
log.info("RPS: OS requested SCRAM")
|
||||
rps.scram()
|
||||
elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then
|
||||
-- lost peripheral(s)
|
||||
log.info("RPS: received PLC degraded alert")
|
||||
rps.trip_fault()
|
||||
elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then
|
||||
-- watchdog tripped
|
||||
println_ts("RPS: supervisor timeout")
|
||||
log.warning("RPS: received supervisor timeout alert")
|
||||
rps.trip_timeout()
|
||||
end
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
-- received data
|
||||
@ -396,17 +368,17 @@ function threads.thread__rps(smem)
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
-- safe exit
|
||||
log.info("rps thread shutdown initiated")
|
||||
if plc_state.init_ok then
|
||||
if rps.scram() then
|
||||
println_ts("reactor disabled")
|
||||
log.info("rps thread reactor SCRAM OK")
|
||||
else
|
||||
println_ts("exiting, reactor failed to disable")
|
||||
log.error("rps thread failed to SCRAM reactor on exit")
|
||||
end
|
||||
log.info("OS: rps thread shutdown initiated")
|
||||
|
||||
if rps.scram() then
|
||||
println_ts("exiting, reactor disabled")
|
||||
log.info("OS: rps thread reactor SCRAM OK on exit")
|
||||
else
|
||||
println_ts("exiting, reactor failed to disable")
|
||||
log.error("OS: rps thread failed to SCRAM reactor on exit")
|
||||
end
|
||||
log.info("rps thread exiting")
|
||||
|
||||
log.info("OS: rps thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@ -428,8 +400,8 @@ function threads.thread__rps(smem)
|
||||
databus.tx_rt_status("rps", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
if plc_state.init_ok then smem.plc_sys.rps.scram() end
|
||||
log.info("rps thread restarting in 5 seconds...")
|
||||
smem.plc_sys.rps.scram()
|
||||
log.info("OS: rps thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@ -448,7 +420,7 @@ function threads.thread__comms_tx(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms_tx", true)
|
||||
log.debug("comms tx thread start")
|
||||
log.debug("OS: comms tx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
@ -465,7 +437,7 @@ function threads.thread__comms_tx(smem)
|
||||
while comms_queue.ready() and not plc_state.shutdown do
|
||||
local msg = comms_queue.pop()
|
||||
|
||||
if msg ~= nil and plc_state.init_ok then
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
if msg.message == MQ__COMM_CMD.SEND_STATUS then
|
||||
@ -486,7 +458,7 @@ function threads.thread__comms_tx(smem)
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("comms tx thread exiting")
|
||||
log.info("OS: comms tx thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@ -508,7 +480,7 @@ function threads.thread__comms_tx(smem)
|
||||
databus.tx_rt_status("comms_tx", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms tx thread restarting in 5 seconds...")
|
||||
log.info("OS: comms tx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@ -521,13 +493,16 @@ end
|
||||
---@nodiscard
|
||||
---@param smem plc_shared_memory
|
||||
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
|
||||
local public = {}
|
||||
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("comms_rx", true)
|
||||
log.debug("comms rx thread start")
|
||||
log.debug("OS: comms rx thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
@ -546,7 +521,7 @@ function threads.thread__comms_rx(smem)
|
||||
while comms_queue.ready() and not plc_state.shutdown do
|
||||
local msg = comms_queue.pop()
|
||||
|
||||
if msg ~= nil and plc_state.init_ok then
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- received a command
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
@ -555,7 +530,7 @@ function threads.thread__comms_rx(smem)
|
||||
-- received a packet
|
||||
-- handle the packet (setpoints passed to update burn rate setpoint)
|
||||
-- (plc_state passed to check if degraded)
|
||||
plc_comms.handle_packet(msg.message, plc_state, setpoints)
|
||||
plc_comms.handle_packet(msg.message, plc_state, setpoints, println_ts)
|
||||
end
|
||||
end
|
||||
|
||||
@ -565,7 +540,7 @@ function threads.thread__comms_rx(smem)
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("comms rx thread exiting")
|
||||
log.info("OS: comms rx thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@ -587,7 +562,7 @@ function threads.thread__comms_rx(smem)
|
||||
databus.tx_rt_status("comms_rx", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("comms rx thread restarting in 5 seconds...")
|
||||
log.info("OS: comms rx thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
@ -606,7 +581,7 @@ function threads.thread__setpoint_control(smem)
|
||||
-- execute thread
|
||||
function public.exec()
|
||||
databus.tx_rt_status("spctl", true)
|
||||
log.debug("setpoint control thread start")
|
||||
log.debug("OS: setpoint control thread start")
|
||||
|
||||
-- load in from shared memory
|
||||
local plc_state = smem.plc_state
|
||||
@ -629,9 +604,7 @@ function threads.thread__setpoint_control(smem)
|
||||
-- get reactor, may have changed do to disconnect/reconnect
|
||||
local reactor = plc_dev.reactor
|
||||
|
||||
if plc_state.init_ok and (not plc_state.no_reactor) then
|
||||
---@cast reactor table won't be nil
|
||||
|
||||
if not plc_state.no_reactor then
|
||||
-- check if we should start ramping
|
||||
if setpoints.burn_rate_en and (setpoints.burn_rate ~= last_burn_sp) then
|
||||
local cur_burn_rate = reactor.getBurnRate()
|
||||
@ -698,7 +671,7 @@ function threads.thread__setpoint_control(smem)
|
||||
|
||||
-- check for termination request
|
||||
if plc_state.shutdown then
|
||||
log.info("setpoint control thread exiting")
|
||||
log.info("OS: setpoint control thread exiting")
|
||||
break
|
||||
end
|
||||
|
||||
@ -720,7 +693,7 @@ function threads.thread__setpoint_control(smem)
|
||||
databus.tx_rt_status("spctl", false)
|
||||
|
||||
if not plc_state.shutdown then
|
||||
log.info("setpoint control thread restarting in 5 seconds...")
|
||||
log.info("OS: setpoint control thread restarting in 5 seconds...")
|
||||
util.psleep(5)
|
||||
end
|
||||
end
|
||||
|
||||
@ -205,7 +205,7 @@ function comms.scada_packet()
|
||||
|
||||
if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
|
||||
-- outside of maximum allowable transmission distance
|
||||
-- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
-- log.debug("COMMS: comms.scada_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
else
|
||||
if type(self.raw) == "table" 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
|
||||
-- outside of maximum allowable transmission distance
|
||||
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
-- log.debug("COMMS: comms.authd_packet.receive(): discarding packet with distance " .. distance .. " (outside trusted range)")
|
||||
else
|
||||
if type(self.raw) == "table" 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 }
|
||||
for i = 1, self.length do insert(self.raw, data[i]) end
|
||||
else
|
||||
log.error("comms.modbus_packet.make(): data not a table")
|
||||
log.error("COMMS: modbus_packet.make(): data not a table")
|
||||
end
|
||||
end
|
||||
|
||||
@ -435,11 +435,11 @@ function comms.modbus_packet()
|
||||
|
||||
return size_ok and valid
|
||||
else
|
||||
log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
|
||||
log.debug("COMMS: attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
log.debug("COMMS: nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
@ -498,7 +498,7 @@ function comms.rplc_packet()
|
||||
self.raw = { self.id, self.type }
|
||||
for i = 1, #data do insert(self.raw, data[i]) end
|
||||
else
|
||||
log.error("comms.rplc_packet.make(): data not a table")
|
||||
log.error("COMMS: rplc_packet.make(): data not a table")
|
||||
end
|
||||
end
|
||||
|
||||
@ -521,11 +521,11 @@ function comms.rplc_packet()
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
|
||||
log.debug("COMMS: attempted RPLC parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
log.debug("COMMS: nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
@ -580,7 +580,7 @@ function comms.mgmt_packet()
|
||||
self.raw = { self.type }
|
||||
for i = 1, #data do insert(self.raw, data[i]) end
|
||||
else
|
||||
log.error("comms.mgmt_packet.make(): data not a table")
|
||||
log.error("COMMS: mgmt_packet.make(): data not a table")
|
||||
end
|
||||
end
|
||||
|
||||
@ -601,11 +601,11 @@ function comms.mgmt_packet()
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
|
||||
log.debug("COMMS: attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
log.debug("COMMS: nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
@ -659,7 +659,7 @@ function comms.crdn_packet()
|
||||
self.raw = { self.type }
|
||||
for i = 1, #data do insert(self.raw, data[i]) end
|
||||
else
|
||||
log.error("comms.crdn_packet.make(): data not a table")
|
||||
log.error("COMMS: crdn_packet.make(): data not a table")
|
||||
end
|
||||
end
|
||||
|
||||
@ -680,11 +680,11 @@ function comms.crdn_packet()
|
||||
|
||||
return ok
|
||||
else
|
||||
log.debug("attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
|
||||
log.debug("COMMS: attempted SCADA_CRDN parse of incorrect protocol " .. frame.protocol(), true)
|
||||
return false
|
||||
end
|
||||
else
|
||||
log.debug("nil frame encountered", true)
|
||||
log.debug("COMMS: nil frame encountered", true)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@ -20,7 +20,7 @@ local MODE = { APPEND = 0, NEW = 1 }
|
||||
|
||||
log.MODE = MODE
|
||||
|
||||
local logger = {
|
||||
local _log = {
|
||||
not_ready = true,
|
||||
path = "/log.txt",
|
||||
mode = MODE.APPEND,
|
||||
@ -42,36 +42,36 @@ local free_space = fs.getFreeSpace
|
||||
---@param err_msg string|nil error message
|
||||
---@return boolean out_of_space
|
||||
local function check_out_of_space(err_msg)
|
||||
return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
|
||||
return (free_space(_log.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
|
||||
end
|
||||
|
||||
-- private log write function
|
||||
---@param msg_bits any[]
|
||||
local function _log(msg_bits)
|
||||
if logger.not_ready then return end
|
||||
local function write_log(msg_bits)
|
||||
if _log.not_ready then return end
|
||||
|
||||
local time_stamp = os.date(TIME_FMT)
|
||||
local stamped = util.c(time_stamp, table.unpack(msg_bits))
|
||||
|
||||
-- attempt to write log
|
||||
local status, result = pcall(function ()
|
||||
logger.file.writeLine(stamped)
|
||||
logger.file.flush()
|
||||
_log.file.writeLine(stamped)
|
||||
_log.file.flush()
|
||||
end)
|
||||
|
||||
-- if we don't have space, we need to create a new log file
|
||||
if check_out_of_space() then
|
||||
-- delete the old log file before opening a new one
|
||||
logger.file.close()
|
||||
fs.delete(logger.path)
|
||||
_log.file.close()
|
||||
fs.delete(_log.path)
|
||||
|
||||
-- re-init logger and pass dmesg_out so that it doesn't change
|
||||
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
|
||||
log.init(_log.path, _log.mode, _log.debug, _log.dmesg_out)
|
||||
|
||||
-- log the message and recycle warning
|
||||
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
|
||||
logger.file.writeLine(stamped)
|
||||
logger.file.flush()
|
||||
_log.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
|
||||
_log.file.writeLine(stamped)
|
||||
_log.file.flush()
|
||||
elseif (not status) and (result ~= nil) then
|
||||
util.println("unexpected error writing to the log file: " .. result)
|
||||
end
|
||||
@ -89,45 +89,45 @@ end
|
||||
function log.init(path, write_mode, include_debug, dmesg_redirect)
|
||||
local err_msg
|
||||
|
||||
logger.path = path
|
||||
logger.mode = write_mode
|
||||
logger.debug = include_debug
|
||||
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
|
||||
_log.path = path
|
||||
_log.mode = write_mode
|
||||
_log.debug = include_debug
|
||||
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
|
||||
|
||||
if dmesg_redirect then
|
||||
logger.dmesg_out = dmesg_redirect
|
||||
_log.dmesg_out = dmesg_redirect
|
||||
else
|
||||
logger.dmesg_out = term.current()
|
||||
_log.dmesg_out = term.current()
|
||||
end
|
||||
|
||||
-- check for space issues
|
||||
local out_of_space = check_out_of_space(err_msg)
|
||||
|
||||
-- try to handle problems
|
||||
if logger.file == nil or out_of_space then
|
||||
if _log.file == nil or out_of_space then
|
||||
if out_of_space then
|
||||
if fs.exists(logger.path) then
|
||||
fs.delete(logger.path)
|
||||
if fs.exists(_log.path) then
|
||||
fs.delete(_log.path)
|
||||
|
||||
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
|
||||
_log.file, err_msg = fs.open(path, util.trinary(_log.mode == MODE.APPEND, "a", "w"))
|
||||
|
||||
if logger.file then
|
||||
logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
|
||||
logger.file.flush()
|
||||
if _log.file then
|
||||
_log.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
|
||||
_log.file.flush()
|
||||
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("unexpected error setting up the log file: " .. err_msg) end
|
||||
end
|
||||
|
||||
logger.not_ready = false
|
||||
_log.not_ready = false
|
||||
end
|
||||
|
||||
-- close the log file handle
|
||||
function log.close() logger.file.close() end
|
||||
function log.close() _log.file.close() end
|
||||
|
||||
-- direct dmesg output to a monitor/window
|
||||
---@param window Window window or terminal reference
|
||||
function log.direct_dmesg(window) logger.dmesg_out = window end
|
||||
function log.direct_dmesg(window) _log.dmesg_out = window end
|
||||
|
||||
-- dmesg style logging for boot because I like linux-y things
|
||||
---@param msg any message
|
||||
@ -142,7 +142,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
tag = util.strval(tag or "")
|
||||
|
||||
local t_stamp = string.format("%12.2f", os.clock())
|
||||
local out = logger.dmesg_out
|
||||
local out = _log.dmesg_out
|
||||
|
||||
if out ~= nil then
|
||||
local out_w, out_h = out.getSize()
|
||||
@ -180,7 +180,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
||||
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
@ -216,7 +216,7 @@ function log.dmesg(msg, tag, tag_color)
|
||||
if cur_y == out_h then
|
||||
out.scroll(1)
|
||||
out.setCursorPos(1, cur_y)
|
||||
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
||||
_log.dmesg_scroll_count = _log.dmesg_scroll_count + 1
|
||||
else
|
||||
out.setCursorPos(1, cur_y + 1)
|
||||
end
|
||||
@ -225,9 +225,9 @@ function log.dmesg(msg, tag, tag_color)
|
||||
out.write(lines[i])
|
||||
end
|
||||
|
||||
logger.dmesg_restore_coord = { out.getCursorPos() }
|
||||
_log.dmesg_restore_coord = { out.getCursorPos() }
|
||||
|
||||
_log{"[", t_stamp, "] [", tag, "] ", msg}
|
||||
write_log{"[", t_stamp, "] [", tag, "] ", msg}
|
||||
end
|
||||
|
||||
return ts_coord
|
||||
@ -241,9 +241,9 @@ end
|
||||
---@return function update, function done
|
||||
function log.dmesg_working(msg, tag, tag_color)
|
||||
local ts_coord = log.dmesg(msg, tag, tag_color)
|
||||
local initial_scroll = logger.dmesg_scroll_count
|
||||
local initial_scroll = _log.dmesg_scroll_count
|
||||
|
||||
local out = logger.dmesg_out
|
||||
local out = _log.dmesg_out
|
||||
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
||||
|
||||
if out ~= nil then
|
||||
@ -252,7 +252,7 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
local counter = 0
|
||||
|
||||
local function update(sec_remaining)
|
||||
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
||||
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
|
||||
if new_y < 1 then return end
|
||||
|
||||
local time = util.sprintf("%ds", sec_remaining)
|
||||
@ -280,11 +280,11 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
|
||||
counter = counter + 1
|
||||
|
||||
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
||||
out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
|
||||
end
|
||||
|
||||
local function done(ok)
|
||||
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
||||
local new_y = ts_coord.y - (_log.dmesg_scroll_count - initial_scroll)
|
||||
if new_y < 1 then return end
|
||||
|
||||
out.setCursorPos(ts_coord.x1, new_y)
|
||||
@ -299,7 +299,7 @@ function log.dmesg_working(msg, tag, tag_color)
|
||||
|
||||
out.setTextColor(initial_color)
|
||||
|
||||
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
||||
out.setCursorPos(table.unpack(_log.dmesg_restore_coord))
|
||||
end
|
||||
|
||||
return update, done
|
||||
@ -312,28 +312,28 @@ end
|
||||
---@param msg any message
|
||||
---@param trace? boolean include file trace
|
||||
function log.debug(msg, trace)
|
||||
if logger.debug then
|
||||
if _log.debug then
|
||||
if trace then
|
||||
local info = debug.getinfo(2)
|
||||
|
||||
if info.name ~= nil then
|
||||
_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
write_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
else
|
||||
_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
write_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
end
|
||||
else
|
||||
_log{DBG_TAG, msg}
|
||||
write_log{DBG_TAG, msg}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- log info messages
|
||||
---@param msg any message
|
||||
function log.info(msg) _log{INF_TAG, msg} end
|
||||
function log.info(msg) write_log{INF_TAG, msg} end
|
||||
|
||||
-- log warning messages
|
||||
---@param msg any message
|
||||
function log.warning(msg) _log{WRN_TAG, msg} end
|
||||
function log.warning(msg) write_log{WRN_TAG, msg} end
|
||||
|
||||
-- log error messages
|
||||
---@param msg any message
|
||||
@ -343,17 +343,17 @@ function log.error(msg, trace)
|
||||
local info = debug.getinfo(2)
|
||||
|
||||
if info.name ~= nil then
|
||||
_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
write_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
||||
else
|
||||
_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
write_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
||||
end
|
||||
else
|
||||
_log{ERR_TAG, msg}
|
||||
write_log{ERR_TAG, msg}
|
||||
end
|
||||
end
|
||||
|
||||
-- log fatal errors
|
||||
---@param msg any message
|
||||
function log.fatal(msg) _log{FTL_TAG, msg} end
|
||||
function log.fatal(msg) write_log{FTL_TAG, msg} end
|
||||
|
||||
return log
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
--
|
||||
-- Network Communications
|
||||
-- Network Communications and Message Authentication
|
||||
--
|
||||
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local md5 = require("lockbox.digest.md5")
|
||||
@ -17,7 +18,7 @@ local array = require("lockbox.util.array")
|
||||
local network = {}
|
||||
|
||||
-- cryptography engine
|
||||
local c_eng = {
|
||||
local _crypt = {
|
||||
key = nil,
|
||||
hmac = nil
|
||||
}
|
||||
@ -39,23 +40,23 @@ function network.init_mac(passkey)
|
||||
key_deriv.setPassword(passkey)
|
||||
key_deriv.finish()
|
||||
|
||||
c_eng.key = array.fromHex(key_deriv.asHex())
|
||||
_crypt.key = array.fromHex(key_deriv.asHex())
|
||||
|
||||
-- initialize HMAC
|
||||
c_eng.hmac = hmac()
|
||||
c_eng.hmac.setBlockSize(64)
|
||||
c_eng.hmac.setDigest(md5)
|
||||
c_eng.hmac.setKey(c_eng.key)
|
||||
_crypt.hmac = hmac()
|
||||
_crypt.hmac.setBlockSize(64)
|
||||
_crypt.hmac.setDigest(md5)
|
||||
_crypt.hmac.setKey(_crypt.key)
|
||||
|
||||
local init_time = util.time_ms() - start
|
||||
log.info("network.init_mac completed in " .. init_time .. "ms")
|
||||
log.info("NET: network.init_mac completed in " .. init_time .. "ms")
|
||||
|
||||
return init_time
|
||||
end
|
||||
|
||||
-- de-initialize message authentication system
|
||||
function network.deinit_mac()
|
||||
c_eng.key, c_eng.hmac = nil, nil
|
||||
_crypt.key, _crypt.hmac = nil, nil
|
||||
end
|
||||
|
||||
-- generate HMAC of message
|
||||
@ -64,29 +65,41 @@ end
|
||||
local function compute_hmac(message)
|
||||
-- local start = util.time_ms()
|
||||
|
||||
c_eng.hmac.init()
|
||||
c_eng.hmac.update(stream.fromString(message))
|
||||
c_eng.hmac.finish()
|
||||
_crypt.hmac.init()
|
||||
_crypt.hmac.update(stream.fromString(message))
|
||||
_crypt.hmac.finish()
|
||||
|
||||
local hash = c_eng.hmac.asHex()
|
||||
local hash = _crypt.hmac.asHex()
|
||||
|
||||
-- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
|
||||
-- log.debug("NET: compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
|
||||
|
||||
return hash
|
||||
end
|
||||
|
||||
-- NIC: Network Interface Controller<br>
|
||||
-- utilizes HMAC-MD5 for message authentication, if enabled
|
||||
---@param modem Modem modem to use
|
||||
-- utilizes HMAC-MD5 for message authentication, if enabled and this is wireless
|
||||
---@param modem Modem|nil modem to use
|
||||
function network.nic(modem)
|
||||
local self = {
|
||||
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
|
||||
-- modem interface name
|
||||
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 = {}
|
||||
}
|
||||
|
||||
---@class nic:Modem
|
||||
local public = {}
|
||||
|
||||
-- get the phy name
|
||||
---@nodiscard
|
||||
function public.phy_name() return self.name end
|
||||
|
||||
-- check if this NIC has a connected modem
|
||||
---@nodiscard
|
||||
function public.is_connected() return self.connected end
|
||||
@ -95,9 +108,14 @@ function network.nic(modem)
|
||||
---@param reconnected_modem Modem
|
||||
function public.connect(reconnected_modem)
|
||||
modem = reconnected_modem
|
||||
self.connected = true
|
||||
|
||||
-- open previously opened channels
|
||||
self.iface = ppm.get_iface(modem)
|
||||
self.name = util.c(util.trinary(modem.isWireless(), "WLAN_PHY", "ETH_PHY"), "{", self.iface, "}")
|
||||
self.connected = true
|
||||
self.use_hash = _crypt.hmac and modem.isWireless()
|
||||
|
||||
-- open only previously opened channels
|
||||
modem.closeAll()
|
||||
for _, channel in ipairs(self.channels) do
|
||||
modem.open(channel)
|
||||
end
|
||||
@ -117,13 +135,13 @@ function network.nic(modem)
|
||||
function public.is_modem(device) return device == modem end
|
||||
|
||||
-- wrap modem functions, then create custom functions
|
||||
public.connect(modem)
|
||||
if modem then public.connect(modem) end
|
||||
|
||||
-- open a channel on the modem<br>
|
||||
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
|
||||
---@param channel integer
|
||||
function public.open(channel)
|
||||
modem.open(channel)
|
||||
if modem then modem.open(channel) end
|
||||
|
||||
local already_open = false
|
||||
for i = 1, #self.channels do
|
||||
@ -141,7 +159,7 @@ function network.nic(modem)
|
||||
-- close a channel on the modem
|
||||
---@param channel integer
|
||||
function public.close(channel)
|
||||
modem.close(channel)
|
||||
if modem then modem.close(channel) end
|
||||
|
||||
for i = 1, #self.channels do
|
||||
if self.channels[i] == channel then
|
||||
@ -153,7 +171,7 @@ function network.nic(modem)
|
||||
|
||||
-- close all channels on the modem
|
||||
function public.closeAll()
|
||||
modem.closeAll()
|
||||
if modem then modem.closeAll() end
|
||||
self.channels = {}
|
||||
end
|
||||
|
||||
@ -165,17 +183,20 @@ function network.nic(modem)
|
||||
if self.connected then
|
||||
local tx_packet = packet ---@type authd_packet|scada_packet
|
||||
|
||||
if c_eng.hmac ~= nil then
|
||||
if self.use_hash then
|
||||
-- local start = util.time_ms()
|
||||
tx_packet = comms.authd_packet()
|
||||
|
||||
---@cast tx_packet authd_packet
|
||||
tx_packet.make(packet, compute_hmac)
|
||||
|
||||
-- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
|
||||
-- log.debug("NET: network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
|
||||
else
|
||||
log.debug("NET: network.transmit tx dropped, link is down")
|
||||
end
|
||||
end
|
||||
|
||||
@ -190,10 +211,10 @@ function network.nic(modem)
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
local packet = nil
|
||||
|
||||
if self.connected then
|
||||
if self.connected and side == self.iface then
|
||||
local s_packet = comms.scada_packet()
|
||||
|
||||
if c_eng.hmac ~= nil then
|
||||
if self.use_hash then
|
||||
-- parse packet as an authenticated SCADA packet
|
||||
local a_packet = comms.authd_packet()
|
||||
a_packet.receive(side, sender, reply_to, message, distance)
|
||||
@ -206,10 +227,10 @@ function network.nic(modem)
|
||||
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
|
||||
|
||||
if a_packet.mac() == computed_hmac then
|
||||
-- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
||||
-- log.debug("NET: network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
||||
s_packet.stamp_authenticated()
|
||||
else
|
||||
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
||||
-- log.debug("NET: network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -22,7 +22,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
|
||||
|
||||
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
|
||||
|
||||
local ppm_sys = {
|
||||
local _ppm = {
|
||||
mounts = {}, ---@type { [string]: ppm_entry }
|
||||
next_vid = 0,
|
||||
auto_cf = false,
|
||||
@ -66,7 +66,7 @@ local function peri_init(iface)
|
||||
if status then
|
||||
-- auto fault clear
|
||||
if self.auto_cf then self.faulted = false end
|
||||
if ppm_sys.auto_cf then ppm_sys.faulted = false end
|
||||
if _ppm.auto_cf then _ppm.faulted = false end
|
||||
|
||||
self.fault_counts[key] = 0
|
||||
|
||||
@ -78,10 +78,10 @@ local function peri_init(iface)
|
||||
self.faulted = true
|
||||
self.last_fault = result
|
||||
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = result
|
||||
_ppm.faulted = true
|
||||
_ppm.last_fault = result
|
||||
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
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
|
||||
|
||||
if result == "Terminated" then ppm_sys.terminate = true end
|
||||
if result == "Terminated" then _ppm.terminate = true end
|
||||
|
||||
return ACCESS_FAULT, result
|
||||
end
|
||||
@ -159,10 +159,10 @@ local function peri_init(iface)
|
||||
self.faulted = true
|
||||
self.last_fault = UNDEFINED_FIELD
|
||||
|
||||
ppm_sys.faulted = true
|
||||
ppm_sys.last_fault = UNDEFINED_FIELD
|
||||
_ppm.faulted = true
|
||||
_ppm.last_fault = UNDEFINED_FIELD
|
||||
|
||||
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
if not _ppm.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
|
||||
local count_str = ""
|
||||
if self.fault_counts[key] > 0 then
|
||||
count_str = " [" .. self.fault_counts[key] .. " total calls]"
|
||||
@ -193,35 +193,35 @@ end
|
||||
-- REPORTING --
|
||||
|
||||
-- silence error prints
|
||||
function ppm.disable_reporting() ppm_sys.mute = true end
|
||||
function ppm.disable_reporting() _ppm.mute = true end
|
||||
|
||||
-- allow error prints
|
||||
function ppm.enable_reporting() ppm_sys.mute = false end
|
||||
function ppm.enable_reporting() _ppm.mute = false end
|
||||
|
||||
-- FAULT MEMORY --
|
||||
|
||||
-- enable automatically clearing fault flag
|
||||
function ppm.enable_afc() ppm_sys.auto_cf = true end
|
||||
function ppm.enable_afc() _ppm.auto_cf = true end
|
||||
|
||||
-- disable automatically clearing fault flag
|
||||
function ppm.disable_afc() ppm_sys.auto_cf = false end
|
||||
function ppm.disable_afc() _ppm.auto_cf = false end
|
||||
|
||||
-- clear fault flag
|
||||
function ppm.clear_fault() ppm_sys.faulted = false end
|
||||
function ppm.clear_fault() _ppm.faulted = false end
|
||||
|
||||
-- check fault flag
|
||||
---@nodiscard
|
||||
function ppm.is_faulted() return ppm_sys.faulted end
|
||||
function ppm.is_faulted() return _ppm.faulted end
|
||||
|
||||
-- get the last fault message
|
||||
---@nodiscard
|
||||
function ppm.get_last_fault() return ppm_sys.last_fault end
|
||||
function ppm.get_last_fault() return _ppm.last_fault end
|
||||
|
||||
-- TERMINATION --
|
||||
|
||||
-- if a caught error was a termination request
|
||||
---@nodiscard
|
||||
function ppm.should_terminate() return ppm_sys.terminate end
|
||||
function ppm.should_terminate() return _ppm.terminate end
|
||||
|
||||
-- MOUNTING --
|
||||
|
||||
@ -229,12 +229,12 @@ function ppm.should_terminate() return ppm_sys.terminate end
|
||||
function ppm.mount_all()
|
||||
local ifaces = peripheral.getNames()
|
||||
|
||||
ppm_sys.mounts = {}
|
||||
_ppm.mounts = {}
|
||||
|
||||
for i = 1, #ifaces do
|
||||
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
_ppm.mounts[ifaces[i]] = peri_init(ifaces[i])
|
||||
|
||||
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
log.info(util.c("PPM: found a ", _ppm.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
|
||||
end
|
||||
|
||||
if #ifaces == 0 then
|
||||
@ -253,10 +253,10 @@ function ppm.mount(iface)
|
||||
|
||||
for i = 1, #ifaces do
|
||||
if iface == ifaces[i] then
|
||||
ppm_sys.mounts[iface] = peri_init(iface)
|
||||
_ppm.mounts[iface] = peri_init(iface)
|
||||
|
||||
pm_type = ppm_sys.mounts[iface].type
|
||||
pm_dev = ppm_sys.mounts[iface].dev
|
||||
pm_type = _ppm.mounts[iface].type
|
||||
pm_dev = _ppm.mounts[iface].dev
|
||||
|
||||
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
|
||||
break
|
||||
@ -278,12 +278,12 @@ function ppm.remount(iface)
|
||||
for i = 1, #ifaces do
|
||||
if iface == ifaces[i] then
|
||||
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type))
|
||||
ppm.unmount(ppm_sys.mounts[iface].dev)
|
||||
ppm.unmount(_ppm.mounts[iface].dev)
|
||||
|
||||
ppm_sys.mounts[iface] = peri_init(iface)
|
||||
_ppm.mounts[iface] = peri_init(iface)
|
||||
|
||||
pm_type = ppm_sys.mounts[iface].type
|
||||
pm_dev = ppm_sys.mounts[iface].dev
|
||||
pm_type = _ppm.mounts[iface].type
|
||||
pm_dev = _ppm.mounts[iface].dev
|
||||
|
||||
log.info(util.c("PPM: remount(", iface, ") -> remounted a ", pm_type))
|
||||
break
|
||||
@ -297,24 +297,24 @@ end
|
||||
---@nodiscard
|
||||
---@return string type, table device
|
||||
function ppm.mount_virtual()
|
||||
local iface = "ppm_vdev_" .. ppm_sys.next_vid
|
||||
local iface = "ppm_vdev_" .. _ppm.next_vid
|
||||
|
||||
ppm_sys.mounts[iface] = peri_init("__virtual__")
|
||||
ppm_sys.next_vid = ppm_sys.next_vid + 1
|
||||
_ppm.mounts[iface] = peri_init("__virtual__")
|
||||
_ppm.next_vid = _ppm.next_vid + 1
|
||||
|
||||
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
|
||||
|
||||
return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev
|
||||
return _ppm.mounts[iface].type, _ppm.mounts[iface].dev
|
||||
end
|
||||
|
||||
-- manually unmount a peripheral from the PPM
|
||||
---@param device table device table
|
||||
function ppm.unmount(device)
|
||||
if device then
|
||||
for iface, data in pairs(ppm_sys.mounts) do
|
||||
for iface, data in pairs(_ppm.mounts) do
|
||||
if data.dev == device then
|
||||
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
|
||||
ppm_sys.mounts[iface] = nil
|
||||
_ppm.mounts[iface] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
@ -330,7 +330,7 @@ function ppm.handle_unmount(iface)
|
||||
local pm_type = nil
|
||||
|
||||
-- what got disconnected?
|
||||
local lost_dev = ppm_sys.mounts[iface]
|
||||
local lost_dev = _ppm.mounts[iface]
|
||||
|
||||
if lost_dev then
|
||||
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))
|
||||
end
|
||||
|
||||
ppm_sys.mounts[iface] = nil
|
||||
_ppm.mounts[iface] = nil
|
||||
|
||||
return pm_type, pm_dev
|
||||
end
|
||||
|
||||
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
|
||||
function ppm.log_mounts()
|
||||
for iface, mount in pairs(ppm_sys.mounts) do
|
||||
for iface, mount in pairs(_ppm.mounts) do
|
||||
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
|
||||
end
|
||||
|
||||
if util.table_len(ppm_sys.mounts) == 0 then
|
||||
if util.table_len(_ppm.mounts) == 0 then
|
||||
log.warning("PPM: no devices had been found")
|
||||
end
|
||||
end
|
||||
@ -369,7 +369,7 @@ function ppm.list_avail() return peripheral.getNames() end
|
||||
---@return { [string]: ppm_entry } mounts
|
||||
function ppm.list_mounts()
|
||||
local list = {}
|
||||
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
|
||||
for k, v in pairs(_ppm.mounts) do list[k] = v end
|
||||
return list
|
||||
end
|
||||
|
||||
@ -379,7 +379,7 @@ end
|
||||
---@return string|nil iface CC peripheral interface
|
||||
function ppm.get_iface(device)
|
||||
if device then
|
||||
for iface, data in pairs(ppm_sys.mounts) do
|
||||
for iface, data in pairs(_ppm.mounts) do
|
||||
if data.dev == device then return iface end
|
||||
end
|
||||
end
|
||||
@ -392,8 +392,8 @@ end
|
||||
---@param iface string CC peripheral interface
|
||||
---@return { [string]: function }|nil device function table
|
||||
function ppm.get_periph(iface)
|
||||
if ppm_sys.mounts[iface] then
|
||||
return ppm_sys.mounts[iface].dev
|
||||
if _ppm.mounts[iface] then
|
||||
return _ppm.mounts[iface].dev
|
||||
else return nil end
|
||||
end
|
||||
|
||||
@ -402,8 +402,8 @@ end
|
||||
---@param iface string CC peripheral interface
|
||||
---@return string|nil type
|
||||
function ppm.get_type(iface)
|
||||
if ppm_sys.mounts[iface] then
|
||||
return ppm_sys.mounts[iface].type
|
||||
if _ppm.mounts[iface] then
|
||||
return _ppm.mounts[iface].type
|
||||
else return nil end
|
||||
end
|
||||
|
||||
@ -414,7 +414,7 @@ end
|
||||
function ppm.get_all_devices(name)
|
||||
local devices = {}
|
||||
|
||||
for _, data in pairs(ppm_sys.mounts) do
|
||||
for _, data in pairs(_ppm.mounts) do
|
||||
if data.type == name then
|
||||
table.insert(devices, data.dev)
|
||||
end
|
||||
@ -430,7 +430,7 @@ end
|
||||
function ppm.get_device(name)
|
||||
local device = nil
|
||||
|
||||
for _, data in pairs(ppm_sys.mounts) do
|
||||
for _, data in pairs(_ppm.mounts) do
|
||||
if data.type == name then
|
||||
device = data.dev
|
||||
break
|
||||
@ -453,9 +453,9 @@ function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAda
|
||||
---@return Modem|nil modem function table
|
||||
function ppm.get_wireless_modem()
|
||||
local w_modem = nil
|
||||
local emulated_env = true
|
||||
local emulated_env = periphemu ~= nil
|
||||
|
||||
for _, device in pairs(ppm_sys.mounts) do
|
||||
for _, device in pairs(_ppm.mounts) do
|
||||
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
|
||||
w_modem = device.dev
|
||||
break
|
||||
@ -471,7 +471,7 @@ end
|
||||
function ppm.get_monitor_list()
|
||||
local list = {}
|
||||
|
||||
for iface, device in pairs(ppm_sys.mounts) do
|
||||
for iface, device in pairs(_ppm.mounts) do
|
||||
if device.type == "monitor" then list[iface] = device end
|
||||
end
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ local t_pack = table.pack
|
||||
local util = {}
|
||||
|
||||
-- scada-common version
|
||||
util.version = "1.5.4"
|
||||
util.version = "1.5.5"
|
||||
|
||||
util.TICK_TIME_S = 0.05
|
||||
util.TICK_TIME_MS = 50
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user