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

View File

@ -387,7 +387,7 @@ function coordinator.comms(version, nic, sv_watchdog)
end end
-- send the auto process control configuration with a start command -- 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) function public.send_auto_start(auto_cfg)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { _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 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 local ack = packet.data[2] == true
if cmd == FAC_COMMAND.SCRAM_ALL then 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 elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack) iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMAND.START then 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") log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end end
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then 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 elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
process.waste_ack_handle(packet.data[2]) process.waste_ack_handle(packet.data[2])
elseif cmd == FAC_COMMAND.SET_PU_FB then elseif cmd == FAC_COMMAND.SET_PU_FB then
process.pu_fb_ack_handle(packet.data[2]) 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 else
log.debug(util.c("received facility command ack with unknown command ", cmd)) log.debug(util.c("received facility command ack with unknown command ", cmd))
end end
@ -625,21 +627,15 @@ function coordinator.comms(version, nic, sv_watchdog)
if unit ~= nil then if unit ~= nil then
if cmd == UNIT_COMMAND.SCRAM then if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack) process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.START then elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack) process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.RESET_RPS then elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack) process.unit_ack(unit_id, cmd, 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)
elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack) process.unit_ack(unit_id, cmd, ack)
elseif cmd == UNIT_COMMAND.SET_GROUP then
-- UI will be updated to display current group if changed successfully
else 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 end
else else
log.debug(util.c("received unit command ack with unknown unit ", unit_id)) 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, save_cfg_ack = __generic_ack,
start_ack = __generic_ack, start_ack = __generic_ack,
stop_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 }, 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, turbine_flow_stable = false,
-- auto control group -- auto control group
a_group = 0, a_group = types.AUTO_GROUP.MANUAL,
start = function () process.start(i) end, start = function () io.process.start(i) end,
scram = function () process.scram(i) end, scram = function () io.process.scram(i) end,
reset_rps = function () process.reset_rps(i) end, reset_rps = function () io.process.reset_rps(i) end,
ack_alarms = function () process.ack_all_alarms(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_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_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 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 = { alarm_callbacks = {
c_breach = { ack = function () ack(1) end, reset = function () reset(1) end }, c_breach = { ack = function () ack(1) end, reset = function () reset(1) end },
radiation = { ack = function () ack(2) end, reset = function () reset(2) 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 -- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms) process.init(io, comms)
-- coordinator's process handle
io.process = process.create_handle()
end end
--#region Front Panel PSIL --#region Front Panel PSIL
@ -575,11 +569,10 @@ function iocontrol.update_facility_status(status)
local group_map = ctl_status[14] local group_map = ctl_status[14]
if (type(group_map) == "table") and (#group_map == fac.num_units) then 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 for i = 1, #group_map do
io.units[i].a_group = group_map[i] 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_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
end end

View File

@ -7,21 +7,23 @@ local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local FAC_COMMAND = comms.FAC_COMMAND local F_CMD = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND local U_CMD = comms.UNIT_COMMAND
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
local PRODUCT = types.WASTE_PRODUCT local PRODUCT = types.WASTE_PRODUCT
local REQUEST_TIMEOUT_MS = 10000
---@class process_controller ---@class process_controller
local process = {} local process = {}
local self = { local pctl = {
io = nil, ---@type ioctl io = nil, ---@type ioctl
comms = nil, ---@type coord_comms comms = nil, ---@type coord_comms
---@class coord_control_states ---@class sys_control_states
control_states = { control_states = {
---@class coord_auto_config ---@class sys_auto_config
process = { process = {
mode = PROCESS.INACTIVE, mode = PROCESS.INACTIVE,
burn_target = 0.0, burn_target = 0.0,
@ -34,28 +36,52 @@ local self = {
}, },
waste_modes = {}, waste_modes = {},
priority_groups = {} priority_groups = {}
},
commands = {
unit = {}, ---@type process_command_state[][]
fac = {} ---@type process_command_state[]
} }
} }
-------------------------- ---@class process_command_state
-- UNIT COMMAND CONTROL -- ---@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 -- initialize the process controller
---@param iocontrol ioctl iocontrl system ---@param iocontrol ioctl iocontrl system
---@param coord_comms coord_comms coordinator communications ---@param coord_comms coord_comms coordinator communications
function process.init(iocontrol, coord_comms) function process.init(iocontrol, coord_comms)
self.io = iocontrol pctl.io = iocontrol
self.comms = coord_comms 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 ctl_proc.limits[i] = 0.1
end end
local ctrl_states = settings.get("ControlStates", {}) 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 -- facility auto control configuration
if type(config) == "table" then if type(config) == "table" then
@ -68,33 +94,33 @@ function process.init(iocontrol, coord_comms)
ctl_proc.pu_fallback = config.pu_fallback ctl_proc.pu_fallback = config.pu_fallback
ctl_proc.sps_low_power = config.sps_low_power ctl_proc.sps_low_power = config.sps_low_power
self.io.facility.ps.publish("process_mode", ctl_proc.mode) pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) pctl.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)) pctl.io.facility.ps.publish("process_charge_target", pctl.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_gen_target", pctl.io.energy_convert_from_fe(ctl_proc.gen_target))
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product) pctl.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback) pctl.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_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do for id = 1, math.min(#ctl_proc.limits, pctl.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit local unit = pctl.io.units[id] ---@type ioctl_unit
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id]) unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
end end
log.info("PROCESS: loaded auto control settings") log.info("PROCESS: loaded auto control settings")
-- notify supervisor of auto waste config -- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product) pctl.comms.send_fac_command(F_CMD.SET_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback) pctl.comms.send_fac_command(F_CMD.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_SPS_LP, ctl_proc.sps_low_power)
end end
-- unit waste states -- unit waste states
local waste_modes = ctrl_states.waste_modes ---@type table|nil local waste_modes = ctrl_states.waste_modes ---@type table|nil
if type(waste_modes) == "table" then if type(waste_modes) == "table" then
for id, mode in pairs(waste_modes) do for id, mode in pairs(waste_modes) do
self.control_states.waste_modes[id] = mode pctl.control_states.waste_modes[id] = mode
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode) pctl.comms.send_unit_command(U_CMD.SET_WASTE, id, mode)
end end
log.info("PROCESS: loaded unit waste mode settings") 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 local prio_groups = ctrl_states.priority_groups ---@type table|nil
if type(prio_groups) == "table" then if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do for id, group in pairs(prio_groups) do
self.control_states.priority_groups[id] = group pctl.control_states.priority_groups[id] = group
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group) pctl.comms.send_unit_command(U_CMD.SET_GROUP, id, group)
end end
log.info("PROCESS: loaded priority groups settings") log.info("PROCESS: loaded priority groups settings")
end end
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 -- facility SCRAM command
function process.fac_scram() function handle.fac_scram()
self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL) 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") log.debug("PROCESS: FAC SCRAM ALL")
end end
end
-- facility alarm acknowledge command -- facility alarm acknowledge command
function process.fac_ack_alarms() function handle.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_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") log.debug("PROCESS: FAC ACK ALL ALARMS")
end 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 ---@param id integer unit ID
function process.start(id) function handle.start(id)
self.io.units[id].control_state = true if u_request(id, U_CMD.START, handle.unit_ack[id].on_start) then
self.comms.send_unit_command(UNIT_COMMAND.START, id) pctl.io.units[id].control_state = true
pctl.comms.send_unit_command(U_CMD.START, id)
log.debug(util.c("PROCESS: UNIT[", id, "] START")) log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end end
end
-- SCRAM reactor -- SCRAM reactor
---@param id integer unit ID ---@param id integer unit ID
function process.scram(id) function handle.scram(id)
self.io.units[id].control_state = false if u_request(id, U_CMD.SCRAM, handle.unit_ack[id].on_scram) then
self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id) pctl.io.units[id].control_state = false
pctl.comms.send_unit_command(U_CMD.SCRAM, id)
log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM")) log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end end
end
-- reset reactor protection system -- reset reactor protection system
---@param id integer unit ID ---@param id integer unit ID
function process.reset_rps(id) function handle.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMAND.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")) log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end 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 -- set burn rate
---@param id integer unit ID ---@param id integer unit ID
---@param rate number burn rate ---@param rate number burn rate
function process.set_rate(id, 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)) log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end end
@ -160,31 +340,24 @@ end
---@param mode integer waste mode ---@param mode integer waste mode
function process.set_unit_waste(id, mode) function process.set_unit_waste(id, mode)
-- publish so that if it fails then it gets reset -- 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)) log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
self.control_states.waste_modes[id] = mode pctl.control_states.waste_modes[id] = mode
settings.set("ControlStates", self.control_states) settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then if not settings.save("/coordinator.settings") then
log.error("process.set_unit_waste(): failed to save coordinator settings file") log.error("process.set_unit_waste(): failed to save coordinator settings file")
end end
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 -- acknowledge an alarm
---@param id integer unit ID ---@param id integer unit ID
---@param alarm integer alarm ID ---@param alarm integer alarm ID
function process.ack_alarm(id, alarm) 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)) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end end
@ -192,7 +365,7 @@ end
---@param id integer unit ID ---@param id integer unit ID
---@param alarm integer alarm ID ---@param alarm integer alarm ID
function process.reset_alarm(id, alarm) 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)) log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end end
@ -200,78 +373,68 @@ end
---@param unit_id integer unit ID ---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent ---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id) 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)) log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
self.control_states.priority_groups[unit_id] = group_id pctl.control_states.priority_groups[unit_id] = group_id
settings.set("ControlStates", self.control_states) settings.set("ControlStates", pctl.control_states)
if not settings.save("/coordinator.settings") then if not settings.save("/coordinator.settings") then
log.error("process.set_group(): failed to save coordinator settings file") log.error("process.set_group(): failed to save coordinator settings file")
end end
end end
--#endregion
-------------------------- --------------------------
-- AUTO PROCESS CONTROL -- -- AUTO PROCESS CONTROL --
-------------------------- --------------------------
-- write auto process control to config file -- start automatic process control
local function _write_auto_config() function process.start_auto()
-- save config pctl.comms.send_auto_start(pctl.control_states.process)
settings.set("ControlStates", self.control_states) log.debug("PROCESS: START AUTO CTL")
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 end
-- stop automatic process control -- stop automatic process control
function process.stop_auto() 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") log.debug("PROCESS: STOP AUTO CTL")
end 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 -- set automatic process control waste mode
---@param product WASTE_PRODUCT waste product for auto control ---@param product WASTE_PRODUCT waste product for auto control
function process.set_process_waste(product) 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)) log.debug(util.c("PROCESS: SET WASTE ", product))
-- update config table and save -- update config table and save
self.control_states.process.waste_product = product pctl.control_states.process.waste_product = product
_write_auto_config() _write_auto_config()
end end
-- set automatic process control plutonium fallback -- set automatic process control plutonium fallback
---@param enabled boolean whether to enable plutonium fallback ---@param enabled boolean whether to enable plutonium fallback
function process.set_pu_fallback(enabled) 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)) log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
-- update config table and save -- update config table and save
self.control_states.process.pu_fallback = enabled pctl.control_states.process.pu_fallback = enabled
_write_auto_config() _write_auto_config()
end end
-- set automatic process control SPS usage at low power -- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power ---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled) 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)) log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save -- update config table and save
self.control_states.process.sps_low_power = enabled pctl.control_states.process.sps_low_power = enabled
_write_auto_config() _write_auto_config()
end end
@ -285,7 +448,7 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.debug("PROCESS: SAVE") log.debug("PROCESS: SAVE")
-- update config table -- update config table
local ctl_proc = self.control_states.process local ctl_proc = pctl.control_states.process
ctl_proc.mode = mode ctl_proc.mode = mode
ctl_proc.burn_target = burn_target ctl_proc.burn_target = burn_target
ctl_proc.charge_target = charge_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 ctl_proc.limits = limits
-- save config -- save config
self.io.facility.save_cfg_ack(_write_auto_config()) pctl.io.facility.save_cfg_ack(_write_auto_config())
end end
-- handle a start command acknowledgement -- handle a start command acknowledgement
@ -301,39 +464,46 @@ end
function process.start_ack_handle(response) function process.start_ack_handle(response)
local ack = response[1] 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.mode = response[2]
ctl_proc.burn_target = response[3] ctl_proc.burn_target = response[3]
ctl_proc.charge_target = response[4] ctl_proc.charge_target = response[4]
ctl_proc.gen_target = response[5] 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] 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]) unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
end end
self.io.facility.ps.publish("process_mode", ctl_proc.mode) pctl.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target) pctl.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)) pctl.io.facility.ps.publish("process_charge_target", pctl.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_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 end
-- record waste product state after attempting to change it -- record waste product settting after attempting to change it
---@param response WASTE_PRODUCT supervisor waste product state ---@param response WASTE_PRODUCT supervisor waste product settting
function process.waste_ack_handle(response) function process.waste_ack_handle(response)
self.control_states.process.waste_product = response pctl.control_states.process.waste_product = response
self.io.facility.ps.publish("process_waste_product", response) pctl.io.facility.ps.publish("process_waste_product", response)
end end
-- record plutonium fallback state after attempting to change it -- record plutonium fallback settting after attempting to change it
---@param response boolean supervisor plutonium fallback state ---@param response boolean supervisor plutonium fallback settting
function process.pu_fb_ack_handle(response) function process.pu_fb_ack_handle(response)
self.control_states.process.pu_fallback = response pctl.control_states.process.pu_fallback = response
self.io.facility.ps.publish("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 end
return process return process

View File

@ -4,12 +4,15 @@ local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util") local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local pocket = {} local pocket = {}
local PROTOCOL = comms.PROTOCOL local PROTOCOL = comms.PROTOCOL
local CRDN_TYPE = comms.CRDN_TYPE local CRDN_TYPE = comms.CRDN_TYPE
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMAND = comms.UNIT_COMMAND
-- retry time constants in ms -- retry time constants in ms
-- local INITIAL_WAIT = 1500 -- local INITIAL_WAIT = 1500
@ -37,7 +40,7 @@ local PERIODICS = {
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@param timeout number communications timeout
function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, 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 = { local self = {
-- connection properties -- connection properties
@ -46,6 +49,8 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
connected = true, connected = true,
conn_watchdog = util.new_watchdog(timeout), conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0, last_rtt = 0,
-- process accessor handle
proc_handle = process.create_handle(),
-- periodic messages -- periodic messages
periodics = { periodics = {
last_update = 0, 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 self.seq_num = self.seq_num + 1
end 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 -- handle a packet
---@param pkt mgmt_frame|crdn_frame ---@param pkt mgmt_frame|crdn_frame
local function _handle_packet(pkt) local function _handle_packet(pkt)
-- check sequence number -- check sequence number
if self.r_seq_num ~= pkt.scada_frame.seq_num() then 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 return
else else
self.r_seq_num = pkt.scada_frame.seq_num() + 1 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() local db = iocontrol.get_db()
-- handle packet by type -- 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 fac = db.facility
local data = { 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.unit_id,
u.connected, u.connected,
u.rtu_hw, u.rtu_hw,
u.a_group,
u.alarms, u.alarms,
u.annunciator, u.annunciator,
u.reactor_data, u.reactor_data,
@ -160,7 +239,7 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
end end
end end
else 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 end
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
---@cast pkt mgmt_frame ---@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 self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then 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 end
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms") -- 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) iocontrol.fp_pkt_rtt(id, self.last_rtt)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_tag .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == MGMT_TYPE.CLOSE then elseif pkt.type == MGMT_TYPE.CLOSE then
-- close the session -- 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 elseif pkt.type == MGMT_TYPE.ESTABLISH then
-- something is wrong, kill the session -- something is wrong, kill the session
_close() _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 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 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() function public.close()
_close() _close()
_send_mgmt(MGMT_TYPE.CLOSE, {}) _send_mgmt(MGMT_TYPE.CLOSE, {})
log.info(log_header .. "session closed by server") log.info(log_tag .. "session closed by server")
end end
-- iterate the session -- 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 -- max 100ms spent processing queue
if util.time() - handle_start > 100 then 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 break
end end
end end
-- exit if connection was closed -- exit if connection was closed
if not self.connected then 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 return self.connected
end end

View File

@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads") local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.5.7" local COORDINATOR_VERSION = "v1.5.8"
local CHUNK_LOAD_DELAY_S = 30.0 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 coordinator = require("coordinator.coordinator")
local iocontrol = require("coordinator.iocontrol") local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
@ -147,6 +148,9 @@ function threads.thread__main(smem)
apisessions.iterate_all() apisessions.iterate_all()
apisessions.free_all_closed() apisessions.free_all_closed()
-- clear timed out process commands
process.clear_timed_out()
if renderer.ui_ready() then if renderer.ui_ready() then
-- update clock used on main and flow monitors -- update clock used on main and flow monitors
iocontrol.get_db().facility.ps.publish("date_time", os.date(smem.date_format)) 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 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 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=process.fac_ack_alarms,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 db.process.fac_ack.on_scram = scram.on_response
facility.ack_alarms_ack = ack_a.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 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} 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 RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
local AUTO_GROUP = types.AUTO_GROUP
local ALIGN = core.ALIGN local ALIGN = core.ALIGN
local cpair = core.cpair 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 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} 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 db.process.unit_ack[id].on_start = start.on_response
unit.scram_ack = scram.on_response db.process.unit_ack[id].on_scram = scram.on_response
unit.reset_rps_ack = reset.on_response db.process.unit_ack[id].on_rps_reset = reset.on_response
unit.ack_alarms_ack = ack_a.on_response db.process.unit_ack[id].on_ack_alarms = ack_a.on_response
local function start_button_en_check() local function start_button_en_check()
if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then if (unit.reactor_data ~= nil) and (unit.reactor_data.mek_status ~= nil) then
local can_start = (not unit.reactor_data.mek_status.status) and local can_start = (not unit.reactor_data.mek_status.status) and
(not unit.reactor_data.rps_tripped) 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 if can_start then start.enable() else start.disable() end
end 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_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 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=types.AUTO_GROUP_NAMES,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=ctl_opts,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) 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) -- enable/disable controls based on group assignment (start button is separate)
burn_rate.register(u_ps, "auto_group_id", function (gid) 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) end)
set_burn_btn.register(u_ps, "auto_group_id", function (gid) 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) end)
-- can't change group if auto is engaged regardless of if this unit is part of auto control -- 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 = {} local core = {}
core.version = "2.3.3" core.version = "2.3.4"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events
@ -123,15 +123,17 @@ end
-- Interactive Field Manager -- Interactive Field Manager
---@param e graphics_base ---@param e graphics_base element
---@param max_len any ---@param max_len any max value length
---@param fg_bg any ---@param fg_bg any enabled fg/bg
---@param dis_fg_bg any ---@param dis_fg_bg any disabled fg/bg
function core.new_ifield(e, max_len, fg_bg, dis_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 = { local self = {
frame_start = 1, frame_start = 1,
visible_text = e.value, visible_text = e.value,
cursor_pos = string.len(e.value) + 1, cursor_pos = string.len(e.value) + 1,
align_offset = 0,
selected_all = false 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_write(string.rep(" ", e.frame.w))
e.w_set_cur(1, 1) 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 if self.censor then
e.w_write(string.rep(self.censor, string.len(self.visible_text))) e.w_write(string.rep(self.censor, string.len(self.visible_text)))
else else
@ -226,15 +233,27 @@ function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
self.selected_all = false self.selected_all = false
-- write text without cursor -- write text without cursor
_write() _write(align_right)
end end
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 ---@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) function public.move_cursor(x)
self.selected_all = false 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) self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
end
public.show() public.show()
end end

View File

@ -574,6 +574,15 @@ function element.new(args, constraint, child_offset_x, child_offset_y)
---@return graphics_element ---@return graphics_element
function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end 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 -- remove a child element
---@param id element_id ---@param id element_id
function public.remove(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 accent color accent color for hazard border
---@field dis_colors? cpair text color and border color when disabled ---@field dis_colors? cpair text color and border color when disabled
---@field callback function function to call on touch ---@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 parent graphics_element
---@field id? string element id ---@field id? string element id
---@field x? integer 1 if omitted ---@field x? integer 1 if omitted
@ -28,6 +29,8 @@ local function hazard_button(args)
args.height = 3 args.height = 3
args.width = string.len(args.text) + 4 args.width = string.len(args.text) + 4
local timeout = args.timeout or 1.5
-- create new graphics element base object -- create new graphics element base object
local e = element.new(args) local e = element.new(args)
@ -149,8 +152,8 @@ local function hazard_button(args)
tcd.abort(on_success) tcd.abort(on_success)
tcd.abort(on_failure) tcd.abort(on_failure)
-- 1.5 second timeout -- operation timeout animation
tcd.dispatch(1.5, on_timeout) tcd.dispatch(timeout, on_timeout)
args.callback() args.callback()
end 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 max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
---@field allow_decimal? boolean true to allow decimals ---@field allow_decimal? boolean true to allow decimals
---@field allow_negative? boolean true to allow negative numbers ---@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 dis_fg_bg? cpair foreground/background colors when disabled
---@field parent graphics_element ---@field parent graphics_element
---@field id? string element id ---@field id? string element id
@ -47,7 +48,7 @@ local function number_field(args)
e.value = "" .. (args.default or 0) e.value = "" .. (args.default or 0)
-- make an interactive field manager -- 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 -- handle mouse interaction
---@param event mouse_interaction mouse event ---@param event mouse_interaction mouse event
@ -55,10 +56,16 @@ local function number_field(args)
-- only handle if on an increment or decrement arrow -- 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 e.enabled and e.in_frame_bounds(event.current.x, event.current.y) then
if core.events.was_clicked(event.type) 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() e.take_focus()
if event.type == MOUSE_CLICK.UP then if event.type == MOUSE_CLICK.UP then
ifield.move_cursor(event.current.x) ifield.move_cursor(x)
end end
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
ifield.select_all() ifield.select_all()

View File

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

View File

@ -3,11 +3,12 @@
-- --
local const = require("scada-common.constants") local const = require("scada-common.constants")
-- local log = require("scada-common.log")
local psil = require("scada-common.psil") local psil = require("scada-common.psil")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local process = require("pocket.process")
local ALARM = types.ALARM local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
@ -38,13 +39,24 @@ local io = {
ps = psil.create() 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 config = nil ---@type pkt_config
local comms = nil ---@type pocket_comms
-- initialize facility-independent components of pocket iocontrol -- initialize facility-independent components of pocket iocontrol
---@param comms pocket_comms ---@param pkt_comms pocket_comms
---@param nav pocket_nav ---@param nav pocket_nav
---@param cfg pkt_config ---@param cfg pkt_config
function iocontrol.init_core(comms, nav, cfg) function iocontrol.init_core(pkt_comms, nav, cfg)
comms = pkt_comms
config = cfg config = cfg
io.nav = nav io.nav = nav
@ -154,6 +166,11 @@ function iocontrol.init_fac(conf)
radiation = types.new_zero_radiation_reading(), 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(), ps = psil.create(),
induction_ps_tbl = {}, induction_ps_tbl = {},
@ -298,7 +315,18 @@ function iocontrol.init_fac(conf)
turbine_flow_stable = false, turbine_flow_stable = false,
-- auto control group -- 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 ---@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 }, 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) table.insert(io.units, entry)
end end
-- pass IO control here since it can't be require'd due to a require loop
process.init(io, comms)
end end
-- set network link state -- set network link state
@ -458,11 +489,15 @@ function iocontrol.record_unit_data(data)
unit.connected = data[2] unit.connected = data[2]
unit.rtu_hw = data[3] 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 --#region Annunciator
unit.annunciator = data[5] unit.annunciator = data[6]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
@ -540,7 +575,7 @@ function iocontrol.record_unit_data(data)
--#region Reactor Data --#region Reactor Data
unit.reactor_data = data[6] unit.reactor_data = data[7]
local control_status = 1 local control_status = 1
local reactor_status = 1 local reactor_status = 1
@ -612,7 +647,7 @@ function iocontrol.record_unit_data(data)
--#region RTU Devices --#region RTU Devices
unit.boiler_data_tbl = data[7] unit.boiler_data_tbl = data[8]
for id = 1, #unit.boiler_data_tbl do for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db 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) ps.publish("BoilerStateStatus", computed_status)
end end
unit.turbine_data_tbl = data[8] unit.turbine_data_tbl = data[9]
for id = 1, #unit.turbine_data_tbl do for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db 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) ps.publish("TurbineStateStatus", computed_status)
end end
unit.tank_data_tbl = data[9] unit.tank_data_tbl = data[10]
unit.last_rate_change_ms = data[10] unit.last_rate_change_ms = data[11]
unit.turbine_flow_stable = data[11] unit.turbine_flow_stable = data[12]
--#endregion --#endregion
--#region Status Information Display --#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 red(text) return { text = text, color = colors.red } end
local function white(text) return { text = text, color = colors.white } 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 ESTABLISH_ACK = comms.ESTABLISH_ACK
local MGMT_TYPE = comms.MGMT_TYPE local MGMT_TYPE = comms.MGMT_TYPE
local CRDN_TYPE = comms.CRDN_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 local LINK_STATE = iocontrol.LINK_STATE
@ -84,13 +86,14 @@ local APP_ID = {
LOADER = 2, LOADER = 2,
-- main app pages -- main app pages
UNITS = 3, UNITS = 3,
GUIDE = 4, CONTROL = 4,
ABOUT = 5, GUIDE = 5,
ABOUT = 6,
-- diag app page -- diag app page
ALARMS = 6, ALARMS = 7,
-- other -- other
DUMMY = 7, DUMMY = 8,
NUM_APPS = 7 NUM_APPS = 8
} }
pocket.APP_ID = APP_ID 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 if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
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 -- parse a packet
---@param side string ---@param side string
---@param sender integer ---@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)) local ok = util.trinary(max == nil, packet.length == length, packet.length >= length and packet.length <= (max or 0))
if not ok then if not ok then
local fmt = "[comms] RX_PACKET{r_chan=%d,proto=%d,type=%d}: packet length mismatch -> expect %d != actual %d" 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 end
return ok return ok
end end
@ -628,12 +646,56 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if protocol == PROTOCOL.SCADA_CRDN then if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame ---@cast packet crdn_frame
if self.api.linked then 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 if _check_length(packet, 11) then
iocontrol.record_facility_data(packet.data) iocontrol.record_facility_data(packet.data)
end end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then 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) iocontrol.record_unit_data(packet.data)
end end
else _fail_type(packet) 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 renderer = require("pocket.renderer")
local threads = require("pocket.threads") 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 = util.println
local println_ts = util.println_ts 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 util = require("scada-common.util")
local log = require("scada-common.log")
local iocontrol = require("pocket.iocontrol") local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
@ -78,6 +79,7 @@ local function new_view(root)
local uis_page = app.new_page(main_page, 4) local uis_page = app.new_page(main_page, 4)
local fps_page = app.new_page(main_page, 5) local fps_page = app.new_page(main_page, 5)
local gls_page = app.new_page(main_page, 6) 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 home = Div{parent=page_div,x=2}
local search = 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 uis = Div{parent=page_div,x=2,width=p_width}
local fps = 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 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 doc_map = {}
local search_db = {} 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="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="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,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} TextBox{parent=search,y=1,text="Search",alignment=ALIGN.CENTER}
@ -113,34 +117,41 @@ local function new_view(root)
function func_ref.run_search() function func_ref.run_search()
local query = string.lower(query_field.get_value()) local query = string.lower(query_field.get_value())
local s_results = { {}, {}, {} } local s_results = { {}, {}, {}, {} }
search_results.remove_all() search_results.remove_all()
if string.len(query) < 3 then if string.len(query) < 2 then
TextBox{parent=search_results,text="Search requires at least 3 characters."} TextBox{parent=search_results,text="Search requires at least 2 characters."}
return return
end end
local start = util.time_ms()
for _, entry in ipairs(search_db) do 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 if s_start == nil then
elseif s_start == 1 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) 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 elseif string.sub(query, s_start - 1, s_start) == " " then
-- start of word, good match -- start of word, good match
table.insert(s_results[2], entry) table.insert(s_results[3], entry)
else else
-- basic match in content -- basic match in content
table.insert(s_results[3], entry) table.insert(s_results[4], entry)
end end
end end
local empty = true local empty = true
for tier = 1, 3 do for tier = 1, 4 do
for idx = 1, #s_results[tier] do for idx = 1, #s_results[tier] do
local entry = s_results[tier][idx] local entry = s_results[tier][idx]
TextBox{parent=search_results,text=entry[3].." >",fg_bg=cpair(colors.gray,colors.black)} 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
end end
log.debug("App.Guide: search for \"" .. query .. "\" completed in " .. (util.time_ms() - start) .. "ms")
if empty then if empty then
TextBox{parent=search_results,text="No results found."} TextBox{parent=search_results,text="No results found."}
end 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_gen_page = guide_section(sect_construct_data, annunc_page, "Unit General", docs.annunc.unit.main_section, 170)
local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100) local unit_rps_page = guide_section(sect_construct_data, annunc_page, "Unit RPS", docs.annunc.unit.rps_section, 100)
local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170) local unit_rcs_page = guide_section(sect_construct_data, annunc_page, "Unit RCS", docs.annunc.unit.rcs_section, 170)
local fac_annunc_page = guide_section(sect_construct_data, annunc_page, "Facility", docs.annunc.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,y=3,text="Unit General >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_gen_page.nav_to}
PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to} PushButton{parent=annunc_div,text="Unit RPS >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=unit_rps_page.nav_to}
@ -199,21 +213,39 @@ local function new_view(root)
TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER} TextBox{parent=fps,y=1,text="Front Panels",alignment=ALIGN.CENTER}
PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=fps,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
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() local fp_common_page = guide_section(sect_construct_data, fps_page, "Common Items", docs.fp.common, 100)
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() local fp_rplc_page = guide_section(sect_construct_data, fps_page, "Reactor PLC", docs.fp.r_plc, 180)
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() local fp_rtu_page = guide_section(sect_construct_data, fps_page, "RTU Gateway", docs.fp.rtu_gw, 100)
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_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() 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} TextBox{parent=gls,y=1,text="Glossary",alignment=ALIGN.CENTER}
PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to} PushButton{parent=gls,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=main_page.nav_to}
local gls_abbv_page = guide_section(sect_construct_data, gls_page, "Abbreviations", docs.glossary.abbvs, 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) 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,y=3,text="Abbreviations >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_abbv_page.nav_to}
PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to} PushButton{parent=gls,text="Terminology >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=gls_term_page.nav_to}
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 -- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes} local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane) app.set_root_pane(u_pane)

View File

@ -1,13 +1,65 @@
local const = require("scada-common.constants")
local docs = {} 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 target
local function doc(key, name, desc) local function sect(name)
---@class pocket_doc_item ---@class pocket_doc_sect
local item = { key = key, name = name, desc = desc } local item = { type = DOC_ITEM_TYPE.SECTION, name = name }
table.insert(target, item) table.insert(target, item)
end 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 -- 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 = {} docs.alarms = {}
@ -28,15 +80,20 @@ doc("TurbineTripAlarm", "Turbine Trip", "A turbine stopped rotating, likely due
docs.annunc = { docs.annunc = {
unit = { unit = {
main_section = {}, rps_section = {}, rcs_section = {}, fac_section = {} main_section = {}, rps_section = {}, rcs_section = {}
},
facility = {
main_section = {}
} }
} }
target = docs.annunc.unit.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("PLCOnline", "PLC Online", "Indicates if the fission reactor PLC is connected. If it isn't, check that your PLC is on and configured properly.")
doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.") doc("PLCHeartbeat", "PLC Heartbeat", "An indicator of status data being live. As status messages are received from the PLC, this light will turn on and off. If it gets stuck, the supervisor has stopped receiving data or a screen has frozen.")
doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.") doc("RadiationMonitor", "Radiation Monitor", "On if at least one environment detector is connected and assigned to this unit.")
doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.") doc("AutoControl", "Automatic Control", "On if the reactor is under the control of one of the automatic control modes.")
sect("Safety Status")
doc("ReactorSCRAM", "Reactor SCRAM", "On if the reactor protection system is holding the reactor SCRAM'd.") 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("ManualReactorSCRAM", "Manual Reactor SCRAM", "On if the operator (you) initiated a SCRAM.")
doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.") doc("AutoReactorSCRAM", "Auto Reactor SCRAM", "On if the automatic control system initiated a SCRAM. The main view screen annunciator will have an indication as to why.")
@ -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("GeneratorTrip", "Generator Trip", "The turbine is no longer outputting power due to it having nowhere to go. Likely due to full power storage. This will lead to a Turbine Trip if not addressed.")
doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.") doc("TurbineTrip", "Turbine Trip", "The turbine has reached its maximum power charge and has stopped rotating, and as a result stopped cooling steam to water. Ensure the turbine has somewhere to output power, as this is the most common cause of reactor meltdowns. However, the likelihood of a meltdown with this system in place is much lower, especially with emergency coolant helping during turbine trips.")
target = docs.annunc.unit.fac_section target = docs.annunc.facility.main_section
doc("?", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.") sect("Connectivity")
doc("?", "Radiation Monitor", "At least one facility radiation monitor is connected") doc("all_sys_ok", "Unit Systems Online", "All unit systems (reactors, boilers, and turbines) are connected.")
doc("?", "Induction Matrix", "The induction matrix is connected.") doc("rad_computed_status", "Radiation Monitor", "At least one facility radiation monitor is connected")
doc("?", "SPS Connected", "Indicates if the super-critical phase shifter is connected.") doc("im_computed_status", "Induction Matrix", "The induction matrix is connected.")
doc("?", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.") doc("sps_computed_status", "SPS Connected", "Indicates if the super-critical phase shifter is connected.")
doc("?", "Process Active", "Automatic process control is active.") sect("Automatic Control")
doc("?", "Process Ramping", "Automatic process control is performing an initial ramp-up of the reactors for later PID control (generation and charge mode).") doc("auto_ready", "Configured Units Ready", "All units assigned to automatic control are ready to run automatic control.")
doc("?", "Min/Max Burn Rate", "Auto control has either commanded 0 mB/t or the maximum total burn rate available (from assigned units).") doc("auto_active", "Process Active", "Automatic process control is active.")
doc("?", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.") 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("?", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.") 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).")
doc("?", "Matrix Charge High", "Automatic SCRAM occurred due to induction matrix charge exceeding acceptable limit.") sect("Automatic SCRAM")
doc("?", "Unit Critical Alarm", "Automatic SCRAM occurred due to critical level unit alarm(s).") doc("auto_scram", "Automatic SCRAM", "Automatic control system SCRAM'ed the assigned reactors due to a safety hazard, shown by the below indicators.")
doc("?", "Facility Radiation High", "Automatic SCRAM occurred due to high facility radiation levels.") doc("as_matrix_dc", "Matrix Disconnected", "Automatic SCRAM occurred due to loss of induction matrix connection.")
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.") 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 = { docs.glossary = {
abbvs = {}, terms = {} 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_RCP", "RCP", "Reactor Coolant Pump. This is from real-world terminology with water-cooled (boiling water and pressurized water) reactors, but in this system it just reflects to the functioning of reactor coolant flow. See the annunciator page on it for more information.")
doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).") doc("G_RCS", "RCS", "Reactor Cooling System. The combination of all machines used to cool the reactor (turbines, boilers, dynamic tanks).")
doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.") doc("G_RPS", "RPS", "Reactor Protection System. A component of the reactor PLC responsible for keeping the reactor safe.")
doc("G_RTU", "RT", "co-RouTine. This is used to identify the status of core Lua co-routines on front panels.")
doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.") doc("G_RTU", "RTU", "Remote Terminal Unit. Provides monitoring to and basic output from a SCADA system, interfacing with various types of devices/interfaces.")
doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.") doc("G_SCADA", "SCADA", "Supervisory Control and Data Acquisition. A control systems architecture used in a wide variety process control applications.")
doc("G_SVR", "SVR", "Supervisor. Abbreviation for the supervisory computer.") 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 iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket") local pocket = require("pocket.pocket")
local control_app = require("pocket.ui.apps.control")
local diag_apps = require("pocket.ui.apps.diag_apps") local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app") local dummy_app = require("pocket.ui.apps.dummy_app")
local guide_app = require("pocket.ui.apps.guide") local guide_app = require("pocket.ui.apps.guide")
@ -62,6 +63,7 @@ local function init(main)
-- create all the apps & pages -- create all the apps & pages
home_page(page_div) home_page(page_div)
unit_app(page_div) unit_app(page_div)
control_app(page_div)
guide_app(page_div) guide_app(page_div)
loader_app(page_div) loader_app(page_div)
sys_apps(page_div) sys_apps(page_div)

View File

@ -1,6 +1,8 @@
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
local docs = require("pocket.ui.docs")
local core = require("graphics.core") local core = require("graphics.core")
local Div = require("graphics.elements.div") 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 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 ALIGN = core.ALIGN
local cpair = core.cpair local cpair = core.cpair
local DOC_TYPE = docs.DOC_ITEM_TYPE
local LIST_TYPE = docs.DOC_LIST_TYPE
-- new guide documentation section -- new guide documentation section
---@param data _guide_section_constructor_data ---@param data _guide_section_constructor_data
---@param base_page nav_tree_page ---@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} TextBox{parent=section_view_div,y=1,text=title,alignment=ALIGN.CENTER}
PushButton{parent=section_view_div,x=3,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=section_page.nav_to} PushButton{parent=section_view_div,x=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 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 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)} if item.type == DOC_TYPE.SECTION then
TextBox{parent=def_list,text=item.desc} ---@cast item pocket_doc_sect
_end = Div{parent=def_list,height=1,can_focus=true}
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() 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() view_page.nav_to()
anchor.focus() anchor.focus()
end end
@ -55,12 +100,46 @@ return function (data, base_page, title, items, scroll_height)
doc_map[item.key] = view doc_map[item.key] = view
table.insert(search_db, { string.lower(item.name), item.name, title, 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 if i % 12 == 0 then util.nop() end
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() 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=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=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=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=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} 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 cpair = core.cpair
local LEFT = core.ALIGN.LEFT
local RIGHT = core.ALIGN.RIGHT local RIGHT = core.ALIGN.RIGHT
local self = { local self = {
@ -606,7 +605,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local textbox local textbox
if height > 1 then 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 else
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT} textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
end end

View File

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

View File

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

View File

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

View File

@ -209,6 +209,23 @@ types.PROCESS_NAMES = {
"GEN_RATE_FAULT_IDLE" "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 ---@enum WASTE_MODE
types.WASTE_MODE = { types.WASTE_MODE = {
AUTO = 1, AUTO = 1,

View File

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

View File

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

View File

@ -1,6 +1,7 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local databus = require("supervisor.databus") local databus = require("supervisor.databus")
@ -15,6 +16,9 @@ local CRDN_TYPE = comms.CRDN_TYPE
local UNIT_COMMAND = comms.UNIT_COMMAND local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMAND = comms.FAC_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 local SV_Q_DATA = svqtypes.SV_Q_DATA
-- retry time constants in ms -- 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() facility.scram_all()
_send(CRDN_TYPE.FAC_CMD, { cmd, true }) _send(CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMAND.STOP then elseif cmd == FAC_COMMAND.STOP then
local was_active = facility.auto_is_active()
if was_active then
facility.auto_stop() 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 elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then if pkt.length == 6 then
---@type coord_auto_config ---@type sys_auto_config
local config = { local config = {
mode = pkt.data[2], mode = pkt.data[2],
burn_target = pkt.data[3], 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 -- continue if valid unit id
if util.is_int(uid) and uid > 0 and uid <= #self.units then if util.is_int(uid) and uid > 0 and uid <= #self.units then
local unit = self.units[uid] ---@type reactor_unit local unit = self.units[uid] ---@type reactor_unit
local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL
if cmd == UNIT_COMMAND.START then if cmd == UNIT_COMMAND.SCRAM then
out_queue.push_data(SV_Q_DATA.START, data)
elseif cmd == UNIT_COMMAND.SCRAM then
out_queue.push_data(SV_Q_DATA.SCRAM, data) 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 elseif cmd == UNIT_COMMAND.RESET_RPS then
out_queue.push_data(SV_Q_DATA.RESET_RPS, data) out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
elseif cmd == UNIT_COMMAND.SET_BURN then elseif cmd == UNIT_COMMAND.SET_BURN then
if pkt.length == 3 then if pkt.length == 3 then
if manual then
out_queue.push_data(SV_Q_DATA.SET_BURN, data) out_queue.push_data(SV_Q_DATA.SET_BURN, data)
end
else else
log.debug(log_tag .. "CRDN unit command burn rate missing option") log.debug(log_tag .. "CRDN unit command burn rate missing option")
end end
elseif cmd == UNIT_COMMAND.SET_WASTE then 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]) unit.set_waste_mode(pkt.data[3])
else else
log.debug(log_tag .. "CRDN unit command set waste missing/invalid option") 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") log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end end
elseif cmd == UNIT_COMMAND.SET_GROUP then elseif cmd == UNIT_COMMAND.SET_GROUP then
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]) facility.set_group(unit.get_id(), pkt.data[3])
_send(CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
else else
log.debug(log_tag .. "CRDN unit command set group missing group id") log.debug(log_tag .. "CRDN unit command set group missing group id")
end 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 elseif ack == false then
log.debug(log_tag .. "burn rate update failed!") log.debug(log_tag .. "burn rate update failed!")
end 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 elseif pkt.type == RPLC_TYPE.RPS_ENABLE then
-- enable acknowledgement -- enable acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)

View File

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