Merge pull request #546 from MikaylaFischler/pocket-alpha-dev

Start of Pocket Controls
This commit is contained in:
Mikayla 2024-09-08 16:54:22 -04:00 committed by GitHub
commit a4452ebbd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1306 additions and 307 deletions

View File

@ -38,7 +38,6 @@ local MGMT_TYPE = comms.MGMT_TYPE
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
@ -1401,7 +1400,7 @@ local function config_view(display)
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end

View File

@ -387,7 +387,7 @@ function coordinator.comms(version, nic, sv_watchdog)
end
-- send the auto process control configuration with a start command
---@param auto_cfg coord_auto_config configuration
---@param auto_cfg sys_auto_config configuration
function public.send_auto_start(auto_cfg)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
@ -576,7 +576,7 @@ function coordinator.comms(version, nic, sv_watchdog)
local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMAND.START then
@ -586,11 +586,13 @@ function coordinator.comms(version, nic, sv_watchdog)
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
process.fac_ack(cmd, ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_SPS_LP then
process.sps_lp_ack_handle(packet.data[2])
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
@ -625,21 +627,15 @@ function coordinator.comms(version, nic, sv_watchdog)
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
elseif cmd == UNIT_COMMAND.SET_BURN then
unit.set_burn_ack(ack)
elseif cmd == UNIT_COMMAND.SET_WASTE then
unit.set_waste_ack(ack)
process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
elseif cmd == UNIT_COMMAND.SET_GROUP then
-- UI will be updated to display current group if changed successfully
process.unit_ack(unit_id, cmd, ack)
else
log.debug(util.c("received unit command ack with unknown command ", cmd))
log.debug(util.c("received unsupported unit command ack for command ", cmd))
end
else
log.debug(util.c("received unit command ack with unknown unit ", unit_id))

View File

@ -118,8 +118,6 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
save_cfg_ack = __generic_ack,
start_ack = __generic_ack,
stop_ack = __generic_ack,
scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
alarm_tones = { false, false, false, false, false, false, false, false },
@ -184,24 +182,17 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
turbine_flow_stable = false,
-- auto control group
a_group = 0,
a_group = types.AUTO_GROUP.MANUAL,
start = function () process.start(i) end,
scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end,
start = function () io.process.start(i) end,
scram = function () io.process.scram(i) end,
reset_rps = function () io.process.reset_rps(i) end,
ack_alarms = function () io.process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
set_waste = function (mode) process.set_unit_waste(i, mode) end, ---@param mode WASTE_MODE waste processing mode
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0 for manual
start_ack = __generic_ack,
scram_ack = __generic_ack,
reset_rps_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
set_burn_ack = __generic_ack,
set_waste_ack = __generic_ack,
alarm_callbacks = {
c_breach = { ack = function () ack(1) end, reset = function () reset(1) end },
radiation = { ack = function () ack(2) end, reset = function () reset(2) end },
@ -281,6 +272,9 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
-- coordinator's process handle
io.process = process.create_handle()
end
--#region Front Panel PSIL
@ -575,11 +569,10 @@ function iocontrol.update_facility_status(status)
local group_map = ctl_status[14]
if (type(group_map) == "table") and (#group_map == fac.num_units) then
local names = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
for i = 1, #group_map do
io.units[i].a_group = group_map[i]
io.units[i].unit_ps.publish("auto_group_id", group_map[i])
io.units[i].unit_ps.publish("auto_group", names[group_map[i] + 1])
io.units[i].unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[group_map[i] + 1])
end
end

View File

@ -7,21 +7,23 @@ local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
local F_CMD = comms.FAC_COMMAND
local U_CMD = comms.UNIT_COMMAND
local PROCESS = types.PROCESS
local PRODUCT = types.WASTE_PRODUCT
local REQUEST_TIMEOUT_MS = 10000
---@class process_controller
local process = {}
local self = {
local pctl = {
io = nil, ---@type ioctl
comms = nil, ---@type coord_comms
---@class coord_control_states
---@class sys_control_states
control_states = {
---@class coord_auto_config
---@class sys_auto_config
process = {
mode = PROCESS.INACTIVE,
burn_target = 0.0,
@ -34,28 +36,52 @@ local self = {
},
waste_modes = {},
priority_groups = {}
},
commands = {
unit = {}, ---@type process_command_state[][]
fac = {} ---@type process_command_state[]
}
}
--------------------------
-- UNIT COMMAND CONTROL --
--------------------------
---@class process_command_state
---@field active boolean if this command is live
---@field timeout integer expiration time of this command request
---@field requestors table list of callbacks from the requestors
-- write auto process control to config file
local function _write_auto_config()
-- save config
settings.set("ControlStates", pctl.control_states)
local saved = settings.save("/coordinator.settings")
if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file")
end
return saved
end
-- initialize the process controller
---@param iocontrol ioctl iocontrl system
---@param coord_comms coord_comms coordinator communications
function process.init(iocontrol, coord_comms)
self.io = iocontrol
self.comms = coord_comms
pctl.io = iocontrol
pctl.comms = coord_comms
local ctl_proc = self.control_states.process
-- create command handling objects
for _, v in pairs(F_CMD) do pctl.commands.fac[v] = { active = false, timeout = 0, requestors = {} } end
for i = 1, pctl.io.facility.num_units do
pctl.commands.unit[i] = {}
for _, v in pairs(U_CMD) do pctl.commands.unit[i][v] = { active = false, timeout = 0, requestors = {} } end
end
for i = 1, self.io.facility.num_units do
local ctl_proc = pctl.control_states.process
for i = 1, pctl.io.facility.num_units do
ctl_proc.limits[i] = 0.1
end
local ctrl_states = settings.get("ControlStates", {})
local config = ctrl_states.process ---@type coord_auto_config
local config = ctrl_states.process ---@type sys_auto_config
-- facility auto control configuration
if type(config) == "table" then
@ -68,33 +94,33 @@ function process.init(iocontrol, coord_comms)
ctl_proc.pu_fallback = config.pu_fallback
ctl_proc.sps_low_power = config.sps_low_power
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", self.io.energy_convert_from_fe(ctl_proc.charge_target))
self.io.facility.ps.publish("process_gen_target", self.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
pctl.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
pctl.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit
for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
local unit = pctl.io.units[id] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
end
log.info("PROCESS: loaded auto control settings")
-- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power)
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product)
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, ctl_proc.pu_fallback)
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, ctl_proc.sps_low_power)
end
-- unit waste states
local waste_modes = ctrl_states.waste_modes ---@type table|nil
if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do
self.control_states.waste_modes[id] = mode
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
pctl.control_states.waste_modes[id] = mode
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
end
log.info("PROCESS: loaded unit waste mode settings")
@ -104,54 +130,208 @@ function process.init(iocontrol, coord_comms)
local prio_groups = ctrl_states.priority_groups ---@type table|nil
if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do
self.control_states.priority_groups[id] = group
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
pctl.control_states.priority_groups[id] = group
pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group)
end
log.info("PROCESS: loaded priority groups settings")
end
end
-- create a handle to process control for usage of commands that get acknowledgements
function process.create_handle()
---@class process_handle
local handle = {}
-- add this handle to the requestors and activate the command if inactive
---@param cmd process_command_state
---@param ack function
local function request(cmd, ack)
local new = not cmd.active
if new then
cmd.active = true
cmd.timeout = util.time_ms() + REQUEST_TIMEOUT_MS
end
table.insert(cmd.requestors, ack)
return new
end
local function u_request(u_id, cmd_id, ack) return request(pctl.commands.unit[u_id][cmd_id], ack) end
local function f_request(cmd_id, ack) return request(pctl.commands.fac[cmd_id], ack) end
--#region Facility Commands
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
function handle.fac_scram()
if f_request(F_CMD.SCRAM_ALL, handle.fac_ack.on_scram) then
pctl.comms.send_fac_command(F_CMD.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
function handle.fac_ack_alarms()
if f_request(F_CMD.ACK_ALL_ALARMS, handle.fac_ack.on_ack_alarms) then
pctl.comms.send_fac_command(F_CMD.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
end
-- start reactor
handle.fac_ack = {}
-- luacheck: no unused args
-- facility SCRAM ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_scram(success) end
-- facility acknowledge all alarms ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function handle.fac_ack.on_ack_alarms(success) end
-- luacheck: unused args
--#endregion
--#region Unit Commands
-- start a reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMAND.START, id)
function handle.start(id)
if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then
pctl.io.units[id].control_state = true
pctl.comms.send_unit_command(U_CMD.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
end
-- SCRAM reactor
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
function handle.scram(id)
if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then
pctl.io.units[id].control_state = false
pctl.comms.send_unit_command(U_CMD.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
function handle.reset_rps(id)
if u_request(id, U_CMD.RESET_RPS, handle.unit_ack[id].on_rps_reset) then
pctl.comms.send_unit_command(U_CMD.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
end
-- acknowledge all alarms
---@param id integer unit ID
function handle.ack_all_alarms(id)
if u_request(id, U_CMD.ACK_ALL_ALARMS, handle.unit_ack[id].on_ack_alarms) then
pctl.comms.send_unit_command(U_CMD.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
end
-- unit command acknowledgement callbacks, indexed by unit ID
---@type process_unit_ack[]
handle.unit_ack = {}
for u = 1, pctl.io.facility.num_units do
handle.unit_ack[u] = {}
---@class process_unit_ack
local u_ack = handle.unit_ack[u]
-- luacheck: no unused args
-- unit start ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_start(success) end
-- unit SCRAM ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_scram(success) end
-- unit RPS reset ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_rps_reset(success) end
-- unit acknowledge all alarms ack, override to implement
---@param success boolean
---@diagnostic disable-next-line: unused-local
function u_ack.on_ack_alarms(success) end
-- luacheck: unused args
end
--#endregion
return handle
end
-- clear outstanding process commands that have timed out
function process.clear_timed_out()
local now = util.time_ms()
local objs = { pctl.commands.fac, table.unpack(pctl.commands.unit) }
for _, obj in pairs(objs) do
-- cancel expired requests
for _, cmd in pairs(obj) do
if cmd.active and now > cmd.timeout then
cmd.active = false
cmd.requestors = {}
end
end
end
end
-- handle a command acknowledgement
---@param cmd_state process_command_state
---@param success boolean if the command was successful
local function cmd_ack(cmd_state, success)
if cmd_state.active then
cmd_state.active = false
-- call all acknowledge callback functions
for i = 1, #cmd_state.requestors do
cmd_state.requestors[i](success)
end
cmd_state.requestors = {}
end
end
-- handle a facility command acknowledgement
---@param command FAC_COMMAND command
---@param success boolean if the command was successful
function process.fac_ack(command, success)
cmd_ack(pctl.commands.fac[command], success)
end
-- handle a unit command acknowledgement
---@param unit integer unit ID
---@param command UNIT_COMMAND command
---@param success boolean if the command was successful
function process.unit_ack(unit, command, success)
cmd_ack(pctl.commands.unit[unit][command], success)
end
--#region One-Way Commands (no acknowledgements)
-- set burn rate
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
pctl.comms.send_unit_command(U_CMD.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
@ -160,31 +340,24 @@ end
---@param mode integer waste mode
function process.set_unit_waste(id, mode)
-- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
pctl.io.units[id].unit_ps.publish("U_WasteMode", mode)
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
self.control_states.waste_modes[id] = mode
settings.set("ControlStates", self.control_states)
pctl.control_states.waste_modes[id] = mode
settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then
log.error("process.set_unit_waste(): failed to save coordinator settings file")
end
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
pctl.comms.send_unit_command(U_CMD.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
@ -192,7 +365,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
pctl.comms.send_unit_command(U_CMD.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
@ -200,78 +373,68 @@ end
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
pctl.comms.send_unit_command(U_CMD.SET_GROUP, unit_id, group_id)
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
self.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", self.control_states)
pctl.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then
log.error("process.set_group(): failed to save coordinator settings file")
end
end
--#endregion
--------------------------
-- AUTO PROCESS CONTROL --
--------------------------
-- write auto process control to config file
local function _write_auto_config()
-- save config
settings.set("ControlStates", self.control_states)
local saved = settings.save("/coordinator.settings")
if not saved then
log.warning("process._write_auto_config(): failed to save coordinator settings file")
end
return saved
-- start automatic process control
function process.start_auto()
pctl.comms.send_auto_start(pctl.control_states.process)
log.debug("PROCESS: START AUTO CTL")
end
-- stop automatic process control
function process.stop_auto()
self.comms.send_fac_command(FAC_COMMAND.STOP)
pctl.comms.send_fac_command(F_CMD.STOP)
log.debug("PROCESS: STOP AUTO CTL")
end
-- start automatic process control
function process.start_auto()
self.comms.send_auto_start(self.control_states.process)
log.debug("PROCESS: START AUTO CTL")
end
-- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product)
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, product)
log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save
self.control_states.process.waste_product = product
pctl.control_states.process.waste_product = product
_write_auto_config()
end
-- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
pctl.comms.send_fac_command(F_CMD.SET_PU_FB, enabled)
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save
self.control_states.process.pu_fallback = enabled
pctl.control_states.process.pu_fallback = enabled
_write_auto_config()
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled)
pctl.comms.send_fac_command(F_CMD.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
self.control_states.process.sps_low_power = enabled
pctl.control_states.process.sps_low_power = enabled
_write_auto_config()
end
@ -285,7 +448,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE")
-- update config table
local ctl_proc = self.control_states.process
local ctl_proc = pctl.control_states.process
ctl_proc.mode = mode
ctl_proc.burn_target = burn_target
ctl_proc.charge_target = charge_target
@ -293,7 +456,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
ctl_proc.limits = limits
-- save config
self.io.facility.save_cfg_ack(_write_auto_config())
pctl.io.facility.save_cfg_ack(_write_auto_config())
end
-- handle a start command acknowledgement
@ -301,39 +464,46 @@ end
function process.start_ack_handle(response)
local ack = response[1]
local ctl_proc = self.control_states.process
local ctl_proc = pctl.control_states.process
ctl_proc.mode = response[2]
ctl_proc.burn_target = response[3]
ctl_proc.charge_target = response[4]
ctl_proc.gen_target = response[5]
for i = 1, math.min(#response[6], self.io.facility.num_units) do
for i = 1, math.min(#response[6], pctl.io.facility.num_units) do
ctl_proc.limits[i] = response[6][i]
local unit = self.io.units[i] ---@type ioctl_unit
local unit = pctl.io.units[i] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
end
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
self.io.facility.ps.publish("process_charge_target", self.io.energy_convert_from_fe(ctl_proc.charge_target))
self.io.facility.ps.publish("process_gen_target", self.io.energy_convert_from_fe(ctl_proc.gen_target))
pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
pctl.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
pctl.io.facility.ps.publish("process_charge_target", pctl.io.energy_convert_from_fe(ctl_proc.charge_target))
pctl.io.facility.ps.publish("process_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.start_ack(ack)
pctl.io.facility.start_ack(ack)
end
-- record waste product state after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state
-- record waste product settting after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product settting
function process.waste_ack_handle(response)
self.control_states.process.waste_product = response
self.io.facility.ps.publish("process_waste_product", response)
pctl.control_states.process.waste_product = response
pctl.io.facility.ps.publish("process_waste_product", response)
end
-- record plutonium fallback state after attempting to change it
---@param response boolean supervisor plutonium fallback state
-- record plutonium fallback settting after attempting to change it
---@param response boolean supervisor plutonium fallback settting
function process.pu_fb_ack_handle(response)
self.control_states.process.pu_fallback = response
self.io.facility.ps.publish("process_pu_fallback", response)
pctl.control_states.process.pu_fallback = response
pctl.io.facility.ps.publish("process_pu_fallback", response)
end
-- record SPS low power settting after attempting to change it
---@param response boolean supervisor SPS low power settting
function process.sps_lp_ack_handle(response)
pctl.control_states.process.sps_low_power = response
pctl.io.facility.ps.publish("process_sps_low_power", response)
end
return process

View File

@ -4,12 +4,15 @@ local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local pocket = {}
local PROTOCOL = comms.PROTOCOL
local CRDN_TYPE = comms.CRDN_TYPE
local MGMT_TYPE = comms.MGMT_TYPE
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
@ -37,7 +40,7 @@ local PERIODICS = {
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local log_header = "pkt_session(" .. id .. "): "
local log_tag = "pkt_session(" .. id .. "): "
local self = {
-- connection properties
@ -46,6 +49,8 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
connected = true,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
-- process accessor handle
proc_handle = process.create_handle(),
-- periodic messages
periodics = {
last_update = 0,
@ -101,12 +106,24 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
self.seq_num = self.seq_num + 1
end
-- link callback transmissions
self.proc_handle.fac_ack.on_scram = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.SCRAM_ALL, success }) end
self.proc_handle.fac_ack.on_ack_alarms = function (success) _send(CRDN_TYPE.FAC_CMD, { FAC_COMMAND.ACK_ALL_ALARMS, success }) end
for u = 1, iocontrol.get_db().facility.num_units do
self.proc_handle.unit_ack[u].on_start = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.START, u, success }) end
self.proc_handle.unit_ack[u].on_scram = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.SCRAM, u, success }) end
self.proc_handle.unit_ack[u].on_rps_reset = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.RESET_RPS, u, success }) end
self.proc_handle.unit_ack[u].on_ack_alarms = function (success) _send(CRDN_TYPE.UNIT_CMD, { UNIT_COMMAND.ACK_ALL_ALARMS, u, success }) end
end
-- handle a packet
---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt)
-- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then
log.warning(log_header .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
log.warning(log_tag .. "sequence out-of-order: next = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num())
return
else
self.r_seq_num = pkt.scada_frame.seq_num() + 1
@ -122,7 +139,68 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
local db = iocontrol.get_db()
-- handle packet by type
if pkt.type == CRDN_TYPE.API_GET_FAC then
if pkt.type == CRDN_TYPE.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
if cmd == FAC_COMMAND.SCRAM_ALL then
log.info(log_tag .. "FAC SCRAM ALL")
self.proc_handle.fac_scram()
elseif cmd == FAC_COMMAND.STOP then
elseif cmd == FAC_COMMAND.START then
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
log.info(log_tag .. "FAC ACK ALL ALARMS")
self.proc_handle.fac_ack_alarms()
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
elseif cmd == FAC_COMMAND.SET_PU_FB then
elseif cmd == FAC_COMMAND.SET_SPS_LP then
else
log.debug(log_tag .. "CRDN facility command unknown")
end
else
log.debug(log_tag .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.UNIT_CMD then
if pkt.length >= 2 then
-- get command and unit id
local cmd = pkt.data[1]
local uid = pkt.data[2]
-- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #db.units then
if cmd == UNIT_COMMAND.SCRAM then
log.info(util.c(log_tag, "UNIT[", uid, "] SCRAM"))
self.proc_handle.scram(uid)
elseif cmd == UNIT_COMMAND.START then
log.info(util.c(log_tag, "UNIT[", uid, "] START"))
self.proc_handle.start(uid)
elseif cmd == UNIT_COMMAND.RESET_RPS then
log.info(util.c(log_tag, "UNIT[", uid, "] RESET RPS"))
self.proc_handle.reset_rps(uid)
elseif cmd == UNIT_COMMAND.SET_BURN then
if pkt.length == 3 then
log.info(util.c(log_tag, "UNIT[", uid, "] SET BURN ", pkt.data[3]))
process.set_rate(uid, pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
log.info(util.c(log_tag, "UNIT[", uid, "] ACK ALL ALARMS"))
self.proc_handle.ack_all_alarms(uid)
elseif cmd == UNIT_COMMAND.ACK_ALARM then
elseif cmd == UNIT_COMMAND.RESET_ALARM then
elseif cmd == UNIT_COMMAND.SET_GROUP then
else
log.debug(log_tag .. "CRDN unit command unknown")
end
else
log.debug(log_tag .. "CRDN unit command invalid")
end
else
log.debug(log_tag .. "CRDN unit command packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.API_GET_FAC then
local fac = db.facility
local data = {
@ -146,6 +224,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
u.unit_id,
u.connected,
u.rtu_hw,
u.a_group,
u.alarms,
u.annunciator,
u.reactor_data,
@ -160,7 +239,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
end
end
else
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported CRDN packet type " .. pkt.type)
end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame
@ -173,7 +252,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
log.warning(log_tag .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
@ -181,7 +260,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
iocontrol.fp_pkt_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end
elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session
@ -189,9 +268,9 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session
_close()
log.warning(log_header .. "terminated session due to an unexpected ESTABLISH packet")
log.warning(log_tag .. "terminated session due to an unexpected ESTABLISH packet")
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
log.debug(log_tag .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
end
end
@ -216,7 +295,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
function public.close()
_close()
_send_mgmt(MGMT_TYPE.CLOSE, {})
log.info(log_header .. "session closed by server")
log.info(log_tag .. "session closed by server")
end
-- iterate the session
@ -247,14 +326,14 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
log.warning(log_header .. "exceeded 100ms queue process limit")
log.warning(log_tag .. "exceeded 100ms queue process limit")
break
end
end
-- exit if connection was closed
if not self.connected then
log.info(log_header .. "session closed by remote host")
log.info(log_tag .. "session closed by remote host")
return self.connected
end

View File

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

View File

@ -6,6 +6,7 @@ local util = require("scada-common.util")
local coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
@ -147,6 +148,9 @@ function threads.thread__main(smem)
apisessions.iterate_all()
apisessions.free_all_closed()
-- clear timed out process commands
process.clear_timed_out()
if renderer.ui_ready() then
-- update clock used on main and flow monitors
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format))

View File

@ -63,11 +63,11 @@ local function new_view(root, x, y)
local main = Div{parent=root,width=128,height=24,x=x,y=y}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=db.process.fac_scram,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=db.process.fac_ack_alarms,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response
facility.ack_alarms_ack = ack_a.on_response
db.process.fac_ack.on_scram = scram.on_response
db.process.fac_ack.on_ack_alarms = ack_a.on_response
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=style.ind_bkg,c2=ind_yel.fgd,c3=ind_grn.fgd}

View File

@ -29,6 +29,8 @@ local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN
local cpair = core.cpair
@ -373,16 +375,16 @@ local function init(parent, id)
local scram = HazardButton{parent=main,x=2,y=32,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,fg_bg=hzd_fg_bg}
local reset = HazardButton{parent=main,x=22,y=32,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,fg_bg=hzd_fg_bg}
unit.start_ack = start.on_response
unit.scram_ack = scram.on_response
unit.reset_rps_ack = reset.on_response
unit.ack_alarms_ack = ack_a.on_response
db.process.unit_ack[id].on_start = start.on_response
db.process.unit_ack[id].on_scram = scram.on_response
db.process.unit_ack[id].on_rps_reset = reset.on_response
db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
local function start_button_en_check()
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == 0)
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
end
@ -486,9 +488,7 @@ local function init(parent, id)
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
local group = RadioButton{parent=auto_div,options=types.AUTO_GROUP_NAMES,callback=function()end,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.purple}
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
@ -523,10 +523,10 @@ local function init(parent, id)
-- enable/disable controls based on group assignment (start button is separate)
burn_rate.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then burn_rate.enable() else burn_rate.disable() end
if gid == AUTO_GROUP.MANUAL then burn_rate.enable() else burn_rate.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end)
-- can't change group if auto is engaged regardless of if this unit is part of auto control

View File

@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "2.3.3"
core.version = "2.3.4"
core.flasher = flasher
core.events = events
@ -123,15 +123,17 @@ end
-- Interactive Field Manager
---@param e graphics_base
---@param max_len any
---@param fg_bg any
---@param dis_fg_bg any
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
---@param e graphics_base element
---@param max_len any max value length
---@param fg_bg any enabled fg/bg
---@param dis_fg_bg any disabled fg/bg
---@param align_right any true to align content right while unfocused
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg, align_right)
local self = {
frame_start = 1,
visible_text = e.value,
cursor_pos = string.len(e.value) + 1,
align_offset = 0,
selected_all = false
}
@ -186,7 +188,12 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
e.w_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1)
local function _write()
local function _write(align_r)
if align_r and string.len(self.visible_text) <=e.frame.w then
self.align_offset = (e.frame.w - string.len(self.visible_text))
e.w_set_cur((e.frame.w - string.len(self.visible_text)) + 1, 1)
end
if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else
@ -226,15 +233,27 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
self.selected_all = false
-- write text without cursor
_write()
_write(align_right)
end
end
-- move cursor to x
-- get an x value to pass to move_cursor taking into account right alignment offset present when unfocused
---@param x integer
function public.get_cursor_align_shift(x)
return math.max(0, x - self.align_offset)
end
-- move cursor to x
---@param x integer x position or 0 to jump to the end
function public.move_cursor(x)
self.selected_all = false
if x <= 0 then
self.cursor_pos = string.len(self.visible_text) + 1
else
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
end
public.show()
end

View File

@ -574,6 +574,15 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
---@return graphics_element
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
-- get all children
---@nodiscard
---@return table children table of graphics_element objects
function public.get_children()
local list = {}
for k, v in pairs(protected.children) do list[k] = v.get() end
return list
end
-- remove a child element
---@param id element_id
function public.remove(id)

View File

@ -10,6 +10,7 @@ local element = require("graphics.element")
---@field accent color accent color for hazard border
---@field dis_colors? cpair text color and border color when disabled
---@field callback function function to call on touch
---@field timeout? integer override for the default 1.5 second timeout, in seconds
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
@ -28,6 +29,8 @@ local function hazard_button(args)
args.height = 3
args.width = string.len(args.text) + 4
local timeout = args.timeout or 1.5
-- create new graphics element base object
local e = element.new(args)
@ -149,8 +152,8 @@ local function hazard_button(args)
tcd.abort(on_success)
tcd.abort(on_failure)
-- 1.5 second timeout
tcd.dispatch(1.5, on_timeout)
-- operation timeout animation
tcd.dispatch(timeout, on_timeout)
args.callback()
end

View File

@ -17,6 +17,7 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers
---@field align_right? boolean true to align right while unfocused
---@field dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element
---@field id? string element id
@ -47,7 +48,7 @@ local function number_field(args)
e.value = "" .. (args.default or 0)
-- make an interactive field manager
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg)
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg, args.align_right)
-- handle mouse interaction
---@param event mouse_interaction mouse event
@ -55,10 +56,16 @@ local function number_field(args)
-- only handle if on an increment or decrement arrow
if e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) then
local x = event.current.x
if not e.is_focused() then
x = ifield.get_cursor_align_shift(x)
end
e.take_focus()
if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x)
ifield.move_cursor(x)
end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all()

View File

@ -27,7 +27,6 @@ local tri = util.trinary
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
@ -536,7 +535,7 @@ local function config_view(display)
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end

View File

@ -3,11 +3,12 @@
--
local const = require("scada-common.constants")
-- local log = require("scada-common.log")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
local util = require("scada-common.util")
local process = require("pocket.process")
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
@ -38,13 +39,24 @@ local io = {
ps = psil.create()
}
-- luacheck: no unused args
-- placeholder acknowledge function for type hinting
---@param success boolean
---@diagnostic disable-next-line: unused-local
local function __generic_ack(success) end
-- luacheck: unused args
local config = nil ---@type pkt_config
local comms = nil ---@type pocket_comms
-- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms
---@param pkt_comms pocket_comms
---@param nav pocket_nav
---@param cfg pkt_config
function iocontrol.init_core(comms, nav, cfg)
function iocontrol.init_core(pkt_comms, nav, cfg)
comms = pkt_comms
config = cfg
io.nav = nav
@ -154,6 +166,11 @@ function iocontrol.init_fac(conf)
radiation = types.new_zero_radiation_reading(),
start_ack = __generic_ack,
stop_ack = __generic_ack,
scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
ps = psil.create(),
induction_ps_tbl = {},
@ -298,7 +315,18 @@ function iocontrol.init_fac(conf)
turbine_flow_stable = false,
-- auto control group
a_group = 0,
a_group = types.AUTO_GROUP.MANUAL,
start = function () process.start(i) end,
scram = function () process.scram(i) end,
reset_rps = function () process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(i) end,
set_burn = function (rate) process.set_rate(i, rate) end, ---@param rate number burn rate
start_ack = __generic_ack,
scram_ack = __generic_ack,
reset_rps_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
---@type alarms
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
@ -346,6 +374,9 @@ function iocontrol.init_fac(conf)
table.insert(io.units, entry)
end
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
end
-- set network link state
@ -458,11 +489,15 @@ function iocontrol.record_unit_data(data)
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.alarms = data[4]
unit.a_group = data[4]
unit.alarms = data[5]
unit.unit_ps.publish("auto_group_id", unit.a_group)
unit.unit_ps.publish("auto_group", types.AUTO_GROUP_NAMES[unit.a_group + 1])
--#region Annunciator
unit.annunciator = data[5]
unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
@ -540,7 +575,7 @@ function iocontrol.record_unit_data(data)
--#region Reactor Data
unit.reactor_data = data[6]
unit.reactor_data = data[7]
local control_status = 1
local reactor_status = 1
@ -612,7 +647,7 @@ function iocontrol.record_unit_data(data)
--#region RTU Devices
unit.boiler_data_tbl = data[7]
unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
@ -645,7 +680,7 @@ function iocontrol.record_unit_data(data)
ps.publish("BoilerStateStatus", computed_status)
end
unit.turbine_data_tbl = data[8]
unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
@ -680,16 +715,16 @@ function iocontrol.record_unit_data(data)
ps.publish("TurbineStateStatus", computed_status)
end
unit.tank_data_tbl = data[9]
unit.tank_data_tbl = data[10]
unit.last_rate_change_ms = data[10]
unit.turbine_flow_stable = data[11]
unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[12]
--#endregion
--#region Status Information Display
local ecam = {} -- aviation reference :) back to VATSIM I go...
local ecam = {} -- aviation reference :)
-- local function red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } end

View File

@ -9,6 +9,8 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
local LINK_STATE = iocontrol.LINK_STATE
@ -84,13 +86,14 @@ local APP_ID = {
LOADER = 2,
-- main app pages
UNITS = 3,
GUIDE = 4,
ABOUT = 5,
CONTROL = 4,
GUIDE = 5,
ABOUT = 6,
-- diag app page
ALARMS = 6,
ALARMS = 7,
-- other
DUMMY = 7,
NUM_APPS = 7
DUMMY = 8,
NUM_APPS = 8
}
pocket.APP_ID = APP_ID
@ -543,6 +546,21 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)
function public.send_fac_command(cmd, option)
_send_api(CRDN_TYPE.FAC_CMD, { cmd, option })
end
-- send a unit command
---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate)
function public.send_unit_command(cmd, unit, option)
_send_api(CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end
-- parse a packet
---@param side string
---@param sender integer
@ -583,7 +601,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0))
if not ok then
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d"
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.scada_frame.length()))
log.debug(util.sprintf(fmt, packet.scada_frame.remote_channel(), packet.scada_frame.protocol(), packet.type, length, packet.length))
end
return ok
end
@ -628,12 +646,56 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
if self.api.linked then
if packet.type == CRDN_TYPE.API_GET_FAC then
if packet.type == CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
elseif cmd == FAC_COMMAND.STOP then
elseif cmd == FAC_COMMAND.START then
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
elseif cmd == FAC_COMMAND.SET_PU_FB then
elseif cmd == FAC_COMMAND.SET_SPS_LP then
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
elseif packet.type == CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement
if packet.length == 3 then
local cmd = packet.data[1]
local unit_id = packet.data[2]
local ack = packet.data[3] == true
local unit = iocontrol.get_db().units[unit_id] ---@type pioctl_unit
if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
else
log.debug(util.c("received unsupported unit command ack for command ", cmd))
end
end
end
elseif packet.type == CRDN_TYPE.API_GET_FAC then
if _check_length(packet, 11) then
iocontrol.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 11) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
iocontrol.record_unit_data(packet.data)
end
else _fail_type(packet) end

94
pocket/process.lua Normal file
View File

@ -0,0 +1,94 @@
--
-- Process Control Management
--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
---@class pocket_process_controller
local process = {}
local self = {
io = nil, ---@type ioctl
comms = nil ---@type pocket_comms
}
-- initialize the process controller
---@param iocontrol pocket_ioctl iocontrl system
---@param pocket_comms pocket_comms pocket communications
function process.init(iocontrol, pocket_comms)
self.io = iocontrol
self.comms = pocket_comms
end
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
log.debug("PROCESS: FAC SCRAM ALL")
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
log.debug("PROCESS: FAC ACK ALL ALARMS")
end
-- start reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMAND.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
-- SCRAM reactor
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
-- set burn rate
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
-- reset an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
return process

View File

@ -20,7 +20,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.11.9-alpha"
local POCKET_VERSION = "v0.12.0-alpha"
local println = util.println
local println_ts = util.println_ts

233
pocket/ui/apps/control.lua Normal file
View File

@ -0,0 +1,233 @@
--
-- Unit Control Page
--
local types = require("scada-common.types")
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local process = require("pocket.process")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
local HazardButton = require("graphics.elements.controls.hazard_button")
local PushButton = require("graphics.elements.controls.push_button")
local NumberField = require("graphics.elements.form.number_field")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local mode_states = style.icon_states.mode_states
local hzd_fg_bg = cpair(colors.white, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray)
-- new unit control page view
---@param root graphics_element parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.CONTROL, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.green,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
local btn_fg_bg = cpair(colors.green, colors.black)
local btn_active = cpair(colors.white, colors.black)
local page_div = nil ---@type nil|graphics_element
-- set sidebar to display unit-specific fields based on a specified unit
local function set_sidebar()
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end },
{ label = "FAC", color = core.cpair(colors.black, colors.orange), callback = function () app.switcher(db.facility.num_units + 1) end }
}
for i = 1, db.facility.num_units do
table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
end
app.set_sidebar(list)
end
-- load the app (create the elements)
local function load()
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {}
local active_unit = 1
-- create all page divs
for _ = 1, db.facility.num_units + 1 do
local div = Div{parent=page_div}
table.insert(panes, div)
end
-- previous unit
local function prev(x)
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
app.switcher(active_unit)
end
-- next unit
local function next(x)
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
app.switcher(active_unit)
end
for i = 1, db.facility.num_units do
local u_pane = panes[i]
local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- refresh data callback, every 500ms it will re-send the query
local last_update = 0
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_unit(i)
last_update = util.time_ms()
end
end
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
local rate = DataIndicator{parent=u_div,y=3,lu_colors=lu_col,label="Burn",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit=db.temp_label,format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local ctrl = IconIndicator{parent=u_div,x=1,y=6,label="Control State",states=mode_states}
rate.register(u_ps, "act_burn_rate", rate.update)
temp.register(u_ps, "temp", function (t) temp.update(db.temp_convert(t)) end)
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
u_div.line_break()
TextBox{parent=u_div,y=8,text="CMD",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=u_div,x=14,y=8,text="mB/t",width=4,fg_bg=cpair(colors.lightGray,colors.black)}
local burn_cmd = NumberField{parent=u_div,x=5,y=8,width=8,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.lightGray)}
local set_burn = function () unit.set_burn(burn_cmd.get_value()) end
local set_burn_btn = PushButton{parent=u_div,x=19,y=8,text="SET",min_width=5,fg_bg=cpair(colors.green,colors.black),active_fg_bg=cpair(colors.white,colors.black),dis_fg_bg=cpair(colors.gray,colors.black),callback=set_burn}
-- enable/disable controls based on group assignment (start button is separate)
burn_cmd.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then burn_cmd.enable() else burn_cmd.disable() end
end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid)
if gid == AUTO_GROUP.MANUAL then set_burn_btn.enable() else set_burn_btn.disable() end
end)
burn_cmd.register(u_ps, "burn_rate", burn_cmd.set_value)
burn_cmd.register(u_ps, "max_burn", burn_cmd.set_max)
local start = HazardButton{parent=u_div,x=2,y=11,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=u_div,x=12,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
local scram = HazardButton{parent=u_div,x=2,y=15,text="SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=unit.scram,timeout=3,fg_bg=hzd_fg_bg}
local reset = HazardButton{parent=u_div,x=12,y=15,text="RESET",accent=colors.red,dis_colors=dis_colors,callback=unit.reset_rps,timeout=3,fg_bg=hzd_fg_bg}
unit.start_ack = start.on_response
unit.ack_alarms_ack = ack_a.on_response
unit.scram_ack = scram.on_response
unit.reset_rps_ack = reset.on_response
local function start_button_en_check()
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) and
(unit.a_group == AUTO_GROUP.MANUAL)
if can_start then start.enable() else start.disable() end
end
end
start.register(u_ps, "status", start_button_en_check)
start.register(u_ps, "rps_tripped", start_button_en_check)
start.register(u_ps, "auto_group_id", start_button_en_check)
start.register(u_ps, "AutoControl", start_button_en_check)
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
util.nop()
end
-- facility controls
local f_pane = panes[db.facility.num_units + 1]
local f_div = Div{parent=f_pane,x=2,width=main.get_width()-2}
app.new_page(nil, db.facility.num_units + 1)
TextBox{parent=f_div,y=1,text="Facility Commands",alignment=ALIGN.CENTER}
local scram = HazardButton{parent=f_div,x=5,y=6,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,timeout=3,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=f_div,x=7,y=11,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,timeout=3,fg_bg=hzd_fg_bg}
db.facility.scram_ack = scram.on_response
db.facility.ack_alarms_ack = ack_a.on_response
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
set_sidebar()
-- done, show the app
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(APP_ID.ROOT) end } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@ -3,6 +3,7 @@
--
local util = require("scada-common.util")
local log = require("scada-common.log")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
@ -78,6 +79,7 @@ local function new_view(root)
local uis_page = app.new_page(main_page, 4)
local fps_page = app.new_page(main_page, 5)
local gls_page = app.new_page(main_page, 6)
local lnk_page = app.new_page(main_page, 7)
local home = Div{parent=page_div,x=2}
local search = Div{parent=page_div,x=2}
@ -85,7 +87,8 @@ local function new_view(root)
local uis = Div{parent=page_div,x=2,width=p_width}
local fps = Div{parent=page_div,x=2,width=p_width}
local gls = Div{parent=page_div,x=2,width=p_width}
local panes = { home, search, use, uis, fps, gls }
local lnk = Div{parent=page_div,x=2,width=p_width}
local panes = { home, search, use, uis, fps, gls, lnk }
local doc_map = {}
local search_db = {}
@ -100,6 +103,7 @@ local function new_view(root)
PushButton{parent=home,text="Operator UIs >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=uis_page.nav_to}
PushButton{parent=home,text="Front Panels >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fps_page.nav_to}
PushButton{parent=home,text="Glossary >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_page.nav_to}
PushButton{parent=home,y=10,text="Wiki and Discord >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=lnk_page.nav_to}
TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
@ -113,34 +117,41 @@ local function new_view(root)
function func_ref.run_search()
local query = string.lower(query_field.get_value())
local s_results = { {}, {}, {} }
local s_results = { {}, {}, {}, {} }
search_results.remove_all()
if string.len(query) < 3 then
TextBox{parent=search_results,text="Search requires at least 3 characters."}
if string.len(query) < 2 then
TextBox{parent=search_results,text="Search requires at least 2 characters."}
return
end
local start = util.time_ms()
for _, entry in ipairs(search_db) do
local s_start, _ = string.find(entry[1], query, 1, true)
local s_start, s_end = string.find(entry[1], query, 1, true)
if s_start == nil then
elseif s_start == 1 then
-- best match, start of key
if s_end == string.len(entry[1]) then
-- best match: full match
table.insert(s_results[1], entry)
else
-- very good match, start of key
table.insert(s_results[2], entry)
end
elseif string.sub(query, s_start - 1, s_start) == " " then
-- start of word, good match
table.insert(s_results[2], entry)
table.insert(s_results[3], entry)
else
-- basic match in content
table.insert(s_results[3], entry)
table.insert(s_results[4], entry)
end
end
local empty = true
for tier = 1, 3 do
for tier = 1, 4 do
for idx = 1, #s_results[tier] do
local entry = s_results[tier][idx]
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)}
@ -150,6 +161,8 @@ local function new_view(root)
end
end
log.debug("App.Guide: search for \"" .. query .. "\" completed in " .. (util.time_ms() - start) .. "ms")
if empty then
TextBox{parent=search_results,text="No results found."}
end
@ -188,7 +201,8 @@ local function new_view(root)
local unit_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.unit.fac_section, 100)
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.facility.main_section, 110)
PushButton{parent=annunc_div,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
@ -199,21 +213,39 @@ local function new_view(root)
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 180)
local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
local fp_supervisor_page = guide_section(sect_construct_data, fps_page, "Supervisor", docs.fp.supervisor, 160)
PushButton{parent=fps,y=3,text="Common Items >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_common_page.nav_to}
PushButton{parent=fps,text="Reactor PLC >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rplc_page.nav_to}
PushButton{parent=fps,text="RTU Gateway >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_rtu_page.nav_to}
PushButton{parent=fps,text="Supervisor >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fp_supervisor_page.nav_to}
PushButton{parent=fps,text="Coordinator >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,dis_fg_bg=btn_disable,callback=function()end}.disable()
TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 130)
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 140)
local gls_term_page = guide_section(sect_construct_data, gls_page, "Terminology", docs.glossary.terms, 100)
PushButton{parent=gls,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
TextBox{parent=lnk,y=1,text="Wiki and Discord",alignment=ALIGN.CENTER}
PushButton{parent=lnk,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
lnk.line_break()
TextBox{parent=lnk,text="GitHub",fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada"}
lnk.line_break()
TextBox{parent=lnk,text="Wiki",fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=lnk,text="https://github.com/MikaylaFischler/cc-mek-scada/wiki"}
lnk.line_break()
TextBox{parent=lnk,text="Discord",fg_bg=cpair(colors.lightGray,colors.black)}
TextBox{parent=lnk,text="discord.gg/R9NSCkhcwt"}
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)

View File

@ -1,13 +1,65 @@
local const = require("scada-common.constants")
local docs = {}
---@enum DOC_ITEM_TYPE
local DOC_ITEM_TYPE = {
SECTION = 1,
SUBSECTION = 2,
TEXT = 3,
LIST = 4
}
---@enum DOC_LIST_TYPE
local DOC_LIST_TYPE = {
BULLET = 1,
NUMBERED = 2,
INDICATOR = 3,
LED = 4
}
docs.DOC_ITEM_TYPE = DOC_ITEM_TYPE
docs.DOC_LIST_TYPE = DOC_LIST_TYPE
local target
local function doc(key, name, desc)
---@class pocket_doc_item
local item = { key = key, name = name, desc = desc }
local function sect(name)
---@class pocket_doc_sect
local item = { type = DOC_ITEM_TYPE.SECTION, name = name }
table.insert(target, item)
end
---@param key string item identifier for linking
---@param name string item name for display
---@param text_a string text body, or the subtitle/note if text_b is specified
---@param text_b? string text body if subtitle/note was specified
local function doc(key, name, text_a, text_b)
if text_b == nil then
text_b = text_a
---@diagnostic disable-next-line: cast-local-type
text_a = nil
end
---@class pocket_doc_subsect
local item = { type = DOC_ITEM_TYPE.SUBSECTION, key = key, name = name, subtitle = text_a, body = text_b }
table.insert(target, item)
end
local function text(body)
---@class pocket_doc_text
local item = { type = DOC_ITEM_TYPE.TEXT, text = body }
table.insert(target, item)
end
---@param type DOC_LIST_TYPE
---@param items table
---@param colors table|nil colors for indicators or nil for normal lists
local function list(type, items, colors)
---@class pocket_doc_list
local list_def = { type = DOC_ITEM_TYPE.LIST, list_type = type, items = items, colors = colors }
table.insert(target, list_def)
end
-- important to note in the future: The PLC should always be in a chunk with the reactor to ensure it can protect it on chunk load if you do not keep it all chunk loaded
docs.alarms = {}
@ -28,15 +80,20 @@ doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due
docs.annunc = {
unit = {
main_section = {}, rps_section = {}, rcs_section = {}, fac_section = {}
main_section = {}, rps_section = {}, rcs_section = {}
},
facility = {
main_section = {}
}
}
target = docs.annunc.unit.main_section
sect("Unit Status")
doc("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.")
doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.")
doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.")
sect("Safety Status")
doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is holding the reactor SCRAM'd.")
doc("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.")
doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.")
@ -78,21 +135,112 @@ doc("TurbineOverSpeed", "Turbine Over Speed", "The turbine is at steam capacity,
doc("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.")
doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.")
target = docs.annunc.unit.fac_section
doc("?", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
doc("?", "Radiation Monitor", "At least one facility radiation monitor is connected")
doc("?", "Induction Matrix", "The induction matrix is connected.")
doc("?", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
doc("?", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
doc("?", "Process Active", "Automatic process control is active.")
doc("?", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).")
doc("?", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
doc("?", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("?", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
doc("?", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
doc("?", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
doc("?", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
doc("?", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
target = docs.annunc.facility.main_section
sect("Connectivity")
doc("all_sys_ok", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
doc("rad_computed_status", "Radiation Monitor", "At least one facility radiation monitor is connected")
doc("im_computed_status", "Induction Matrix", "The induction matrix is connected.")
doc("sps_computed_status", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
sect("Automatic Control")
doc("auto_ready", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
doc("auto_active", "Process Active", "Automatic process control is active.")
doc("auto_ramping", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).")
doc("auto_saturated", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).")
sect("Automatic SCRAM")
doc("auto_scram", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("as_matrix_dc", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
doc("as_matrix_fill", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.")
doc("as_crit_alarm", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).")
doc("as_radiation", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.")
doc("as_gen_fault", "Gen. Control Fault", "Automatic SCRAM occurred due to assigned units being degraded/no longer ready during generation mode. The system will automatically resume (starting with initial ramp) once the problem is resolved.")
docs.fp = {
common = {}, r_plc = {}, rtu_gw = {}, supervisor = {}
}
--comp id "This must never be the identical between devices, and that can only happen if you duplicate a computer (such as middle-click on it and place it elsewhere in creative mode)."
target = docs.fp.common
sect("Core Status")
doc("fp_status", "STATUS", "This is always lit, except on the Reactor PLC (see Reactor PLC section).")
doc("fp_heartbeat", "HEARTBEAT", "This alternates between lit and unlit as the main loop on the device runs. If this freezes, something is wrong and the logs will indicate why.")
sect("Hardware & Network")
doc("fp_modem", "MODEM", "This lights up if the wireless/ender modem is connected. In parentheses is the unique computer ID of this device, which will show up in places such as the supervisor's connection lists.")
doc("fp_modem", "NETWORK", "This is present when in standard color modes and indicates the network status using multiple colors.")
list(DOC_LIST_TYPE.LED, { "not linked", "linked", "link denied", "bad comms version", "duplicate PLC" }, { colors.gray, colors.green, colors.red, colors.orange, colors.yellow })
text("You can fix \"bad comms version\" by ensuring all devices are up-to-date, as this indicates a communications protocol version mismatch. Note that yellow is Reactor PLC-specific, indicating duplicate unit IDs in use.")
doc("fp_nt_linked", "NT LINKED", "(color accessibility modes only)", "This indicates the device is linked to the supervisor.")
doc("fp_nt_version", "NT VERSION", "(color accessibility modes only)", "This indicates the communications versions of the supervisor and this device do not match. Make sure everything is up-to-date.")
sect("Versions")
doc("fp_fw", "FW", "Firmware application version of this device.")
doc("fp_nt", "NT", "Network (comms) version this device has. These must match between devices in order for them to connect.")
target = docs.fp.r_plc
sect("Core Status")
doc("fp_status", "STATUS", "This is green once the PLC is initialized and OK (has all its peripherals) and red if something is wrong, in which case you should refer to the other indicator lights (REACTOR & MODEM).")
sect("Hardware & Network")
doc("fp_rplc_reactor", "REACTOR", "This indicates the status of the connected reactor peripheral.")
list(DOC_LIST_TYPE.LED, { "disconnected", "unformed", "ok" }, { colors.red, colors.yellow, colors.green })
doc("fp_nt_collision", "NT COLLISION", "(color accessibility modes only)", "This indicates the Reactor PLC unit ID is a duplicate of another already connected Reactor PLC.")
sect("Co-Routine States")
doc("fp_rplc_rt_main", "RT MAIN", "This lights up as long as the device's main loop co-routine is running, which it should be as long as STATUS is green.")
doc("fp_rplc_rt_rps", "RT RPS", "This should always be lit up if a reactor is connected as it indicates the RPS co-routine is running, otherwise safety checks will not be running.")
doc("fp_rplc_rt_ctx", "RT COMMS TX", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the communications transmission co-routine is running.")
doc("fp_rplc_rt_crx", "RT COMMS RX", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the communications receiver/handler co-routine is running.")
doc("fp_rplc_rt_spctl", "RT SPCTL", "This should always be lit if the Reactor PLC is not running in standalone mode, as it indicates the process setpoint controller co-routine is running.")
sect("Status")
doc("fp_rct_active", "RCT ACTIVE", "The reactor is active (running).")
doc("fp_emer_cool", "EMER COOLANT", "This is only present if PLC-controlled emergency coolant is configured on that device. When lit, it indicates that it has been activated.")
doc("fp_rps_trip", "RPS TRIP", "Flashes when the RPS has SCRAM'd the reactor due to a safety trip.")
sect("RPS Conditions")
doc("fp_rps_man", "MANUAL", "The RPS was tripped manually (SCRAM by user, not via the Mekanism Reactor UI).")
doc("fp_rps_auto", "AUTOMATIC", "The RPS was tripped by the supervisor automatically.")
doc("fp_rps_to", "TIMEOUT", "The RPS tripped due to losing the supervisor connection.")
doc("fp_rps_pflt", "PLC FAULT", "The RPS tripped due to a peripheral error.")
doc("fp_rps_rflt", "RCT FAULT", "The RPS tripped due to the reactor not being formed.")
doc("fp_rps_temp", "HI DAMAGE", "The RPS tripped due to being >=" .. const.RPS_LIMITS.MAX_DAMAGE_PERCENT .. "% damaged.")
doc("fp_rps_temp", "HI TEMP", "The RPS tripped due to high reactor temperature (>=" .. const.RPS_LIMITS.MAX_DAMAGE_TEMPERATURE .. "K).")
doc("fp_rps_fuel", "LO FUEL", "The RPS tripped due to having no fuel.")
doc("fp_rps_waste", "HI WASTE", "The RPS tripped due to having high levels of waste (>" .. (const.RPS_LIMITS.MAX_WASTE_FILL * 100) .. "%).")
doc("fp_rps_ccool", "LO CCOOLANT", "The RPS tripped due to having low levels of cooled coolant (<" .. (const.RPS_LIMITS.MIN_COOLANT_FILL * 100) .. "%).")
doc("fp_rps_ccool", "HI HCOOLANT", "The RPS tripped due to having high levels of heated coolant (>" .. (const.RPS_LIMITS.MAX_HEATED_COLLANT_FILL * 100) .. "%).")
target = docs.fp.rtu_gw
sect("Co-Routine States")
doc("fp_rtu_rt_main", "RT MAIN", "This indicates if the device's main loop co-routine is running.")
doc("fp_rtu_rt_comms", "RT COMMS", "This indicates if the communications handler co-routine is running.")
sect("Device List")
doc("fp_rtu_rt", "RT", "In each RTU entry row, an RT light indicates if the co-routine for that RTU unit is running. This is never lit for redstone units.")
doc("fp_rtu_rt", "Device Status", "In each RTU entry row, the light to the left of the device name indicates its peripheral status.")
list(DOC_LIST_TYPE.LED, { "disconnected", "faulted", "unformed", "ok" }, { colors.red, colors.orange, colors.yellow, colors.green })
text("Note that disconnected devices lack detailed information and will not be modifiable in configuration until re-connected.")
doc("fp_rtu_rt", "Device Assignment", "In each RTU entry row, the device identification is to the right of the status light. This begins with the device type and its index followed by its assignment after the \x1a, which is a unit or the facility (FACIL). Unit 1's 3rd turbine would show up as 'TURBINE 3 \x1a UNIT 1'.")
target = docs.fp.supervisor
sect("Round Trip Times")
doc("fp_sv_fw", "RTT", "Each connection has a round trip time, or RTT. Since the supervisor updates at a rate of 150ms, RTTs from ~150ms to ~300ms are typical. Higher RTTs indicate lag, and if they end up in the thousands there will be performance problems.")
list(DOC_LIST_TYPE.BULLET, { "green: <=300ms", "yellow: <=500ms ", "red: >500ms" })
sect("SVR Tab")
text("This tab includes information about the supervisor, covered by 'Common Items'.")
sect("PLC Tab")
text("This tab lists the expected PLC connections based on the number of configured units. Status information about each connection is shown when linked.")
doc("fp_sv_link", "LINK", "This indicates if the reactor PLC is linked.")
doc("fp_sv_p_cmpid", "PLC Computer ID", "This shows the computer ID of the reactor PLC, or --- if disconnected.")
doc("fp_sv_p_fw", "PLC FW", "This shows the firmware version of the reactor PLC.")
sect("RTU Tab")
text("As RTU gateways connect to the supervisor, they will show up here along with some information.")
doc("fp_sv_r_cmpid", "RTU Computer ID", "At the start of the entry is an @ sign followed by the computer ID of the RTU gateway.")
doc("fp_sv_r_units", "UNITS", "This is a count of the number of RTUs configured on the RTU gateway (each line on the RTU gateway's front panel).")
doc("fp_sv_r_fw", "RTU FW", "This shows the firmware version of the RTU gateway.")
sect("PKT Tab")
text("As pocket computers connect to the supervisor, they will show up here along with some information. The properties listed are the same as with RTU gateways (except for UNITS), so they will not be further described here.")
sect("DEV Tab")
text("If nothing is connected, this will list all the expected RTU devices that aren't found. This page should be blank if everything is connected and configured correctly. If not, it will list certain types of detectable problems.")
doc("fp_sv_d_miss", "MISSING", "These items list missing devices, with the details that should be used in the RTU's configuration.")
doc("fp_sv_d_oor", "BAD INDEX", "If you have a configuration entry that has an index outside of the maximum number of devices configured on the supervisor, this will show up indicating what entry is incorrect. For example, if you specified a unit has 2 turbines and a #3 connected, it would show up here as out of range.")
doc("fp_sv_d_dupe", "DUPLICATE", "If a device tries to connect that is configured the same as another, it will be rejected and show up here. If you try to connect two #1 turbines for a unit, that would fail and one would appear here.")
sect("INF Tab")
text("This tab gives information about the other tabs, along with extra details on the DEV tab.")
docs.glossary = {
abbvs = {}, terms = {}
@ -113,6 +261,7 @@ doc("G_PPM", "PPM", "Protected Peripheral Manager. This is an abstraction layer
doc("G_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.")
doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.")
doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.")
doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.")
doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.")
doc("G_SVR", "SVR", "Supervisor. Abbreviation for the supervisory computer.")

View File

@ -7,6 +7,7 @@ local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local control_app = require("pocket.ui.apps.control")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local guide_app = require("pocket.ui.apps.guide")
@ -62,6 +63,7 @@ local function init(main)
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
control_app(page_div)
guide_app(page_div)
loader_app(page_div)
sys_apps(page_div)

View File

@ -1,6 +1,8 @@
local log = require("scada-common.log")
local util = require("scada-common.util")
local docs = require("pocket.ui.docs")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
@ -9,9 +11,15 @@ local TextBox = require("graphics.elements.textbox")
local PushButton = require("graphics.elements.controls.push_button")
local IndicatorLight = require("graphics.elements.indicators.light")
local LED = require("graphics.elements.indicators.led")
local ALIGN = core.ALIGN
local cpair = core.cpair
local DOC_TYPE = docs.DOC_ITEM_TYPE
local LIST_TYPE = docs.DOC_LIST_TYPE
-- new guide documentation section
---@param data _guide_section_constructor_data
---@param base_page nav_tree_page
@ -34,20 +42,57 @@ return function (data, base_page, title, items, scroll_height)
TextBox{parent=section_view_div,y=1,text=title,alignment=ALIGN.CENTER}
PushButton{parent=section_view_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to}
local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=30,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local name_list = ListBox{parent=section_div,x=1,y=3,scroll_height=60,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local def_list = ListBox{parent=section_view_div,x=1,y=3,scroll_height=scroll_height,nav_fg_bg=cpair(colors.lightGray,colors.gray),nav_active=cpair(colors.white,colors.gray)}
local _end
local sect_id = 1
local page_end
for i = 1, #items do
local item = items[i] ---@type pocket_doc_item
local item = items[i] ---@type pocket_doc_sect|pocket_doc_subsect|pocket_doc_text|pocket_doc_list
local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)}
TextBox{parent=def_list,text=item.desc}
_end = Div{parent=def_list,height=1,can_focus=true}
if item.type == DOC_TYPE.SECTION then
---@cast item pocket_doc_sect
local title_text = sect_id.."."
local title_offs = string.len(title_text) + 2
local sect_title = Div{parent=def_list,height=1}
TextBox{parent=sect_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
local anchor = TextBox{parent=sect_title,x=title_offs,y=1,text=item.name,anchor=true,fg_bg=cpair(colors.green,colors.black)}
page_end = Div{parent=def_list,height=1,can_focus=true}
local function view()
_end.focus()
page_end.focus()
view_page.nav_to()
anchor.focus()
end
if #name_list.get_children() > 0 then
local _ = Div{parent=name_list,height=1}
end
local name_title = Div{parent=name_list,height=1}
TextBox{parent=name_title,x=1,text=title_text,fg_bg=cpair(colors.lightGray,colors.black)}
PushButton{parent=name_title,x=title_offs,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.green,colors.black),active_fg_bg=btn_active,callback=view}
sect_id = sect_id + 1
elseif item.type == DOC_TYPE.SUBSECTION then
---@cast item pocket_doc_subsect
local anchor = TextBox{parent=def_list,text=item.name,anchor=true,fg_bg=cpair(colors.blue,colors.black)}
if item.subtitle then
TextBox{parent=def_list,text=item.subtitle,fg_bg=cpair(colors.gray,colors.black)}
end
TextBox{parent=def_list,text=item.body}
page_end = Div{parent=def_list,height=1,can_focus=true}
local function view()
page_end.focus()
view_page.nav_to()
anchor.focus()
end
@ -55,12 +100,46 @@ return function (data, base_page, title, items, scroll_height)
doc_map[item.key] = view
table.insert(search_db, { string.lower(item.name), item.name, title, view })
PushButton{parent=name_list,text=item.name,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
local name_entry = Div{parent=name_list,height=#util.strwrap(item.name,name_list.get_width()-3)}
TextBox{parent=name_entry,x=1,text="\x10",fg_bg=cpair(colors.gray,colors.black)}
PushButton{parent=name_entry,x=3,y=1,text=item.name,alignment=ALIGN.LEFT,fg_bg=cpair(colors.blue,colors.black),active_fg_bg=btn_active,callback=view}
elseif item.type == DOC_TYPE.TEXT then
---@cast item pocket_doc_text
TextBox{parent=def_list,text=item.text}
page_end = Div{parent=def_list,height=1,can_focus=true}
elseif item.type == DOC_TYPE.LIST then
---@cast item pocket_doc_list
local container = Div{parent=def_list,height=#item.items}
if item.list_type == LIST_TYPE.BULLET then
for _, li in ipairs(item.items) do
TextBox{parent=container,x=2,text="\x07 "..li}
end
elseif item.list_type == LIST_TYPE.NUMBERED then
local width = string.len("" .. #item.items)
for idx, li in ipairs(item.items) do
TextBox{parent=container,x=2,text=util.sprintf("%" .. width .. "d. %s", idx, li)}
end
elseif item.list_type == LIST_TYPE.INDICATOR then
for idx, li in ipairs(item.items) do
local _ = IndicatorLight{parent=container,x=2,label=li,colors=cpair(colors.black,item.colors[idx])}
end
elseif item.list_type == LIST_TYPE.LED then
for idx, li in ipairs(item.items) do
local _ = LED{parent=container,x=2,label=li,colors=cpair(colors.black,item.colors[idx])}
end
end
page_end = Div{parent=def_list,height=1,can_focus=true}
end
if i % 12 == 0 then util.nop() end
end
log.debug("guide section " .. title .. " generated with final height ".. _end.get_y())
log.debug("guide section " .. title .. " generated with final height ".. page_end.get_y())
util.nop()

View File

@ -47,7 +47,7 @@ local function new_view(root)
App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=7,text="\x08",title="Devices",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}

View File

@ -22,7 +22,6 @@ local IndLight = require("graphics.elements.indicators.light")
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local RIGHT = core.ALIGN.RIGHT
local self = {
@ -606,7 +605,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end

View File

@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
local R_PLC_VERSION = "v1.8.7"
local R_PLC_VERSION = "v1.8.8"
local println = util.println
local println_ts = util.println_ts

View File

@ -1457,7 +1457,7 @@ local function config_view(display)
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.10.7"
local RTU_VERSION = "v1.10.8"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE

View File

@ -209,6 +209,23 @@ types.PROCESS_NAMES = {
"GEN_RATE_FAULT_IDLE"
}
---@enum AUTO_GROUP
types.AUTO_GROUP = {
MANUAL = 0,
PRIMARY = 1,
SECONDARY = 2,
TERTIARY = 3,
BACKUP = 4
}
types.AUTO_GROUP_NAMES = {
"Manual",
"Primary",
"Secondary",
"Tertiary",
"Backup"
}
---@enum WASTE_MODE
types.WASTE_MODE = {
AUTO = 1,

View File

@ -30,7 +30,6 @@ local tri = util.trinary
local cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
@ -1174,7 +1173,7 @@ local function config_view(display)
local textbox
if height > 1 then
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1}
else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end

View File

@ -8,6 +8,7 @@ local fac_update = require("supervisor.facility_update")
local rsctl = require("supervisor.session.rsctl")
local svsessions = require("supervisor.session.svsessions")
local AUTO_GROUP = types.AUTO_GROUP
local PROCESS = types.PROCESS
local RTU_ID_FAIL = types.RTU_ID_FAIL
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
@ -73,8 +74,8 @@ function facility.new(config)
burn_target = 0.1, -- burn rate target for aggregate burn mode
charge_setpoint = 0, -- FE charge target setpoint
gen_rate_setpoint = 0, -- FE/t charge rate target setpoint
group_map = {}, -- units -> group IDs
prio_defs = { {}, {}, {}, {} }, -- priority definitions (each level is a table of units)
group_map = {}, ---@type AUTO_GROUP[] units -> group IDs
prio_defs = { {}, {}, {}, {} }, ---@type reactor_unit[][] priority definitions (each level is a table of units)
at_max_burn = false,
ascram = false,
ascram_reason = AUTO_SCRAM.NONE,
@ -130,7 +131,7 @@ function facility.new(config)
for i = 1, config.UnitCount do
table.insert(self.units,
unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling))
table.insert(self.group_map, 0)
table.insert(self.group_map, AUTO_GROUP.MANUAL)
end
-- list for RTU session management
@ -375,11 +376,14 @@ function facility.new(config)
end
end
-- check automatic control mode
function public.auto_is_active() return self.mode ~= PROCESS.INACTIVE end
-- stop auto control
function public.auto_stop() self.mode = PROCESS.INACTIVE end
-- set automatic control configuration and start the process
---@param auto_cfg coord_auto_config configuration
---@param auto_cfg sys_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(auto_cfg)
local charge_scaler = 1000000 -- convert MFE to FE
@ -451,24 +455,29 @@ function facility.new(config)
-- set the automatic control group of a unit
---@param unit_id integer unit ID
---@param group integer group ID or 0 for independent
---@param group AUTO_GROUP group ID or 0 for independent
function public.set_group(unit_id, group)
if (group >= 0 and group <= 4) and (unit_id > 0 and unit_id <= config.UnitCount) and self.mode == PROCESS.INACTIVE then
if (group >= AUTO_GROUP.MANUAL and group <= AUTO_GROUP.BACKUP) and (unit_id > 0 and unit_id <= config.UnitCount) and self.mode == PROCESS.INACTIVE then
-- remove from old group if previously assigned
local old_group = self.group_map[unit_id]
if old_group ~= 0 then
if old_group ~= AUTO_GROUP.MANUAL then
util.filter_table(self.prio_defs[old_group], function (u) return u.get_id() ~= unit_id end)
end
self.group_map[unit_id] = group
-- add to group if not independent
if group > 0 then
if group > AUTO_GROUP.MANUAL then
table.insert(self.prio_defs[group], self.units[unit_id])
end
end
end
-- get the automatic control group of a unit
---@param unit_id integer unit ID
---@nodiscard
function public.get_group(unit_id) return self.group_map[unit_id] end
-- set waste production
---@param product WASTE_PRODUCT target product
---@return WASTE_PRODUCT product newly set value, if valid

View File

@ -1,6 +1,7 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
local databus = require("supervisor.databus")
@ -15,6 +16,9 @@ local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_COMMAND
local AUTO_GROUP = types.AUTO_GROUP
local WASTE_MODE = types.WASTE_MODE
local SV_Q_DATA = svqtypes.SV_Q_DATA
-- retry time constants in ms
@ -241,11 +245,16 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
facility.scram_all()
_send(CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMAND.STOP then
local was_active = facility.auto_is_active()
if was_active then
facility.auto_stop()
_send(CRDN_TYPE.FAC_CMD, { cmd, true })
end
_send(CRDN_TYPE.FAC_CMD, { cmd, was_active })
elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then
---@type coord_auto_config
---@type sys_auto_config
local config = {
mode = pkt.data[2],
burn_target = pkt.data[3],
@ -300,21 +309,30 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
-- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #self.units then
local unit = self.units[uid] ---@type reactor_unit
local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL
if cmd == UNIT_COMMAND.START then
out_queue.push_data(SV_Q_DATA.START, data)
elseif cmd == UNIT_COMMAND.SCRAM then
if cmd == UNIT_COMMAND.SCRAM then
out_queue.push_data(SV_Q_DATA.SCRAM, data)
elseif cmd == UNIT_COMMAND.START then
if manual then
out_queue.push_data(SV_Q_DATA.START, data)
else
-- denied
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, false })
end
elseif cmd == UNIT_COMMAND.RESET_RPS then
out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
elseif cmd == UNIT_COMMAND.SET_BURN then
if pkt.length == 3 then
if manual then
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
end
else
log.debug(log_tag .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMAND.SET_WASTE then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= WASTE_MODE.AUTO) and (pkt.data[3] <= WASTE_MODE.MANUAL_ANTI_MATTER) then
unit.set_waste_mode(pkt.data[3])
else
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option")
@ -335,9 +353,9 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end
elseif cmd == UNIT_COMMAND.SET_GROUP then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
facility.set_group(unit.get_id(), pkt.data[3])
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_tag .. "CRDN unit command set group missing group id")
end

View File

@ -395,13 +395,6 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
elseif ack == false then
log.debug(log_tag .. "burn rate update failed!")
end
-- send acknowledgement to coordinator
out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
unit = reactor_id,
cmd = UNIT_COMMAND.SET_BURN,
ack = ack
})
elseif pkt.type == RPLC_TYPE.RPS_ENABLE then
-- enable acknowledgement
local ack = _get_ack(pkt)

View File

@ -22,7 +22,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.5.2"
local SUPERVISOR_VERSION = "v1.5.3"
local println = util.println
local println_ts = util.println_ts