diff --git a/.github/workflows/shields.yml b/.github/workflows/manifest.yml
similarity index 52%
rename from .github/workflows/shields.yml
rename to .github/workflows/manifest.yml
index 50698af..d96b93e 100644
--- a/.github/workflows/shields.yml
+++ b/.github/workflows/manifest.yml
@@ -1,13 +1,13 @@
# Simple workflow for deploying static content to GitHub Pages
-name: Deploy Component Versions
+name: Deploy Installation Manifests and Versions
on:
- # Runs on pushes targeting the default branch
- push:
- branches: ["main"]
-
- # Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
+ push:
+ branches:
+ - main
+ - latest
+ - devel
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
@@ -35,13 +35,26 @@ jobs:
uses: actions/configure-pages@v3
- name: Setup Python
uses: actions/setup-python@v3.1.3
- - run: mkdir shields
- - run: python imgen.py shields
- - name: Upload artifact
+ - name: Extract branch name
+ shell: bash
+ run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
+ id: extract_branch
+ - name: Create outputs folder
+ shell: bash
+ run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/${{ steps.extract_branch.outputs.branch }}
+ - name: Generate manifest and shields for main branch
+ if: github.event.pull_request.base.ref == 'main'
+ run: python imgen.py gh_actions
+ - name: Generate only manifest for non-main branches
+ if: github.event.pull_request.base.ref != 'main'
+ run: python imgen.py
+ - name: Move manifest
+ run: mv install_manifest.json deploy/manifests/${{ steps.extract_branch.outputs.branch }}
+ - name: Upload artifacts
uses: actions/upload-pages-artifact@v1
with:
- # Upload shields JSON
- path: 'shields/'
+ # Upload manifest JSON
+ path: 'deploy/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
diff --git a/ccmsi.lua b/ccmsi.lua
index 962d5f0..a8fff8a 100644
--- a/ccmsi.lua
+++ b/ccmsi.lua
@@ -20,9 +20,10 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end
-local CCMSI_VERSION = "v1.0"
+local CCMSI_VERSION = "v1.2"
local install_dir = "/.install-cache"
+local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/"
local opts = { ... }
@@ -122,8 +123,8 @@ if mode == "check" then
-- GET REMOTE MANIFEST --
-------------------------
- if opts[2] then repo_path = repo_path .. opts[2] .. "/" else repo_path = repo_path .. "main/" end
- local install_manifest = repo_path .. "install_manifest.json"
+ if opts[2] then manifest_path = manifest_path .. opts[2] .. "/" else manifest_path = manifest_path .. "main/" end
+ local install_manifest = manifest_path .. "install_manifest.json"
local response, error = http.get(install_manifest)
@@ -203,7 +204,8 @@ elseif mode == "install" or mode == "update" then
-------------------------
if opts[3] then repo_path = repo_path .. opts[3] .. "/" else repo_path = repo_path .. "main/" end
- local install_manifest = repo_path .. "install_manifest.json"
+ if opts[3] then manifest_path = manifest_path .. opts[3] .. "/" else manifest_path = manifest_path .. "main/" end
+ local install_manifest = manifest_path .. "install_manifest.json"
local response, error = http.get(install_manifest)
diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua
index 486be82..6f586ef 100644
--- a/coordinator/coordinator.lua
+++ b/coordinator/coordinator.lua
@@ -335,12 +335,13 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
_send_establish()
clock.start()
+ elseif event == "timer" then
+ -- keep checking watchdog timers
+ apisessions.check_all_watchdogs(p1)
elseif event == "modem_message" then
-- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5)
- if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then
- public.handle_packet(packet)
- end
+ public.handle_packet(packet)
elseif event == "terminate" then
terminated = true
break
diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua
index 4aa0b53..fec6629 100644
--- a/coordinator/renderer.lua
+++ b/coordinator/renderer.lua
@@ -138,9 +138,9 @@ function renderer.close_ui()
-- stop blinking indicators
flasher.clear()
- -- hide to stop animation callbacks
- if engine.ui.main_display ~= nil then engine.ui.main_display.hide() end
- for _, display in ipairs(engine.ui.unit_displays) do display.hide() end
+ -- delete element trees
+ if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
+ for _, display in ipairs(engine.ui.unit_displays) do display.delete() end
-- report ui as not ready
engine.ui_ready = false
@@ -163,9 +163,9 @@ end
function renderer.ui_ready() return engine.ui_ready end
-- handle a touch event
----@param event mouse_interaction
+---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
- if engine.ui_ready then
+ if engine.ui_ready and event ~= nil then
if event.monitor == engine.monitors.primary_name then
engine.ui.main_display.handle_mouse(event)
else
diff --git a/coordinator/session/api.lua b/coordinator/session/api.lua
index 8f21483..4ba7383 100644
--- a/coordinator/session/api.lua
+++ b/coordinator/session/api.lua
@@ -117,8 +117,6 @@ function api.new_session(id, in_queue, out_queue, timeout)
-- process packet
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
---@cast pkt capi_frame
- -- feed watchdog
- self.conn_watchdog.feed()
-- handle packet by type
if pkt.type == nil then
diff --git a/coordinator/startup.lua b/coordinator/startup.lua
index c93b3ec..aacfe1b 100644
--- a/coordinator/startup.lua
+++ b/coordinator/startup.lua
@@ -4,23 +4,23 @@
require("/initenv").init_env()
-local crash = require("scada-common.crash")
-local log = require("scada-common.log")
-local ppm = require("scada-common.ppm")
-local tcallbackdsp = require("scada-common.tcallbackdsp")
-local util = require("scada-common.util")
+local crash = require("scada-common.crash")
+local log = require("scada-common.log")
+local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcd")
+local util = require("scada-common.util")
-local core = require("graphics.core")
+local core = require("graphics.core")
-local config = require("coordinator.config")
-local coordinator = require("coordinator.coordinator")
-local iocontrol = require("coordinator.iocontrol")
-local renderer = require("coordinator.renderer")
-local sounder = require("coordinator.sounder")
+local config = require("coordinator.config")
+local coordinator = require("coordinator.coordinator")
+local iocontrol = require("coordinator.iocontrol")
+local renderer = require("coordinator.renderer")
+local sounder = require("coordinator.sounder")
-local apisessions = require("coordinator.session.apisessions")
+local apisessions = require("coordinator.session.apisessions")
-local COORDINATOR_VERSION = "v0.13.8"
+local COORDINATOR_VERSION = "v0.15.8"
local println = util.println
local println_ts = util.println_ts
@@ -50,7 +50,6 @@ cfv.assert_type_num(config.SOUNDER_VOLUME)
cfv.assert_type_bool(config.TIME_24_HOUR)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
-cfv.assert_type_bool(config.LOG_DEBUG)
assert(cfv.valid(), "bad config file: missing/invalid fields")
@@ -58,7 +57,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
-- log init
----------------------------------------
-log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG)
+log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================")
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
@@ -335,7 +334,7 @@ local function main()
apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
- tcallbackdsp.handle(param1)
+ tcd.handle(param1)
end
elseif event == "modem_message" then
-- got a packet
@@ -359,7 +358,7 @@ local function main()
end
elseif event == "monitor_touch" then
-- handle a monitor touch event
- renderer.handle_mouse(core.events.touch(param1, param2, param3))
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "speaker_audio_empty" then
-- handle speaker buffer emptied
sounder.continue()
diff --git a/coordinator/ui/components/boiler.lua b/coordinator/ui/components/boiler.lua
index c4a433b..114a0bb 100644
--- a/coordinator/ui/components/boiler.lua
+++ b/coordinator/ui/components/boiler.lua
@@ -9,8 +9,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
-- new boiler view
---@param root graphics_element parent
@@ -27,9 +27,9 @@ local function new_view(root, x, y, ps)
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
- ps.subscribe("computed_status", status.update)
- ps.subscribe("temperature", temp.update)
- ps.subscribe("boil_rate", boil_r.update)
+ status.register(ps, "computed_status", status.update)
+ temp.register(ps, "temperature", temp.update)
+ boil_r.register(ps, "boil_rate", boil_r.update)
TextBox{parent=boiler,text="H",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=boiler,text="W",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
@@ -41,10 +41,10 @@ local function new_view(root, x, y, ps)
local steam = VerticalBar{parent=boiler,x=27,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local ccool = VerticalBar{parent=boiler,x=28,y=1,fg_bg=cpair(colors.lightBlue,colors.gray),height=4,width=1}
- ps.subscribe("hcool_fill", hcool.update)
- ps.subscribe("water_fill", water.update)
- ps.subscribe("steam_fill", steam.update)
- ps.subscribe("ccool_fill", ccool.update)
+ hcool.register(ps, "hcool_fill", hcool.update)
+ water.register(ps, "water_fill", water.update)
+ steam.register(ps, "steam_fill", steam.update)
+ ccool.register(ps, "ccool_fill", ccool.update)
end
return new_view
diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua
index 2910fbb..a234cbc 100644
--- a/coordinator/ui/components/imatrix.lua
+++ b/coordinator/ui/components/imatrix.lua
@@ -13,10 +13,10 @@ local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-- new induction matrix view
---@param root graphics_element parent
@@ -50,15 +50,15 @@ local function new_view(root, x, y, data, ps, id)
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg_bg}
- ps.subscribe("computed_status", status.update)
- ps.subscribe("energy", function (val) energy.update(util.joules_to_fe(val)) end)
- ps.subscribe("max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
- ps.subscribe("last_input", function (val) input.update(util.joules_to_fe(val)) end)
- ps.subscribe("last_output", function (val) output.update(util.joules_to_fe(val)) end)
+ status.register(ps, "computed_status", status.update)
+ energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
+ capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
+ input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
+ output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
- ps.subscribe("avg_charge", avg_chg.update)
- ps.subscribe("avg_inflow", avg_in.update)
- ps.subscribe("avg_outflow", avg_out.update)
+ avg_chg.register(ps, "avg_charge", avg_chg.update)
+ avg_in.register(ps, "avg_inflow", avg_in.update)
+ avg_out.register(ps, "avg_outflow", avg_out.update)
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg_bg}
@@ -68,10 +68,10 @@ local function new_view(root, x, y, data, ps, id)
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=label_fg_bg}
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg_bg}
- ps.subscribe("cells", cells.update)
- ps.subscribe("providers", providers.update)
- ps.subscribe("energy_fill", function (val) fill.update(val * 100) end)
- ps.subscribe("transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
+ cells.register(ps, "cells", cells.update)
+ providers.register(ps, "providers", providers.update)
+ fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
+ trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
@@ -88,9 +88,9 @@ local function new_view(root, x, y, data, ps, id)
end
end
- ps.subscribe("energy_fill", charge.update)
- ps.subscribe("last_input", function (val) in_cap.update(calc_saturation(val)) end)
- ps.subscribe("last_output", function (val) out_cap.update(calc_saturation(val)) end)
+ charge.register(ps, "energy_fill", charge.update)
+ in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
+ out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
end
return new_view
diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua
index 5b8d8ae..eaeb8ab 100644
--- a/coordinator/ui/components/processctl.lua
+++ b/coordinator/ui/components/processctl.lua
@@ -1,4 +1,4 @@
-local tcd = require("scada-common.tcallbackdsp")
+local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
@@ -21,10 +21,10 @@ local HazardButton = require("graphics.elements.controls.hazard_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
local period = core.flasher.PERIOD
@@ -33,7 +33,7 @@ local period = core.flasher.PERIOD
---@param x integer top left x
---@param y integer top left y
local function new_view(root, x, y)
- assert(root.height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
+ assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
@@ -55,9 +55,9 @@ local function new_view(root, x, y)
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
- facility.ps.subscribe("all_sys_ok", all_ok.update)
- facility.induction_ps_tbl[1].subscribe("computed_status", function (status) ind_mat.update(status > 1) end)
- facility.ps.subscribe("rad_computed_status", rad_mon.update)
+ all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
+ ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
+ rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
main.line_break()
@@ -66,10 +66,10 @@ local function new_view(root, x, y)
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
- facility.ps.subscribe("auto_ready", auto_ready.update)
- facility.ps.subscribe("auto_active", auto_act.update)
- facility.ps.subscribe("auto_ramping", auto_ramp.update)
- facility.ps.subscribe("auto_saturated", auto_sat.update)
+ auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
+ auto_act.register(facility.ps, "auto_active", auto_act.update)
+ auto_ramp.register(facility.ps, "auto_ramping", auto_ramp.update)
+ auto_sat.register(facility.ps, "auto_saturated", auto_sat.update)
main.line_break()
@@ -80,20 +80,20 @@ local function new_view(root, x, y)
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
- facility.ps.subscribe("auto_scram", auto_scram.update)
- facility.ps.subscribe("as_matrix_dc", matrix_dc.update)
- facility.ps.subscribe("as_matrix_fill", matrix_fill.update)
- facility.ps.subscribe("as_crit_alarm", unit_crit.update)
- facility.ps.subscribe("as_radiation", fac_rad_h.update)
- facility.ps.subscribe("as_gen_fault", gen_fault.update)
+ auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
+ matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
+ matrix_fill.register(facility.ps, "as_matrix_fill", matrix_fill.update)
+ unit_crit.register(facility.ps, "as_crit_alarm", unit_crit.update)
+ fac_rad_h.register(facility.ps, "as_radiation", fac_rad_h.update)
+ gen_fault.register(facility.ps, "as_gen_fault", gen_fault.update)
TextBox{parent=main,y=23,text="Radiation",height=1,width=13,fg_bg=style.label}
local radiation = RadIndicator{parent=main,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
- facility.ps.subscribe("radiation", radiation.update)
+ radiation.register(facility.ps, "radiation", radiation.update)
TextBox{parent=main,x=15,y=23,text="Linked RTUs",height=1,width=11,fg_bg=style.label}
local rtu_count = DataIndicator{parent=main,x=15,y=24,label="",format="%11d",value=0,lu_colors=lu_cpair,width=11,fg_bg=bw_fg_bg}
- facility.ps.subscribe("rtu_count", rtu_count.update)
+ rtu_count.register(facility.ps, "rtu_count", rtu_count.update)
---------------------
-- process control --
@@ -115,8 +115,8 @@ local function new_view(root, x, y)
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
- facility.ps.subscribe("process_burn_target", b_target.set_value)
- facility.ps.subscribe("burn_sum", burn_sum.update)
+ b_target.register(facility.ps, "process_burn_target", b_target.set_value)
+ burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
@@ -126,8 +126,8 @@ local function new_view(root, x, y)
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
- facility.ps.subscribe("process_charge_target", c_target.set_value)
- facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
+ c_target.register(facility.ps, "process_charge_target", c_target.set_value)
+ cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
@@ -137,8 +137,8 @@ local function new_view(root, x, y)
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
- facility.ps.subscribe("process_gen_target", g_target.set_value)
- facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
+ g_target.register(facility.ps, "process_gen_target", g_target.set_value)
+ cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
-----------------
-- unit limits --
@@ -160,12 +160,12 @@ local function new_view(root, x, y)
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
- unit.unit_ps.subscribe("max_burn", rate_limits[i].set_max)
- unit.unit_ps.subscribe("burn_limit", rate_limits[i].set_value)
+ rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
+ rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
- unit.unit_ps.subscribe("act_burn_rate", cur_burn.update)
+ cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
end
-------------------
@@ -186,8 +186,8 @@ local function new_view(root, x, y)
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)}
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
- unit.unit_ps.subscribe("U_AutoReady", ready.update)
- unit.unit_ps.subscribe("U_AutoDegraded", degraded.update)
+ ready.register(unit.unit_ps, "U_AutoReady", ready.update)
+ degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
end
-------------------------
@@ -197,14 +197,14 @@ local function new_view(root, x, y)
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
- facility.ps.subscribe("process_mode", mode.set_value)
+ mode.register(facility.ps, "process_mode", mode.set_value)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
- facility.ps.subscribe("status_line_1", stat_line_1.set_value)
- facility.ps.subscribe("status_line_2", stat_line_2.set_value)
+ stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
+ stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
@@ -233,11 +233,14 @@ local function new_view(root, x, y)
tcd.dispatch(0.2, function () save.on_response(ack) end)
end
- facility.ps.subscribe("auto_ready", function (ready)
+ start.register(facility.ps, "auto_ready", function (ready)
if ready and (not facility.auto_active) then start.enable() else start.disable() end
end)
- facility.ps.subscribe("auto_active", function (active)
+ -- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree when it comes
+ -- to the process control display and coordinator GUI as a whole, child elements will not directly be registered here
+ -- (preventing garbage collection until the parent 'proc' is deleted)
+ proc.register(facility.ps, "auto_active", function (active)
if active then
b_target.disable()
c_target.disable()
@@ -246,9 +249,7 @@ local function new_view(root, x, y)
mode.disable()
start.disable()
- for i = 1, #rate_limits do
- rate_limits[i].disable()
- end
+ for i = 1, #rate_limits do rate_limits[i].disable() end
else
b_target.enable()
c_target.enable()
@@ -257,9 +258,7 @@ local function new_view(root, x, y)
mode.enable()
if facility.auto_ready then start.enable() end
- for i = 1, #rate_limits do
- rate_limits[i].enable()
- end
+ for i = 1, #rate_limits do rate_limits[i].enable() end
end
end)
end
diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua
index db75fb1..b4a2c1b 100644
--- a/coordinator/ui/components/reactor.lua
+++ b/coordinator/ui/components/reactor.lua
@@ -11,8 +11,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local HorizontalBar = require("graphics.elements.indicators.hbar")
local StateIndicator = require("graphics.elements.indicators.state")
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
-- create new reactor view
---@param root graphics_element parent
@@ -30,10 +30,10 @@ local function new_view(root, x, y, ps)
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
local heating_r = DataIndicator{parent=reactor,x=2,y=5,lu_colors=lu_col,label="Heating:",unit="mB/t",format="%12.0f",value=0,commas=true,width=26,fg_bg=text_fg_bg}
- ps.subscribe("computed_status", status.update)
- ps.subscribe("temp", core_temp.update)
- ps.subscribe("act_burn_rate", burn_r.update)
- ps.subscribe("heating_rate", heating_r.update)
+ status.register(ps, "computed_status", status.update)
+ core_temp.register(ps, "temp", core_temp.update)
+ burn_r.register(ps, "act_burn_rate", burn_r.update)
+ heating_r.register(ps, "heating_rate", heating_r.update)
local reactor_fills = Rectangle{parent=root,border=border(1, colors.gray, true),width=24,height=7,x=(x + 29),y=y}
@@ -47,7 +47,7 @@ local function new_view(root, x, y, ps)
local hcool = HorizontalBar{parent=reactor_fills,x=8,y=4,show_percent=true,bar_fg_bg=cpair(colors.white,colors.gray),height=1,width=14}
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
- ps.subscribe("ccool_type", function (type)
+ ccool.register(ps, "ccool_type", function (type)
if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
@@ -55,7 +55,7 @@ local function new_view(root, x, y, ps)
end
end)
- ps.subscribe("hcool_type", function (type)
+ hcool.register(ps, "hcool_type", function (type)
if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray))
else
@@ -63,10 +63,10 @@ local function new_view(root, x, y, ps)
end
end)
- ps.subscribe("fuel_fill", fuel.update)
- ps.subscribe("ccool_fill", ccool.update)
- ps.subscribe("hcool_fill", hcool.update)
- ps.subscribe("waste_fill", waste.update)
+ fuel.register(ps, "fuel_fill", fuel.update)
+ ccool.register(ps, "ccool_fill", ccool.update)
+ hcool.register(ps, "hcool_fill", hcool.update)
+ waste.register(ps, "waste_fill", waste.update)
end
return new_view
diff --git a/coordinator/ui/components/turbine.lua b/coordinator/ui/components/turbine.lua
index e4d6967..0e4cb21 100644
--- a/coordinator/ui/components/turbine.lua
+++ b/coordinator/ui/components/turbine.lua
@@ -12,8 +12,8 @@ local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
-- new turbine view
---@param root graphics_element parent
@@ -30,9 +30,9 @@ local function new_view(root, x, y, ps)
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}
local flow_rate = DataIndicator{parent=turbine,x=5,y=4,lu_colors=lu_col,label="",unit="mB/t",format="%10.0f",value=0,commas=true,width=16,fg_bg=text_fg_bg}
- ps.subscribe("computed_status", status.update)
- ps.subscribe("prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
- ps.subscribe("flow_rate", flow_rate.update)
+ status.register(ps, "computed_status", status.update)
+ prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
+ flow_rate.register(ps, "flow_rate", flow_rate.update)
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
@@ -40,8 +40,8 @@ local function new_view(root, x, y, ps)
TextBox{parent=turbine,text="S",x=2,y=5,height=1,width=1,fg_bg=text_fg_bg}
TextBox{parent=turbine,text="E",x=3,y=5,height=1,width=1,fg_bg=text_fg_bg}
- ps.subscribe("steam_fill", steam.update)
- ps.subscribe("energy_fill", energy.update)
+ steam.register(ps, "steam_fill", steam.update)
+ energy.register(ps, "energy_fill", energy.update)
end
return new_view
diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua
index a29dd4b..30e2044 100644
--- a/coordinator/ui/components/unit_detail.lua
+++ b/coordinator/ui/components/unit_detail.lua
@@ -26,10 +26,10 @@ local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
local period = core.flasher.PERIOD
@@ -79,16 +79,16 @@ local function init(parent, id)
-----------------------------
local core_map = CoreMap{parent=main,x=2,y=3,reactor_l=18,reactor_w=18}
- u_ps.subscribe("temp", core_map.update)
- u_ps.subscribe("size", function (s) core_map.resize(s[1], s[2]) end)
+ core_map.register(u_ps, "temp", core_map.update)
+ core_map.register(u_ps, "size", function (s) core_map.resize(s[1], s[2]) end)
TextBox{parent=main,x=12,y=22,text="Heating Rate",height=1,width=12,fg_bg=style.label}
local heating_r = DataIndicator{parent=main,x=12,label="",format="%14.0f",value=0,unit="mB/t",commas=true,lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
- u_ps.subscribe("heating_rate", heating_r.update)
+ heating_r.register(u_ps, "heating_rate", heating_r.update)
TextBox{parent=main,x=12,y=25,text="Commanded Burn Rate",height=1,width=19,fg_bg=style.label}
local burn_r = DataIndicator{parent=main,x=12,label="",format="%14.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=19,fg_bg=bw_fg_bg}
- u_ps.subscribe("burn_rate", burn_r.update)
+ burn_r.register(u_ps, "burn_rate", burn_r.update)
TextBox{parent=main,text="F",x=2,y=22,width=1,height=1,fg_bg=style.label}
TextBox{parent=main,text="C",x=4,y=22,width=1,height=1,fg_bg=style.label}
@@ -102,12 +102,12 @@ local function init(parent, id)
local hcool = VerticalBar{parent=main,x=8,y=23,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
local waste = VerticalBar{parent=main,x=10,y=23,fg_bg=cpair(colors.brown,colors.gray),height=4,width=1}
- u_ps.subscribe("fuel_fill", fuel.update)
- u_ps.subscribe("ccool_fill", ccool.update)
- u_ps.subscribe("hcool_fill", hcool.update)
- u_ps.subscribe("waste_fill", waste.update)
+ fuel.register(u_ps, "fuel_fill", fuel.update)
+ ccool.register(u_ps, "ccool_fill", ccool.update)
+ hcool.register(u_ps, "hcool_fill", hcool.update)
+ waste.register(u_ps, "waste_fill", waste.update)
- u_ps.subscribe("ccool_type", function (type)
+ ccool.register(u_ps, "ccool_type", function (type)
if type == "mekanism:sodium" then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
@@ -115,7 +115,7 @@ local function init(parent, id)
end
end)
- u_ps.subscribe("hcool_type", function (type)
+ hcool.register(u_ps, "hcool_type", function (type)
if type == "mekanism:superheated_sodium" then
hcool.recolor(cpair(colors.orange, colors.gray))
else
@@ -125,19 +125,19 @@ local function init(parent, id)
TextBox{parent=main,x=32,y=22,text="Core Temp",height=1,width=9,fg_bg=style.label}
local core_temp = DataIndicator{parent=main,x=32,label="",format="%11.2f",value=0,unit="K",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
- u_ps.subscribe("temp", core_temp.update)
+ core_temp.register(u_ps, "temp", core_temp.update)
TextBox{parent=main,x=32,y=25,text="Burn Rate",height=1,width=9,fg_bg=style.label}
local act_burn_r = DataIndicator{parent=main,x=32,label="",format="%8.2f",value=0,unit="mB/t",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
- u_ps.subscribe("act_burn_rate", act_burn_r.update)
+ act_burn_r.register(u_ps, "act_burn_rate", act_burn_r.update)
TextBox{parent=main,x=32,y=28,text="Damage",height=1,width=6,fg_bg=style.label}
local damage_p = DataIndicator{parent=main,x=32,label="",format="%11.0f",value=0,unit="%",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
- u_ps.subscribe("damage", damage_p.update)
+ damage_p.register(u_ps, "damage", damage_p.update)
TextBox{parent=main,x=32,y=31,text="Radiation",height=1,width=21,fg_bg=style.label}
local radiation = RadIndicator{parent=main,x=32,label="",format="%9.3f",lu_colors=lu_cpair,width=13,fg_bg=bw_fg_bg}
- u_ps.subscribe("radiation", radiation.update)
+ radiation.register(u_ps, "radiation", radiation.update)
-------------------
-- system status --
@@ -147,8 +147,8 @@ local function init(parent, id)
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
- u_ps.subscribe("U_StatusLine1", stat_line_1.set_value)
- u_ps.subscribe("U_StatusLine2", stat_line_2.set_value)
+ stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
+ stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
-----------------
-- annunciator --
@@ -163,9 +163,9 @@ local function init(parent, id)
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
- u_ps.subscribe("PLCOnline", plc_online.update)
- u_ps.subscribe("PLCHeartbeat", plc_hbeat.update)
- u_ps.subscribe("RadiationMonitor", rad_mon.update)
+ plc_online.register(u_ps, "PLCOnline", plc_online.update)
+ plc_hbeat.register(u_ps, "PLCHeartbeat", plc_hbeat.update)
+ rad_mon.register(u_ps, "RadiationMonitor", rad_mon.update)
annunciator.line_break()
@@ -173,8 +173,8 @@ local function init(parent, id)
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
- u_ps.subscribe("status", r_active.update)
- u_ps.subscribe("AutoControl", r_auto.update)
+ r_active.register(u_ps, "status", r_active.update)
+ r_auto.register(u_ps, "AutoControl", r_auto.update)
-- main unit transient/warning annunciator panel
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
@@ -190,18 +190,18 @@ local function init(parent, id)
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
- u_ps.subscribe("ReactorSCRAM", r_scram.update)
- u_ps.subscribe("ManualReactorSCRAM", r_mscrm.update)
- u_ps.subscribe("AutoReactorSCRAM", r_ascrm.update)
- u_ps.subscribe("RadiationWarning", rad_wrn.update)
- u_ps.subscribe("RCPTrip", r_rtrip.update)
- u_ps.subscribe("RCSFlowLow", r_cflow.update)
- u_ps.subscribe("CoolantLevelLow", r_clow.update)
- u_ps.subscribe("ReactorTempHigh", r_temp.update)
- u_ps.subscribe("ReactorHighDeltaT", r_rhdt.update)
- u_ps.subscribe("FuelInputRateLow", r_firl.update)
- u_ps.subscribe("WasteLineOcclusion", r_wloc.update)
- u_ps.subscribe("HighStartupRate", r_hsrt.update)
+ r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
+ r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
+ r_ascrm.register(u_ps, "AutoReactorSCRAM", r_ascrm.update)
+ rad_wrn.register(u_ps, "RadiationWarning", rad_wrn.update)
+ r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
+ r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
+ r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
+ r_temp.register(u_ps, "ReactorTempHigh", r_temp.update)
+ r_rhdt.register(u_ps, "ReactorHighDeltaT", r_rhdt.update)
+ r_firl.register(u_ps, "FuelInputRateLow", r_firl.update)
+ r_wloc.register(u_ps, "WasteLineOcclusion", r_wloc.update)
+ r_hsrt.register(u_ps, "HighStartupRate", r_hsrt.update)
-- RPS annunciator panel
@@ -220,16 +220,16 @@ local function init(parent, id)
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
- u_ps.subscribe("rps_tripped", rps_trp.update)
- u_ps.subscribe("high_dmg", rps_dmg.update)
- u_ps.subscribe("ex_hcool", rps_exh.update)
- u_ps.subscribe("ex_waste", rps_exw.update)
- u_ps.subscribe("high_temp", rps_tmp.update)
- u_ps.subscribe("no_fuel", rps_nof.update)
- u_ps.subscribe("low_cool", rps_loc.update)
- u_ps.subscribe("fault", rps_flt.update)
- u_ps.subscribe("timeout", rps_tmo.update)
- u_ps.subscribe("sys_fail", rps_sfl.update)
+ rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
+ rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
+ rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
+ rps_exw.register(u_ps, "ex_waste", rps_exw.update)
+ rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
+ rps_nof.register(u_ps, "no_fuel", rps_nof.update)
+ rps_loc.register(u_ps, "low_cool", rps_loc.update)
+ rps_flt.register(u_ps, "fault", rps_flt.update)
+ rps_tmo.register(u_ps, "timeout", rps_tmo.update)
+ rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
-- cooling annunciator panel
@@ -245,12 +245,12 @@ local function init(parent, id)
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
- u_ps.subscribe("RCSFault", c_flt.update)
- u_ps.subscribe("EmergencyCoolant", c_emg.update)
- u_ps.subscribe("CoolantFeedMismatch", c_cfm.update)
- u_ps.subscribe("BoilRateMismatch", c_brm.update)
- u_ps.subscribe("SteamFeedMismatch", c_sfm.update)
- u_ps.subscribe("MaxWaterReturnFeed", c_mwrf.update)
+ c_flt.register(u_ps, "RCSFault", c_flt.update)
+ c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
+ c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
+ c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
+ c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
+ c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
local available_space = 16 - (unit.num_boilers * 2 + unit.num_turbines * 4)
@@ -267,11 +267,11 @@ local function init(parent, id)
if unit.num_boilers > 0 then
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
- b_ps[1].subscribe("WasterLevelLow", b1_wll.update)
+ b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
- b_ps[1].subscribe("HeatingRateLow", b1_hr.update)
+ b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
end
if unit.num_boilers > 1 then
-- note, can't (shouldn't for sure...) have 0 turbines
@@ -283,11 +283,11 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
- b_ps[2].subscribe("WasterLevelLow", b2_wll.update)
+ b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
- b_ps[2].subscribe("HeatingRateLow", b2_hr.update)
+ b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
end
-- turbine annunciator panels
@@ -296,19 +296,19 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
- t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
+ t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
- t_ps[1].subscribe("TurbineOverSpeed", t1_tos.update)
+ t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
- t_ps[1].subscribe("GeneratorTrip", t1_gtrp.update)
+ t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
- t_ps[1].subscribe("TurbineTrip", t1_trp.update)
+ t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
if unit.num_turbines > 1 then
if (available_space > 2 and unit.num_turbines == 2) or available_space > 3 then
@@ -317,19 +317,19 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
- t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
+ t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
- t_ps[2].subscribe("TurbineOverSpeed", t2_tos.update)
+ t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
- t_ps[2].subscribe("GeneratorTrip", t2_gtrp.update)
+ t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
- t_ps[2].subscribe("TurbineTrip", t2_trp.update)
+ t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
end
if unit.num_turbines > 2 then
@@ -337,19 +337,19 @@ local function init(parent, id)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
- t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
+ t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
- t_ps[3].subscribe("TurbineOverSpeed", t3_tos.update)
+ t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
- t_ps[3].subscribe("GeneratorTrip", t3_gtrp.update)
+ t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
- t_ps[3].subscribe("TurbineTrip", t3_trp.update)
+ t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
end
----------------------
@@ -365,8 +365,8 @@ local function init(parent, id)
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn}
- u_ps.subscribe("burn_rate", burn_rate.set_value)
- u_ps.subscribe("max_burn", burn_rate.set_max)
+ burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
+ burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
local start = HazardButton{parent=main,x=2,y=28,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=unit.start,fg_bg=hzd_fg_bg}
local ack_a = HazardButton{parent=main,x=12,y=32,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=unit.ack_alarms,fg_bg=hzd_fg_bg}
@@ -387,9 +387,12 @@ local function init(parent, id)
end
end
- u_ps.subscribe("status", start_button_en_check)
- u_ps.subscribe("rps_tripped", start_button_en_check)
- u_ps.subscribe("rps_tripped", function (active) if active then reset.enable() else reset.disable() 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)
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
@@ -397,7 +400,7 @@ local function init(parent, id)
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6}
- u_ps.subscribe("U_WasteMode", waste_mode.set_value)
+ waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
----------------------
-- alarm management --
@@ -420,20 +423,20 @@ local function init(parent, id)
local a_clt = AlarmLight{parent=alarm_panel,x=6,label="RCS Transient",c1=colors.gray,c2=colors.yellow,c3=colors.green,flash=true,period=period.BLINK_500_MS}
local a_tbt = AlarmLight{parent=alarm_panel,x=6,label="Turbine Trip",c1=colors.gray,c2=colors.red,c3=colors.green,flash=true,period=period.BLINK_250_MS}
- u_ps.subscribe("Alarm_1", a_brc.update)
- u_ps.subscribe("Alarm_2", a_rad.update)
- u_ps.subscribe("Alarm_4", a_dmg.update)
+ a_brc.register(u_ps, "Alarm_1", a_brc.update)
+ a_rad.register(u_ps, "Alarm_2", a_rad.update)
+ a_dmg.register(u_ps, "Alarm_4", a_dmg.update)
- u_ps.subscribe("Alarm_3", a_rcl.update)
- u_ps.subscribe("Alarm_5", a_rcd.update)
- u_ps.subscribe("Alarm_6", a_rot.update)
- u_ps.subscribe("Alarm_7", a_rht.update)
- u_ps.subscribe("Alarm_8", a_rwl.update)
- u_ps.subscribe("Alarm_9", a_rwh.update)
+ a_rcl.register(u_ps, "Alarm_3", a_rcl.update)
+ a_rcd.register(u_ps, "Alarm_5", a_rcd.update)
+ a_rot.register(u_ps, "Alarm_6", a_rot.update)
+ a_rht.register(u_ps, "Alarm_7", a_rht.update)
+ a_rwl.register(u_ps, "Alarm_8", a_rwl.update)
+ a_rwh.register(u_ps, "Alarm_9", a_rwh.update)
- u_ps.subscribe("Alarm_10", a_rps.update)
- u_ps.subscribe("Alarm_11", a_clt.update)
- u_ps.subscribe("Alarm_12", a_tbt.update)
+ a_rps.register(u_ps, "Alarm_10", a_rps.update)
+ a_clt.register(u_ps, "Alarm_11", a_clt.update)
+ a_tbt.register(u_ps, "Alarm_12", a_tbt.update)
-- ack's and resets
@@ -487,7 +490,7 @@ local function init(parent, id)
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
- u_ps.subscribe("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)
auto_div.line_break()
@@ -499,44 +502,35 @@ local function init(parent, id)
TextBox{parent=auto_div,text="Prio. Group",height=1,width=11,fg_bg=style.label}
local auto_grp = TextBox{parent=auto_div,text="Manual",height=1,width=11,fg_bg=bw_fg_bg}
- u_ps.subscribe("auto_group", auto_grp.set_value)
+ auto_grp.register(u_ps, "auto_group", auto_grp.set_value)
auto_div.line_break()
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)}
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS}
- u_ps.subscribe("U_AutoReady", a_rdy.update)
+ a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
-- update standby indicator
- u_ps.subscribe("status", function (active)
+ a_stb.register(u_ps, "status", function (active)
a_stb.update(unit.annunciator.AutoControl and (not active))
end)
-
- -- enable and disable controls based on group assignment
- u_ps.subscribe("auto_group_id", function (gid)
- start_button_en_check()
-
- if gid == 0 then
- burn_rate.enable()
- set_burn_btn.enable()
- else
- burn_rate.disable()
- set_burn_btn.disable()
- end
- end)
-
- -- enable and disable controls based on auto control state (start button is handled separately)
- u_ps.subscribe("AutoControl", function (auto_active)
- start_button_en_check()
-
+ a_stb.register(u_ps, "AutoControl", function (auto_active)
if auto_active then
a_stb.update(unit.reactor_data.mek_status.status == false)
else a_stb.update(false) end
end)
+ -- enable/disable controls based on group assignment (start button is separate)
+ burn_rate.register(u_ps, "auto_group_id", function (gid)
+ if gid == 0 then burn_rate.enable() else burn_rate.disable() end
+ end)
+ set_burn_btn.register(u_ps, "auto_group_id", function (gid)
+ if gid == 0 then set_burn_btn.enable() else set_burn_btn.disable() end
+ end)
+
-- can't change group if auto is engaged regardless of if this unit is part of auto control
- f_ps.subscribe("auto_active", function (auto_active)
+ set_grp_btn.register(f_ps, "auto_active", function (auto_active)
if auto_active then set_grp_btn.disable() else set_grp_btn.enable() end
end)
diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua
index 24bc02e..3af8c83 100644
--- a/coordinator/ui/components/unit_overview.lua
+++ b/coordinator/ui/components/unit_overview.lua
@@ -14,9 +14,9 @@ local Div = require("graphics.elements.div")
local PipeNetwork = require("graphics.elements.pipenet")
local TextBox = require("graphics.elements.textbox")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local pipe = core.graphics.pipe
+local pipe = core.pipe
-- make a new unit overview window
---@param parent graphics_element parent
@@ -38,7 +38,7 @@ local function make(parent, x, y, unit)
height = 17
end
- assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
+ assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
-- bounding box div
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua
index a7f8ae2..a7b3c86 100644
--- a/coordinator/ui/layout/main_view.lua
+++ b/coordinator/ui/layout/main_view.lua
@@ -18,9 +18,9 @@ local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- create new main view
---@param main graphics_element main displaybox
@@ -32,10 +32,10 @@ local function init(main)
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
- local datetime = TextBox{parent=main,x=(header.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
+ local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
- facility.ps.subscribe("sv_ping", ping.update)
- facility.ps.subscribe("date_time", datetime.set_value)
+ ping.register(facility.ps, "sv_ping", ping.update)
+ datetime.register(facility.ps, "date_time", datetime.set_value)
local uo_1, uo_2, uo_3, uo_4 ---@type graphics_element
@@ -45,12 +45,12 @@ local function init(main)
-- unit overviews
if facility.num_units >= 1 then
uo_1 = unit_overview(main, 2, 3, units[1])
- row_1_height = uo_1.height()
+ row_1_height = uo_1.get_height()
end
if facility.num_units >= 2 then
uo_2 = unit_overview(main, 84, 3, units[2])
- row_1_height = math.max(row_1_height, uo_2.height())
+ row_1_height = math.max(row_1_height, uo_2.get_height())
end
cnc_y_start = cnc_y_start + row_1_height + 1
@@ -60,11 +60,11 @@ local function init(main)
local row_2_offset = cnc_y_start
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
- cnc_y_start = row_2_offset + uo_3.height() + 1
+ cnc_y_start = row_2_offset + uo_3.get_height() + 1
if facility.num_units == 4 then
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
- cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.height() + 1)
+ cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
end
end
@@ -73,11 +73,11 @@ local function init(main)
cnc_y_start = cnc_y_start
-- induction matrix and process control interfaces are 24 tall + space needed for divider
- local cnc_bottom_align_start = main.height() - 26
+ local cnc_bottom_align_start = main.get_height() - 26
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
- TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
+ TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.get_width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
cnc_bottom_align_start = cnc_bottom_align_start + 2
diff --git a/coordinator/ui/style.lua b/coordinator/ui/style.lua
index 74923f2..b78fc91 100644
--- a/coordinator/ui/style.lua
+++ b/coordinator/ui/style.lua
@@ -6,7 +6,7 @@ local core = require("graphics.core")
local style = {}
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- GLOBAL --
diff --git a/graphics/core.lua b/graphics/core.lua
index d03e551..58b6b8c 100644
--- a/graphics/core.lua
+++ b/graphics/core.lua
@@ -1,96 +1,19 @@
--
--- Graphics Core Functions and Objects
+-- Graphics Core Types, Checks, and Constructors
--
+local events = require("graphics.events")
+local flasher = require("graphics.flasher")
+
local core = {}
-local flasher = require("graphics.flasher")
-
core.flasher = flasher
-
-local events = {}
-
----@enum click_type
-events.click_type = {
- VIRTUAL = 0,
- LEFT_BUTTON = 1,
- RIGHT_BUTTON = 2,
- MID_BUTTON = 3
-}
-
----@class mouse_interaction
----@field monitor string
----@field button integer
----@field x integer
----@field y integer
-
--- create a new monitor touch mouse interaction event
----@nodiscard
----@param monitor string
----@param x integer
----@param y integer
----@return mouse_interaction
-function events.touch(monitor, x, y)
- return {
- monitor = monitor,
- button = events.click_type.LEFT_BUTTON,
- x = x,
- y = y
- }
-end
-
--- create a new mouse click mouse interaction event
----@nodiscard
----@param button click_type
----@param x integer
----@param y integer
----@return mouse_interaction
-function events.click(button, x, y)
- return {
- monitor = "terminal",
- button = button,
- x = x,
- y = y
- }
-end
-
--- create a new transposed mouse interaction event using the event's monitor/button fields
----@nodiscard
----@param event mouse_interaction
----@param new_x integer
----@param new_y integer
----@return mouse_interaction
-function events.mouse_transposed(event, new_x, new_y)
- return {
- monitor = event.monitor,
- button = event.button,
- x = new_x,
- y = new_y
- }
-end
-
--- create a new generic mouse interaction event
----@nodiscard
----@param monitor string
----@param button click_type
----@param x integer
----@param y integer
----@return mouse_interaction
-function events.mouse_generic(monitor, button, x, y)
- return {
- monitor = monitor,
- button = button,
- x = x,
- y = y
- }
-end
-
core.events = events
-local graphics = {}
+-- Core Types
---@enum TEXT_ALIGN
-graphics.TEXT_ALIGN = {
+core.TEXT_ALIGN = {
LEFT = 1,
CENTER = 2,
RIGHT = 3
@@ -109,7 +32,7 @@ graphics.TEXT_ALIGN = {
---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
---@return graphics_border
-function graphics.border(width, color, even)
+function core.border(width, color, even)
return {
width = width,
color = color,
@@ -130,7 +53,7 @@ end
---@param w integer
---@param h integer
---@return graphics_frame
-function graphics.gframe(x, y, w, h)
+function core.gframe(x, y, w, h)
return {
x = x,
y = y,
@@ -154,7 +77,7 @@ end
---@param a color
---@param b color
---@return cpair
-function graphics.cpair(a, b)
+function core.cpair(a, b)
return {
-- color pairs
color_a = a,
@@ -191,7 +114,7 @@ end
---@param thin? boolean true for 1 subpixel, false (default) for 2
---@param align_tr? boolean false to align bottom left (default), true to align top right
---@return pipe
-function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
+function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
return {
x1 = x1,
y1 = y1,
@@ -205,6 +128,4 @@ function graphics.pipe(x1, y1, x2, y2, color, thin, align_tr)
}
end
-core.graphics = graphics
-
return core
diff --git a/graphics/element.lua b/graphics/element.lua
index 04d4fd2..ae2daee 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -12,12 +12,11 @@ local element = {}
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer next line if omitted
----@field offset_x? integer 0 if omitted
----@field offset_y? integer 0 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
---@alias graphics_args graphics_args_generic
---|waiting_args
@@ -28,6 +27,7 @@ local element = {}
---|sidebar_args
---|spinbox_args
---|switch_button_args
+---|tabbar_args
---|alarm_indicator_light
---|core_map_args
---|data_indicator_args
@@ -45,38 +45,47 @@ local element = {}
---|colormap_args
---|displaybox_args
---|div_args
+---|listbox_args
---|multipane_args
---|pipenet_args
---|rectangle_args
---|textbox_args
---|tiling_args
+---@class element_subscription
+---@field ps psil ps used
+---@field key string data key
+---@field func function callback
+
-- a base graphics element, should not be created on its own
---@nodiscard
---@param args graphics_args arguments
function element.new(args)
local self = {
- id = -1,
+ id = nil, ---@type element_id|nil
elem_type = debug.getinfo(2).name,
define_completed = false,
- p_window = nil, ---@type table
- position = { x = 1, y = 1 },
- child_offset = { x = 0, y = 0 },
- bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1},
+ p_window = nil, ---@type table
+ position = { x = 1, y = 1 }, ---@type coordinate_2d
+ bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds
next_y = 1,
- children = {},
+ subscriptions = {},
mt = {}
}
- ---@class graphics_template
+ ---@class graphics_base
local protected = {
enabled = true,
- value = nil, ---@type any
- window = nil, ---@type table
- fg_bg = core.graphics.cpair(colors.white, colors.black),
- frame = core.graphics.gframe(1, 1, 1, 1)
+ value = nil, ---@type any
+ window = nil, ---@type table
+ content_window = nil, ---@type table|nil
+ fg_bg = core.cpair(colors.white, colors.black),
+ frame = core.gframe(1, 1, 1, 1),
+ children = {}
}
+ local name_brief = "graphics.element{" .. self.elem_type .. "}: "
+
-- element as string
function self.mt.__tostring()
return "graphics.element{" .. self.elem_type .. "} @ " .. tostring(self)
@@ -92,10 +101,8 @@ function element.new(args)
-------------------------
-- prepare the template
- ---@param offset_x integer x offset
- ---@param offset_y integer y offset
---@param next_y integer next line if no y was provided
- function protected.prepare_template(offset_x, offset_y, next_y)
+ function protected.prepare_template(next_y)
-- get frame coordinates/size
if args.gframe ~= nil then
protected.frame.x = args.gframe.x
@@ -105,46 +112,28 @@ function element.new(args)
else
local w, h = self.p_window.getSize()
protected.frame.x = args.x or 1
-
- if args.parent ~= nil then
- protected.frame.y = args.y or (next_y - offset_y)
- else
- protected.frame.y = args.y or next_y
- end
-
+ protected.frame.y = args.y or next_y
protected.frame.w = args.width or w
protected.frame.h = args.height or h
end
- -- inner offsets
- if args.offset_x ~= nil then self.child_offset.x = args.offset_x end
- if args.offset_y ~= nil then self.child_offset.y = args.offset_y end
-
-- adjust window frame if applicable
local f = protected.frame
- local x = f.x
- local y = f.y
-
- -- apply offsets
if args.parent ~= nil then
-- constrain to parent inner width/height
local w, h = self.p_window.getSize()
- f.w = math.min(f.w, w - ((2 * offset_x) + (f.x - 1)))
- f.h = math.min(f.h, h - ((2 * offset_y) + (f.y - 1)))
-
- -- offset x/y
- f.x = x + offset_x
- f.y = y + offset_y
+ f.w = math.min(f.w, w - (f.x - 1))
+ f.h = math.min(f.h, h - (f.y - 1))
end
-- check frame
- assert(f.x >= 1, "graphics.element{" .. self.elem_type .. "}: frame x not >= 1")
- assert(f.y >= 1, "graphics.element{" .. self.elem_type .. "}: frame y not >= 1")
- assert(f.w >= 1, "graphics.element{" .. self.elem_type .. "}: frame width not >= 1")
- assert(f.h >= 1, "graphics.element{" .. self.elem_type .. "}: frame height not >= 1")
+ assert(f.x >= 1, name_brief .. "frame x not >= 1")
+ assert(f.y >= 1, name_brief .. "frame y not >= 1")
+ assert(f.w >= 1, name_brief .. "frame width not >= 1")
+ assert(f.h >= 1, name_brief .. "frame height not >= 1")
-- create window
- protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, true)
+ protected.window = window.create(self.p_window, f.x, f.y, f.w, f.h, args.hidden ~= true)
-- init colors
if args.fg_bg ~= nil then
@@ -168,9 +157,38 @@ function element.new(args)
self.bounds.y2 = self.position.y + f.h - 1
end
+ -- check if a coordinate relative to the parent is within the bounds of this element
+ ---@param x integer
+ ---@param y integer
+ function protected.in_window_bounds(x, y)
+ local in_x = x >= self.bounds.x1 and x <= self.bounds.x2
+ local in_y = y >= self.bounds.y1 and y <= self.bounds.y2
+ return in_x and in_y
+ end
+
+ -- check if a coordinate relative to this window is within the bounds of this element
+ ---@param x integer
+ ---@param y integer
+ function protected.in_frame_bounds(x, y)
+ local in_x = x >= 1 and x <= protected.frame.w
+ local in_y = y >= 1 and y <= protected.frame.h
+ return in_x and in_y
+ end
+
-- luacheck: push ignore
---@diagnostic disable: unused-local, unused-vararg
+ -- handle a child element having been added
+ ---@param id element_id element identifier
+ ---@param child graphics_element child element
+ function protected.on_added(id, child)
+ end
+
+ -- handle a child element having been removed
+ ---@param id element_id element identifier
+ function protected.on_removed(id)
+ end
+
-- handle a mouse event
---@param event mouse_interaction mouse interaction event
function protected.handle_mouse(event)
@@ -241,6 +259,14 @@ function element.new(args)
---@return graphics_element element, element_id id
function protected.get() return public, self.id end
+ -- report completion of element instantiation and get the public interface
+ ---@nodiscard
+ ---@return graphics_element element, element_id id
+ function protected.complete()
+ if args.parent ~= nil then args.parent.__child_ready(self.id, public) end
+ return public, self.id
+ end
+
-----------
-- SETUP --
-----------
@@ -252,11 +278,12 @@ function element.new(args)
end
-- check window
- assert(self.p_window, "graphics.element{" .. self.elem_type .. "}: no parent window provided")
+ assert(self.p_window, name_brief .. "no parent window provided")
-- prepare the template
if args.parent == nil then
- protected.prepare_template(0, 0, 1)
+ self.id = args.id or "__ROOT__"
+ protected.prepare_template(1)
else
self.id = args.parent.__add_child(args.id, protected)
end
@@ -267,57 +294,108 @@ function element.new(args)
-- get the window object
---@nodiscard
- function public.window() return protected.window end
+ function public.window() return protected.content_window or protected.window end
- -- CHILD ELEMENTS --
+ -- delete this element (hide and unsubscribe from PSIL)
+ function public.delete()
+ local fg_bg = protected.fg_bg
+
+ if args.parent ~= nil then
+ -- grab parent fg/bg so we can clear cleanly as a child element
+ fg_bg = args.parent.get_fg_bg()
+ end
+
+ -- clear, hide, and stop animations
+ protected.window.setBackgroundColor(fg_bg.bkg)
+ protected.window.setTextColor(fg_bg.fgd)
+ protected.window.clear()
+ public.hide()
+
+ -- unsubscribe from PSIL
+ for i = 1, #self.subscriptions do
+ local s = self.subscriptions[i] ---@type element_subscription
+ s.ps.unsubscribe(s.key, s.func)
+ end
+
+ -- delete all children
+ for k, v in pairs(protected.children) do
+ v.delete()
+ protected.children[k] = nil
+ end
+
+ if args.parent ~= nil then
+ -- remove self from parent
+ args.parent.__remove_child(self.id)
+ end
+ end
+
+ -- ELEMENT TREE --
-- add a child element
---@nodiscard
---@param key string|nil id
- ---@param child graphics_template
+ ---@param child graphics_base
---@return integer|string key
function public.__add_child(key, child)
- -- offset first automatic placement
- if self.next_y <= self.child_offset.y then
- self.next_y = self.child_offset.y + 1
- end
-
- child.prepare_template(self.child_offset.x, self.child_offset.y, self.next_y)
+ child.prepare_template(self.next_y)
self.next_y = child.frame.y + child.frame.h
local child_element = child.get()
if key == nil then
- table.insert(self.children, child_element)
- return #self.children
+ table.insert(protected.children, child_element)
+ return #protected.children
else
- self.children[key] = child_element
+ protected.children[key] = child_element
return key
end
end
+ -- remove a child element
+ ---@param key element_id id
+ function public.__remove_child(key)
+ if protected.children[key] ~= nil then
+ protected.on_removed(key)
+ protected.children[key] = nil
+ end
+ end
+
+ -- actions to take upon a child element becoming ready (initial draw/construction completed)
+ ---@param key element_id id
+ ---@param child graphics_element
+ function public.__child_ready(key, child)
+ protected.on_added(key, child)
+ end
+
-- get a child element
---@nodiscard
+ ---@param id element_id
---@return graphics_element
- function public.get_child(key) return self.children[key] end
+ function public.get_child(id) return protected.children[id] end
- -- remove child
- ---@param key string|integer
- function public.remove(key) self.children[key] = nil end
+ -- remove a child element
+ ---@param id element_id
+ function public.remove(id)
+ if protected.children[id] ~= nil then
+ protected.children[id].delete()
+ protected.on_removed(id)
+ protected.children[id] = nil
+ end
+ end
-- attempt to get a child element by ID (does not include this element itself)
---@nodiscard
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
- if self.children[id] == nil then
- for _, child in pairs(self.children) do
+ if protected.children[id] == nil then
+ for _, child in pairs(protected.children) do
local elem = child.get_element_by_id(id)
if elem ~= nil then return elem end
end
else
- return self.children[id]
+ return protected.children[id]
end
return nil
@@ -356,14 +434,14 @@ function element.new(args)
-- get element width
---@nodiscard
---@return integer width
- function public.width()
+ function public.get_width()
return protected.frame.w
end
-- get element height
---@nodiscard
---@return integer height
- function public.height()
+ function public.get_height()
return protected.frame.h
end
@@ -416,22 +494,29 @@ function element.new(args)
protected.resize(...)
end
+ -- reposition the element window
+ -- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
+ ---@param x integer x position relative to parent frame
+ ---@param y integer y position relative to parent frame
+ function public.reposition(x, y)
+ protected.window.reposition(x, y)
+ end
+
-- FUNCTION CALLBACKS --
-- handle a monitor touch or mouse click
---@param event mouse_interaction mouse interaction event
function public.handle_mouse(event)
- local in_x = event.x >= self.bounds.x1 and event.x <= self.bounds.x2
- local in_y = event.y >= self.bounds.y1 and event.y <= self.bounds.y2
+ local x_ini, y_ini = event.initial.x, event.initial.y
- if in_x and in_y then
- local event_T = core.events.mouse_transposed(event, (event.x - self.position.x) + 1, (event.y - self.position.y) + 1)
+ local ini_in = protected.in_window_bounds(x_ini, y_ini)
- -- handle the touch event, transformed into the window frame
+ if ini_in then
+ local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
+
+ -- handle the mouse event then pass to children
protected.handle_mouse(event_T)
-
- -- pass on touch event to children
- for _, val in pairs(self.children) do val.handle_mouse(event_T) end
+ for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
end
end
@@ -447,27 +532,71 @@ function element.new(args)
protected.response_callback(result)
end
- -- VISIBILITY --
-
- -- show the element
- function public.show()
- protected.window.setVisible(true)
- protected.start_anim()
- for _, child in pairs(self.children) do child.show() end
+ -- register a callback with a PSIL, allowing for automatic unregister on delete
+ -- do not use graphics elements directly with PSIL subscribe()
+ ---@param ps psil PSIL to subscribe to
+ ---@param key string key to subscribe to
+ ---@param func function function to link
+ function public.register(ps, key, func)
+ table.insert(self.subscriptions, { ps = ps, key = key, func = func })
+ ps.subscribe(key, func)
end
- -- hide the element
+ -- VISIBILITY & ANIMATIONS --
+
+ -- show the element and enables animations by default
+ ---@param animate? boolean true (default) to automatically resume animations
+ function public.show(animate)
+ protected.window.setVisible(true)
+ if animate ~= false then public.animate_all() end
+ end
+
+ -- hide the element and disables animations
+ -- this alone does not cause an element to be fully hidden, it only prevents updates from being shown
+ ---@see graphics_element.content_redraw
function public.hide()
- protected.stop_anim()
- for _, child in pairs(self.children) do child.hide() end
+ public.freeze_all() -- stop animations for efficiency/performance
protected.window.setVisible(false)
end
+ -- start/resume animation(s)
+ function public.animate()
+ protected.start_anim()
+ end
+
+ -- start/resume animation(s) for this element and all its children
+ -- only animates if a window is visible
+ function public.animate_all()
+ if protected.window.isVisible() then
+ public.animate()
+ for _, child in pairs(protected.children) do child.animate_all() end
+ end
+ end
+
+ -- freeze animation(s)
+ function public.freeze()
+ protected.stop_anim()
+ end
+
+ -- freeze animation(s) for this element and all its children
+ function public.freeze_all()
+ public.freeze()
+ for _, child in pairs(protected.children) do child.freeze_all() end
+ end
+
-- re-draw the element
function public.redraw()
protected.window.redraw()
end
+ -- if a content window is set, clears it then re-draws all children
+ function public.content_redraw()
+ if protected.content_window ~= nil then
+ protected.content_window.clear()
+ for _, child in pairs(protected.children) do child.redraw() end
+ end
+ end
+
return protected
end
diff --git a/graphics/elements/animations/waiting.lua b/graphics/elements/animations/waiting.lua
index a0d7b3e..286c82e 100644
--- a/graphics/elements/animations/waiting.lua
+++ b/graphics/elements/animations/waiting.lua
@@ -1,6 +1,6 @@
-- Loading/Waiting Animation Graphics Element
-local tcd = require("scada-common.tcallbackdsp")
+local tcd = require("scada-common.tcd")
local element = require("graphics.element")
@@ -10,6 +10,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new waiting animation element
---@param args waiting_args
@@ -102,7 +103,7 @@ local function waiting(args)
e.start_anim()
- return e.get()
+ return e.complete()
end
return waiting
diff --git a/graphics/elements/colormap.lua b/graphics/elements/colormap.lua
index 4c7ba94..be92d83 100644
--- a/graphics/elements/colormap.lua
+++ b/graphics/elements/colormap.lua
@@ -9,6 +9,7 @@ local element = require("graphics.element")
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
+---@field hidden? boolean true to hide on initial draw
-- new color map
---@param args colormap_args
@@ -27,7 +28,7 @@ local function colormap(args)
e.window.setCursorPos(1, 1)
e.window.blit(spaces, bkg, bkg)
- return e.get()
+ return e.complete()
end
return colormap
diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua
index 2b9baa6..4745f4f 100644
--- a/graphics/elements/controls/hazard_button.lua
+++ b/graphics/elements/controls/hazard_button.lua
@@ -1,6 +1,6 @@
-- Hazard-bordered Button Graphics Element
-local tcd = require("scada-common.tcallbackdsp")
+local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
local core = require("graphics.core")
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new hazard button
---@param args hazard_button_args
@@ -141,23 +142,26 @@ local function hazard_button(args)
end
-- handle mouse interaction
- function e.handle_mouse(_)
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
if e.enabled then
- -- change text color to indicate clicked
- e.window.setTextColor(args.accent)
- e.window.setCursorPos(3, 2)
- e.window.write(args.text)
+ if core.events.was_clicked(event.type) then
+ -- change text color to indicate clicked
+ e.window.setTextColor(args.accent)
+ e.window.setCursorPos(3, 2)
+ e.window.write(args.text)
- -- abort any other callbacks
- tcd.abort(on_timeout)
- tcd.abort(on_success)
- tcd.abort(on_failure)
+ -- abort any other callbacks
+ tcd.abort(on_timeout)
+ tcd.abort(on_success)
+ tcd.abort(on_failure)
- -- 1.5 second timeout
- tcd.dispatch(1.5, on_timeout)
+ -- 1.5 second timeout
+ tcd.dispatch(1.5, on_timeout)
- -- call the touch callback
- args.callback()
+ -- call the touch callback
+ args.callback()
+ end
end
end
@@ -165,18 +169,13 @@ local function hazard_button(args)
---@param result boolean true for success, false for failure
function e.response_callback(result)
tcd.abort(on_timeout)
-
- if result then
- on_success()
- else
- on_failure(0)
- end
+ if result then on_success() else on_failure(0) end
end
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
end
-- show the button as disabled
@@ -200,7 +199,7 @@ local function hazard_button(args)
-- initial draw of border
draw_border(args.accent)
- return e.get()
+ return e.complete()
end
return hazard_button
diff --git a/graphics/elements/controls/multi_button.lua b/graphics/elements/controls/multi_button.lua
index 2549e2b..279c9a7 100644
--- a/graphics/elements/controls/multi_button.lua
+++ b/graphics/elements/controls/multi_button.lua
@@ -2,13 +2,13 @@
local util = require("scada-common.util")
+local core = require("graphics.core")
local element = require("graphics.element")
---@class button_option
---@field text string
---@field fg_bg cpair
---@field active_fg_bg cpair
----@field _lpad integer automatically calculated left pad
---@field _start_x integer starting touch x range (inclusive)
---@field _end_x integer ending touch x range (inclusive)
@@ -23,6 +23,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new multi button (latch selection, exclusively one button at a time)
---@param args multi_button_args
@@ -62,9 +63,7 @@ local function multi_button(args)
local next_x = 2
for i = 1, #args.options do
local opt = args.options[i] ---@type button_option
- local w = string.len(opt.text)
- opt._lpad = math.floor((e.frame.w - w) / 2)
opt._start_x = next_x
opt._end_x = next_x + button_width - 1
@@ -92,20 +91,32 @@ local function multi_button(args)
end
end
+ -- check which button a given x is within
+ ---@return integer|nil button index or nil if not within a button
+ local function which_button(x)
+ for i = 1, #args.options do
+ local opt = args.options[i] ---@type button_option
+ if x >= opt._start_x and x <= opt._end_x then return i end
+ end
+
+ return nil
+ end
+
-- handle mouse interaction
---@param event mouse_interaction mouse event
----@diagnostic disable-next-line: unused-local
function e.handle_mouse(event)
- -- determine what was pressed
- if e.enabled and event.y == 1 then
- for i = 1, #args.options do
- local opt = args.options[i] ---@type button_option
+ -- if enabled and the button row was pressed...
+ if e.enabled and core.events.was_clicked(event.type) then
+ -- a button may have been pressed, which one was it?
+ local button_ini = which_button(event.initial.x)
+ local button_cur = which_button(event.current.x)
- if event.x >= opt._start_x and event.x <= opt._end_x then
- e.value = i
- draw()
- args.callback(e.value)
- end
+ -- mouse up must always have started with a mouse down on the same button to count as a click
+ -- tap always has identical coordinates, so this always passes for taps
+ if button_ini == button_cur and button_cur ~= nil then
+ e.value = button_cur
+ draw()
+ args.callback(e.value)
end
end
end
@@ -120,7 +131,7 @@ local function multi_button(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return multi_button
diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua
index 9b5f971..e6f4920 100644
--- a/graphics/elements/controls/push_button.lua
+++ b/graphics/elements/controls/push_button.lua
@@ -1,10 +1,12 @@
-- Button Graphics Element
-local tcd = require("scada-common.tcallbackdsp")
+local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
+local CLICK_TYPE = core.events.CLICK_TYPE
+
---@class push_button_args
---@field text string button text
---@field callback function function to call on touch
@@ -17,6 +19,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new push button
---@param args push_button_args
@@ -24,6 +27,8 @@ local element = require("graphics.element")
local function push_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
+ assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
+ "graphics.elements.controls.push_button: min_width must be nil or a number > 0")
local text_width = string.len(args.text)
@@ -47,36 +52,50 @@ local function push_button(args)
e.window.write(args.text)
end
+ -- draw the button as pressed (if active_fg_bg set)
+ local function show_pressed()
+ if e.enabled and args.active_fg_bg ~= nil then
+ e.value = true
+ e.window.setTextColor(args.active_fg_bg.fgd)
+ e.window.setBackgroundColor(args.active_fg_bg.bkg)
+ draw()
+ end
+ end
+
+ -- draw the button as unpressed (if active_fg_bg set)
+ local function show_unpressed()
+ if e.enabled and args.active_fg_bg ~= nil then
+ e.value = false
+ e.window.setTextColor(e.fg_bg.fgd)
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ draw()
+ end
+ end
+
-- handle mouse interaction
- function e.handle_mouse(_)
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
if e.enabled then
- if args.active_fg_bg ~= nil then
- -- show as pressed
- e.value = true
- e.window.setTextColor(args.active_fg_bg.fgd)
- e.window.setBackgroundColor(args.active_fg_bg.bkg)
- draw()
-
+ if event.type == CLICK_TYPE.TAP then
+ show_pressed()
-- show as unpressed in 0.25 seconds
- tcd.dispatch(0.25, function ()
- e.value = false
- if e.enabled then
- e.window.setTextColor(e.fg_bg.fgd)
- e.window.setBackgroundColor(e.fg_bg.bkg)
- end
- draw()
- end)
+ if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
+ args.callback()
+ elseif event.type == CLICK_TYPE.DOWN then
+ show_pressed()
+ elseif event.type == CLICK_TYPE.UP then
+ show_unpressed()
+ if e.in_frame_bounds(event.current.x, event.current.y) then
+ args.callback()
+ end
end
-
- -- call the touch callback
- args.callback()
end
end
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_mouse(core.events.mouse_generic("", core.events.click_type.VIRTUAL, 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
end
-- show butten as enabled
@@ -102,7 +121,7 @@ local function push_button(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return push_button
diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua
index 3b2a593..e3edf24 100644
--- a/graphics/elements/controls/radio_button.lua
+++ b/graphics/elements/controls/radio_button.lua
@@ -1,5 +1,6 @@
-- Radio Button Graphics Element
+local core = require("graphics.core")
local element = require("graphics.element")
---@class radio_button_args
@@ -14,6 +15,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new radio button list (latch selection, exclusively one button at a time)
---@param args radio_button_args
@@ -82,10 +84,10 @@ local function radio_button(args)
-- handle mouse interaction
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
- -- determine what was pressed
- if e.enabled then
- if args.options[event.y] ~= nil then
- e.value = event.y
+ if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
+ -- determine what was pressed
+ if args.options[event.current.y] ~= nil then
+ e.value = event.current.y
draw()
args.callback(e.value)
end
@@ -102,7 +104,7 @@ local function radio_button(args)
-- initial draw
draw()
- return e.get()
+ return e.complete()
end
return radio_button
diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua
index 885761d..d43317a 100644
--- a/graphics/elements/controls/sidebar.lua
+++ b/graphics/elements/controls/sidebar.lua
@@ -1,9 +1,12 @@
-- Sidebar Graphics Element
-local tcd = require("scada-common.tcallbackdsp")
+local tcd = require("scada-common.tcd")
+local core = require("graphics.core")
local element = require("graphics.element")
+local CLICK_TYPE = core.events.CLICK_TYPE
+
---@class sidebar_tab
---@field char string character identifier
---@field color cpair tab colors (fg/bg)
@@ -17,6 +20,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new sidebar tab selector
---@param args sidebar_args
@@ -39,7 +43,10 @@ local function sidebar(args)
-- show the button state
---@param pressed boolean if the currently selected tab should appear as actively pressed
- local function draw(pressed)
+ ---@param pressed_idx? integer optional index to show as held (that is not yet selected)
+ local function draw(pressed, pressed_idx)
+ pressed_idx = pressed_idx or e.value
+
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type sidebar_tab
@@ -47,7 +54,7 @@ local function sidebar(args)
e.window.setCursorPos(1, y)
- if pressed and e.value == i then
+ if pressed and i == pressed_idx then
e.window.setTextColor(e.fg_bg.fgd)
e.window.setBackgroundColor(e.fg_bg.bkg)
else
@@ -74,16 +81,27 @@ local function sidebar(args)
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
- local idx = math.ceil(event.y / 3)
+ local cur_idx = math.ceil(event.current.y / 3)
+ local ini_idx = math.ceil(event.initial.y / 3)
- if args.tabs[idx] ~= nil then
- e.value = idx
- draw(true)
-
- -- show as unpressed in 0.25 seconds
- tcd.dispatch(0.25, function () draw(false) end)
-
- args.callback(e.value)
+ if args.tabs[cur_idx] ~= nil then
+ if event.type == CLICK_TYPE.TAP then
+ e.value = cur_idx
+ draw(true)
+ -- show as unpressed in 0.25 seconds
+ tcd.dispatch(0.25, function () draw(false) end)
+ args.callback(e.value)
+ elseif event.type == CLICK_TYPE.DOWN then
+ draw(true, cur_idx)
+ elseif event.type == CLICK_TYPE.UP then
+ if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
+ e.value = cur_idx
+ draw(false)
+ args.callback(e.value)
+ else draw(false) end
+ end
+ elseif event.type == CLICK_TYPE.UP then
+ draw(false)
end
end
end
@@ -98,7 +116,7 @@ local function sidebar(args)
-- initial draw
draw(false)
- return e.get()
+ return e.complete()
end
return sidebar
diff --git a/graphics/elements/controls/spinbox_numeric.lua b/graphics/elements/controls/spinbox_numeric.lua
index 15e0e76..767d97b 100644
--- a/graphics/elements/controls/spinbox_numeric.lua
+++ b/graphics/elements/controls/spinbox_numeric.lua
@@ -2,6 +2,7 @@
local util = require("scada-common.util")
+local core = require("graphics.core")
local element = require("graphics.element")
---@class spinbox_args
@@ -17,6 +18,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new spinbox control (minimum value is 0)
---@param args spinbox_args
@@ -130,19 +132,22 @@ local function spinbox(args)
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
-- only handle if on an increment or decrement arrow
- if e.enabled and event.x ~= dec_point_x then
- local idx = util.trinary(event.x > dec_point_x, event.x - 1, event.x)
- if digits[idx] ~= nil then
- if event.y == 1 then
- -- increment
- digits[idx] = digits[idx] + 1
- elseif event.y == 3 then
- -- decrement
- digits[idx] = digits[idx] - 1
- end
+ if e.enabled and core.events.was_clicked(event.type) and
+ (event.current.x ~= dec_point_x) and (event.current.y ~= 2) then
+ if event.current.x == event.initial.x and event.current.y == event.initial.y then
+ local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
+ if digits[idx] ~= nil then
+ if event.current.y == 1 then
+ -- increment
+ digits[idx] = digits[idx] + 1
+ elseif event.current.y == 3 then
+ -- decrement
+ digits[idx] = digits[idx] - 1
+ end
- update_value()
- show_num()
+ update_value()
+ show_num()
+ end
end
end
end
@@ -184,7 +189,7 @@ local function spinbox(args)
e.value = 0
set_digits()
- return e.get()
+ return e.complete()
end
return spinbox
diff --git a/graphics/elements/controls/switch_button.lua b/graphics/elements/controls/switch_button.lua
index 012872c..6d2e09c 100644
--- a/graphics/elements/controls/switch_button.lua
+++ b/graphics/elements/controls/switch_button.lua
@@ -1,5 +1,6 @@
-- Button Graphics Element
+local core = require("graphics.core")
local element = require("graphics.element")
---@class switch_button_args
@@ -14,6 +15,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer parent height if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new switch button (latch high/low)
---@param args switch_button_args
@@ -22,13 +24,15 @@ local function switch_button(args)
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
+ assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
+ "graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
- -- single line
- args.height = 1
-
- -- determine widths
local text_width = string.len(args.text)
- args.width = math.max(text_width + 2, args.min_width)
+
+ -- single line height, calculate width
+ args.height = 1
+ args.min_width = args.min_width or 0
+ args.width = math.max(text_width, args.min_width)
-- create new graphics element base object
local e = element.new(args)
@@ -63,8 +67,9 @@ local function switch_button(args)
draw_state()
-- handle mouse interaction
- function e.handle_mouse(_)
- if e.enabled then
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
+ if e.enabled and core.events.was_clicked(event.type) then
-- toggle state
e.value = not e.value
draw_state()
@@ -82,7 +87,7 @@ local function switch_button(args)
draw_state()
end
- return e.get()
+ return e.complete()
end
return switch_button
diff --git a/graphics/elements/controls/tabbar.lua b/graphics/elements/controls/tabbar.lua
new file mode 100644
index 0000000..da4738b
--- /dev/null
+++ b/graphics/elements/controls/tabbar.lua
@@ -0,0 +1,131 @@
+-- Tab Bar Graphics Element
+
+local util = require("scada-common.util")
+
+local core = require("graphics.core")
+local element = require("graphics.element")
+
+---@class tabbar_tab
+---@field name string tab name
+---@field color cpair tab colors (fg/bg)
+---@field _start_x integer starting touch x range (inclusive)
+---@field _end_x integer ending touch x range (inclusive)
+
+---@class tabbar_args
+---@field tabs table tab options
+---@field callback function function to call on tab change
+---@field min_width? integer text length + 2 if omitted
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer 1 if omitted
+---@field width? integer parent width if omitted
+---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
+
+-- new tab selector
+---@param args tabbar_args
+---@return graphics_element element, element_id id
+local function tabbar(args)
+ assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field")
+ assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required")
+ assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field")
+ assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
+ "graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
+
+ -- always 1 tall
+ args.height = 1
+
+ -- determine widths
+ local max_width = 1
+ for i = 1, #args.tabs do
+ local opt = args.tabs[i] ---@type tabbar_tab
+ if string.len(opt.name) > max_width then
+ max_width = string.len(opt.name)
+ end
+ end
+
+ local button_width = math.max(max_width, args.min_width or 0)
+
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs")
+
+ -- default to 1st tab
+ e.value = 1
+
+ -- calculate required tab dimension information
+ local next_x = 1
+ for i = 1, #args.tabs do
+ local tab = args.tabs[i] ---@type tabbar_tab
+
+ tab._start_x = next_x
+ tab._end_x = next_x + button_width - 1
+
+ next_x = next_x + button_width
+ end
+
+ -- show the tab state
+ local function draw()
+ for i = 1, #args.tabs do
+ local tab = args.tabs[i] ---@type tabbar_tab
+
+ e.window.setCursorPos(tab._start_x, 1)
+
+ if e.value == i then
+ e.window.setTextColor(tab.color.fgd)
+ e.window.setBackgroundColor(tab.color.bkg)
+ else
+ e.window.setTextColor(e.fg_bg.fgd)
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+
+ e.window.write(util.pad(tab.name, button_width))
+ end
+ end
+
+ -- check which tab a given x is within
+ ---@return integer|nil button index or nil if not within a tab
+ local function which_tab(x)
+ for i = 1, #args.tabs do
+ local tab = args.tabs[i] ---@type tabbar_tab
+ if x >= tab._start_x and x <= tab._end_x then return i end
+ end
+
+ return nil
+ end
+
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
+ -- determine what was pressed
+ if e.enabled and core.events.was_clicked(event.type) then
+ -- a button may have been pressed, which one was it?
+ local tab_ini = which_tab(event.initial.x)
+ local tab_cur = which_tab(event.current.x)
+
+ -- mouse up must always have started with a mouse down on the same tab to count as a click
+ -- tap always has identical coordinates, so this always passes for taps
+ if tab_ini == tab_cur and tab_cur ~= nil then
+ e.value = tab_cur
+ draw()
+ args.callback(e.value)
+ end
+ end
+ end
+
+ -- set the value
+ ---@param val integer new value
+ function e.set_value(val)
+ e.value = val
+ draw()
+ end
+
+ -- initial draw
+ draw()
+
+ return e.complete()
+end
+
+return tabbar
diff --git a/graphics/elements/displaybox.lua b/graphics/elements/displaybox.lua
index c7e5c9f..3578a63 100644
--- a/graphics/elements/displaybox.lua
+++ b/graphics/elements/displaybox.lua
@@ -4,19 +4,22 @@ local element = require("graphics.element")
---@class displaybox_args
---@field window table
+---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field width? integer parent width if omitted
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new root display box
---@nodiscard
---@param args displaybox_args
+---@return graphics_element element, element_id id
local function displaybox(args)
-- create new graphics element base object
- return element.new(args).get()
+ return element.new(args).complete()
end
return displaybox
diff --git a/graphics/elements/div.lua b/graphics/elements/div.lua
index 5eeef71..4b6bd6a 100644
--- a/graphics/elements/div.lua
+++ b/graphics/elements/div.lua
@@ -11,6 +11,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new div element
---@nodiscard
@@ -18,7 +19,7 @@ local element = require("graphics.element")
---@return graphics_element element, element_id id
local function div(args)
-- create new graphics element base object
- return element.new(args).get()
+ return element.new(args).complete()
end
return div
diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua
index 8bb8fa6..ff9b1ad 100644
--- a/graphics/elements/indicators/alight.lua
+++ b/graphics/elements/indicators/alight.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new alarm indicator light
---@nodiscard
@@ -108,7 +109,7 @@ local function alarm_indicator_light(args)
e.on_update(1)
e.window.write(args.label)
- return e.get()
+ return e.complete()
end
return alarm_indicator_light
diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/coremap.lua
index 323e17c..127a8a3 100644
--- a/graphics/elements/indicators/coremap.lua
+++ b/graphics/elements/indicators/coremap.lua
@@ -26,7 +26,7 @@ local function core_map(args)
args.height = 18
-- inherit only foreground color
- args.fg_bg = core.graphics.cpair(args.parent.get_fg_bg().fgd, colors.gray)
+ args.fg_bg = core.cpair(args.parent.get_fg_bg().fgd, colors.gray)
-- create new graphics element base object
local e = element.new(args)
@@ -163,7 +163,7 @@ local function core_map(args)
-- initial draw
e.on_update(0)
- return e.get()
+ return e.complete()
end
return core_map
diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/data.lua
index 66d45dc..9282a03 100644
--- a/graphics/elements/indicators/data.lua
+++ b/graphics/elements/indicators/data.lua
@@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new data indicator
---@nodiscard
@@ -43,8 +44,9 @@ local function data(args)
e.window.setCursorPos(1, 1)
e.window.write(args.label)
- local label_len = string.len(args.label)
- local data_start = 1
+ local value_color = e.fg_bg.fgd
+ local label_len = string.len(args.label)
+ local data_start = 1
local clear_width = args.width
if label_len > 0 then
@@ -64,7 +66,7 @@ local function data(args)
-- write data
local data_str = util.sprintf(args.format, value)
e.window.setCursorPos(data_start, 1)
- e.window.setTextColor(e.fg_bg.fgd)
+ e.window.setTextColor(value_color)
if args.commas then
e.window.write(util.comma_format(data_str))
else
@@ -84,10 +86,17 @@ local function data(args)
---@param val any new value
function e.set_value(val) e.on_update(val) end
+ -- change the foreground color of the value, or all text if no label/unit colors provided
+ ---@param c color
+ function e.recolor(c)
+ value_color = c
+ e.on_update(e.value)
+ end
+
-- initial value draw
e.on_update(args.value)
- return e.get()
+ return e.complete()
end
return data
diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/hbar.lua
index 2d9b110..9bee59f 100644
--- a/graphics/elements/indicators/hbar.lua
+++ b/graphics/elements/indicators/hbar.lua
@@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new horizontal bar
---@nodiscard
@@ -119,7 +120,7 @@ local function hbar(args)
-- initialize to 0
e.on_update(0)
- return e.get()
+ return e.complete()
end
return hbar
diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua
index f31479d..03c88fb 100644
--- a/graphics/elements/indicators/icon.lua
+++ b/graphics/elements/indicators/icon.lua
@@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new icon indicator
---@nodiscard
@@ -68,7 +69,7 @@ local function icon(args)
-- initial icon draw
e.on_update(args.value or 1)
- return e.get()
+ return e.complete()
end
return icon
diff --git a/graphics/elements/indicators/led.lua b/graphics/elements/indicators/led.lua
index dd2264e..077cab3 100644
--- a/graphics/elements/indicators/led.lua
+++ b/graphics/elements/indicators/led.lua
@@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new indicator LED
---@nodiscard
@@ -94,7 +95,7 @@ local function indicator_led(args)
e.window.write(args.label)
end
- return e.get()
+ return e.complete()
end
return indicator_led
diff --git a/graphics/elements/indicators/ledpair.lua b/graphics/elements/indicators/ledpair.lua
index 727d4cd..47c9a0a 100644
--- a/graphics/elements/indicators/ledpair.lua
+++ b/graphics/elements/indicators/ledpair.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new dual LED indicator light
---@nodiscard
@@ -108,7 +109,7 @@ local function indicator_led_pair(args)
e.window.write(args.label)
end
- return e.get()
+ return e.complete()
end
return indicator_led_pair
diff --git a/graphics/elements/indicators/ledrgb.lua b/graphics/elements/indicators/ledrgb.lua
index 266e0ab..dbcb947 100644
--- a/graphics/elements/indicators/ledrgb.lua
+++ b/graphics/elements/indicators/ledrgb.lua
@@ -11,6 +11,7 @@ local element = require("graphics.element")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new RGB LED indicator light
---@nodiscard
@@ -53,7 +54,7 @@ local function indicator_led_rgb(args)
e.window.write(args.label)
end
- return e.get()
+ return e.complete()
end
return indicator_led_rgb
diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua
index e764ad9..d4e8b09 100644
--- a/graphics/elements/indicators/light.lua
+++ b/graphics/elements/indicators/light.lua
@@ -16,6 +16,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new indicator light
---@nodiscard
@@ -92,7 +93,7 @@ local function indicator_light(args)
e.window.setCursorPos(3, 1)
e.window.write(args.label)
- return e.get()
+ return e.complete()
end
return indicator_light
diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua
index 1d727ae..323fe58 100644
--- a/graphics/elements/indicators/power.lua
+++ b/graphics/elements/indicators/power.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new power indicator
---@nodiscard
@@ -79,7 +80,7 @@ local function power(args)
-- initial value draw
e.on_update(args.value)
- return e.get()
+ return e.complete()
end
return power
diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua
index 2e4ad56..fc89044 100644
--- a/graphics/elements/indicators/rad.lua
+++ b/graphics/elements/indicators/rad.lua
@@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field width integer length
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new radiation indicator
---@nodiscard
@@ -84,7 +85,7 @@ local function rad(args)
-- initial value draw
e.on_update(types.new_zero_radiation_reading())
- return e.get()
+ return e.complete()
end
return rad
diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/state.lua
index 10d081b..d0e57b5 100644
--- a/graphics/elements/indicators/state.lua
+++ b/graphics/elements/indicators/state.lua
@@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
---@field height? integer 1 if omitted, must be an odd number
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new state indicator
---@nodiscard
@@ -74,7 +75,7 @@ local function state_indicator(args)
-- initial draw
e.on_update(args.value or 1)
- return e.get()
+ return e.complete()
end
return state_indicator
diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua
index 543ebf5..ef8a8b6 100644
--- a/graphics/elements/indicators/trilight.lua
+++ b/graphics/elements/indicators/trilight.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new tri-state indicator light
---@nodiscard
@@ -105,7 +106,7 @@ local function tristate_indicator_light(args)
e.on_update(1)
e.window.write(args.label)
- return e.get()
+ return e.complete()
end
return tristate_indicator_light
diff --git a/graphics/elements/indicators/vbar.lua b/graphics/elements/indicators/vbar.lua
index fe7f9bc..4cfb6e7 100644
--- a/graphics/elements/indicators/vbar.lua
+++ b/graphics/elements/indicators/vbar.lua
@@ -13,6 +13,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new vertical bar
---@nodiscard
@@ -99,7 +100,7 @@ local function vbar(args)
---@param val number 0.0 to 1.0
function e.set_value(val) e.on_update(val) end
- return e.get()
+ return e.complete()
end
return vbar
diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua
new file mode 100644
index 0000000..e37a772
--- /dev/null
+++ b/graphics/elements/listbox.lua
@@ -0,0 +1,283 @@
+-- Scroll-able List Box Display Graphics Element
+
+local tcd = require("scada-common.tcd")
+
+local core = require("graphics.core")
+local element = require("graphics.element")
+
+local CLICK_TYPE = core.events.CLICK_TYPE
+
+---@class listbox_args
+---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
+---@field item_pad? integer spacing (lines) between items in the list (default 0)
+---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
+---@field nav_active? cpair active colors for bar held down or arrow held down
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer 1 if omitted
+---@field width? integer parent width if omitted
+---@field height? integer parent height if omitted
+---@field gframe? graphics_frame frame instead of x/y/width/height
+---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
+
+---@class listbox_item
+---@field id string|integer element ID
+---@field e graphics_element element
+---@field y integer y position
+---@field h integer element height
+
+-- new listbox element
+---@nodiscard
+---@param args listbox_args
+---@return graphics_element element, element_id id
+local function listbox(args)
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ -- create content window for child elements
+ local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
+ e.content_window = scroll_frame
+
+ -- item list and scroll management
+ local list = {}
+ local item_pad = args.item_pad or 0
+ local scroll_offset = 0
+ local content_height = 0
+ local max_down_scroll = 0
+ -- bar control/tracking variables
+ local max_bar_height = e.frame.h - 2
+ local bar_height = 0 -- full height of bar
+ local bar_bounds = { 0, 0 } -- top and bottom of bar
+ local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
+ local holding_bar = false -- bar is being held by mouse
+ local bar_grip_pos = 0 -- where the bar was gripped by mouse down
+ local mouse_last_y = 0 -- last reported y coordinate of drag
+
+ -- draw scroll bar arrows, optionally showing one of them as pressed
+ ---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
+ local function draw_arrows(pressed_arrow)
+ local nav_fg_bg = args.nav_fg_bg or e.fg_bg
+ local active_fg_bg = args.nav_active or nav_fg_bg
+
+ -- draw up/down arrows
+ if pressed_arrow == 1 then
+ e.window.setTextColor(active_fg_bg.fgd)
+ e.window.setBackgroundColor(active_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, 1)
+ e.window.write("\x1e")
+ e.window.setTextColor(nav_fg_bg.fgd)
+ e.window.setBackgroundColor(nav_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, e.frame.h)
+ e.window.write("\x1f")
+ elseif pressed_arrow == -1 then
+ e.window.setTextColor(nav_fg_bg.fgd)
+ e.window.setBackgroundColor(nav_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, 1)
+ e.window.write("\x1e")
+ e.window.setTextColor(active_fg_bg.fgd)
+ e.window.setBackgroundColor(active_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, e.frame.h)
+ e.window.write("\x1f")
+ else
+ e.window.setTextColor(nav_fg_bg.fgd)
+ e.window.setBackgroundColor(nav_fg_bg.bkg)
+ e.window.setCursorPos(e.frame.w, 1)
+ e.window.write("\x1e")
+ e.window.setCursorPos(e.frame.w, e.frame.h)
+ e.window.write("\x1f")
+ end
+
+ e.window.setTextColor(e.fg_bg.fgd)
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+
+ -- render the scroll bar and re-cacluate height & bounds
+ local function draw_bar()
+ local offset = 2 + math.abs(scroll_offset)
+
+ bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
+
+ if bar_height < 1 then
+ bar_is_scaled = true
+ -- can't do a 1:1 ratio
+ -- use minimum size bar with scaled offset
+ local scroll_progress = scroll_offset / max_down_scroll
+ offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
+ bar_height = 1
+ else
+ bar_is_scaled = false
+ end
+
+ bar_bounds = { offset, (bar_height + offset) - 1 }
+
+ for i = 2, e.frame.h - 1 do
+ if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
+ if args.nav_fg_bg ~= nil then
+ e.window.setBackgroundColor(args.nav_fg_bg.fgd)
+ else
+ e.window.setBackgroundColor(e.fg_bg.fgd)
+ end
+ else
+ if args.nav_fg_bg ~= nil then
+ e.window.setBackgroundColor(args.nav_fg_bg.bkg)
+ else
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+ end
+
+ e.window.setCursorPos(e.frame.w, i)
+ e.window.write(" ")
+ end
+
+ e.window.setBackgroundColor(e.fg_bg.bkg)
+ end
+
+ -- update item y positions and move elements
+ local function update_positions()
+ local next_y = 1
+
+ scroll_frame.setVisible(false)
+ scroll_frame.setBackgroundColor(e.fg_bg.bkg)
+ scroll_frame.setTextColor(e.fg_bg.fgd)
+ scroll_frame.clear()
+
+ for i = 1, #list do
+ local item = list[i] ---@type listbox_item
+ item.y = next_y
+ next_y = next_y + item.h + item_pad
+ item.e.reposition(1, item.y)
+ item.e.show()
+ end
+
+ content_height = next_y
+ max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
+ if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
+
+ scroll_frame.reposition(1, 1 + scroll_offset)
+ scroll_frame.setVisible(true)
+
+ draw_bar()
+ end
+
+ -- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
+ ---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
+ local function scaled_bar_scroll(direction)
+ local scroll_progress = scroll_offset / max_down_scroll
+ local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
+
+ -- check what moving the scroll bar up or down would mean for the scroll progress
+ scroll_progress = (bar_position + direction) / (max_bar_height - 1)
+
+ return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
+ end
+
+ -- scroll down the list
+ local function scroll_down(scaled)
+ if scroll_offset > max_down_scroll then
+ if scaled then
+ scroll_offset = scaled_bar_scroll(1)
+ else
+ scroll_offset = scroll_offset - 1
+ end
+
+ update_positions()
+ end
+ end
+
+ -- scroll up the list
+ local function scroll_up(scaled)
+ if scroll_offset < 0 then
+ if scaled then
+ scroll_offset = scaled_bar_scroll(-1)
+ else
+ scroll_offset = scroll_offset + 1
+ end
+
+ update_positions()
+ end
+ end
+
+ -- handle a child element having been added to the list
+ ---@param id element_id element identifier
+ ---@param child graphics_element child element
+ function e.on_added(id, child)
+ table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
+ update_positions()
+ end
+
+ -- handle a child element having been removed from the list
+ ---@param id element_id element identifier
+ function e.on_removed(id)
+ for idx, elem in ipairs(list) do
+ if elem.id == id then
+ table.remove(list, idx)
+ update_positions()
+ return
+ end
+ end
+ end
+
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
+ if e.enabled then
+ if event.type == CLICK_TYPE.TAP then
+ if event.current.x == e.frame.w then
+ if event.current.y == 1 or event.current.y < bar_bounds[1] then
+ draw_arrows(1)
+ scroll_up()
+ if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
+ elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
+ draw_arrows(-1)
+ scroll_down()
+ if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
+ end
+ end
+ elseif event.type == CLICK_TYPE.DOWN then
+ if event.current.x == e.frame.w then
+ if event.current.y == 1 or event.current.y < bar_bounds[1] then
+ draw_arrows(1)
+ scroll_up()
+ elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
+ draw_arrows(-1)
+ scroll_down()
+ else
+ -- clicked on bar
+ holding_bar = true
+ bar_grip_pos = event.current.y - bar_bounds[1]
+ mouse_last_y = event.current.y
+ end
+ end
+ elseif event.type == CLICK_TYPE.UP then
+ holding_bar = false
+ draw_arrows(0)
+ elseif event.type == CLICK_TYPE.DRAG then
+ if holding_bar then
+ -- if mouse is within vertical frame, including the grip point
+ if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
+ if event.current.y < mouse_last_y then
+ scroll_up(bar_is_scaled)
+ elseif event.current.y > mouse_last_y then
+ scroll_down(bar_is_scaled)
+ end
+
+ mouse_last_y = event.current.y
+ end
+ end
+ elseif event.type == CLICK_TYPE.SCROLL_DOWN then
+ scroll_down()
+ elseif event.type == CLICK_TYPE.SCROLL_UP then
+ scroll_up()
+ end
+ end
+ end
+
+ draw_arrows(0)
+ draw_bar()
+
+ return e.complete()
+end
+
+return listbox
diff --git a/graphics/elements/multipane.lua b/graphics/elements/multipane.lua
index 8e25bab..790b595 100644
--- a/graphics/elements/multipane.lua
+++ b/graphics/elements/multipane.lua
@@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new multipane element
---@nodiscard
@@ -36,7 +37,7 @@ local function multipane(args)
e.set_value(1)
- return e.get()
+ return e.complete()
end
return multipane
diff --git a/graphics/elements/pipenet.lua b/graphics/elements/pipenet.lua
index 71ee9fd..5ca4745 100644
--- a/graphics/elements/pipenet.lua
+++ b/graphics/elements/pipenet.lua
@@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field id? string element id
---@field x? integer 1 if omitted
---@field y? integer 1 if omitted
+---@field hidden? boolean true to hide on initial draw
-- new pipe network
---@param args pipenet_args
@@ -37,7 +38,7 @@ local function pipenet(args)
args.y = args.y or 1
if args.bg ~= nil then
- args.fg_bg = core.graphics.cpair(args.bg, args.bg)
+ args.fg_bg = core.cpair(args.bg, args.bg)
end
-- create new graphics element base object
@@ -55,7 +56,7 @@ local function pipenet(args)
e.window.setCursorPos(x, y)
- local c = core.graphics.cpair(pipe.color, e.fg_bg.bkg)
+ local c = core.cpair(pipe.color, e.fg_bg.bkg)
if pipe.align_tr then
-- cross width then height
@@ -141,7 +142,7 @@ local function pipenet(args)
end
- return e.get()
+ return e.complete()
end
return pipenet
diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua
index 2f7a68d..cd4b8cf 100644
--- a/graphics/elements/rectangle.lua
+++ b/graphics/elements/rectangle.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new rectangle
---@param args rectangle_args
@@ -30,27 +31,35 @@ local function rectangle(args)
end
-- offset children
+ local offset_x = 0
+ local offset_y = 0
if args.border ~= nil then
- args.offset_x = args.border.width
- args.offset_y = args.border.width
+ offset_x = args.border.width
+ offset_y = args.border.width
-- slightly different y offset if the border is set to even
if args.border.even then
local width_x2 = (2 * args.border.width)
- args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
+ offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
end
end
-- create new graphics element base object
local e = element.new(args)
+ -- create content window for child elements
+ e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
+ e.content_window.setBackgroundColor(e.fg_bg.bkg)
+ e.content_window.setTextColor(e.fg_bg.fgd)
+ e.content_window.clear()
+
-- draw bordered box if requested
-- element constructor will have drawn basic colored rectangle regardless
if args.border ~= nil then
e.window.setCursorPos(1, 1)
- local border_width = args.offset_x
- local border_height = args.offset_y
+ local border_width = offset_x
+ local border_height = offset_y
local border_blit = colors.toBlit(args.border.color)
local width_x2 = border_width * 2
local inner_width = e.frame.w - width_x2
@@ -177,7 +186,7 @@ local function rectangle(args)
end
end
- return e.get()
+ return e.complete()
end
return rectangle
diff --git a/graphics/elements/textbox.lua b/graphics/elements/textbox.lua
index c911677..e72571b 100644
--- a/graphics/elements/textbox.lua
+++ b/graphics/elements/textbox.lua
@@ -5,7 +5,7 @@ local util = require("scada-common.util")
local core = require("graphics.core")
local element = require("graphics.element")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
---@class textbox_args
---@field text string text to show
@@ -18,6 +18,7 @@ local TEXT_ALIGN = core.graphics.TEXT_ALIGN
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new text box
---@param args textbox_args
@@ -64,7 +65,7 @@ local function textbox(args)
display_text(val)
end
- return e.get()
+ return e.complete()
end
return textbox
diff --git a/graphics/elements/tiling.lua b/graphics/elements/tiling.lua
index a97438a..536ed45 100644
--- a/graphics/elements/tiling.lua
+++ b/graphics/elements/tiling.lua
@@ -16,6 +16,7 @@ local element = require("graphics.element")
---@field height? integer parent height if omitted
---@field gframe? graphics_frame frame instead of x/y/width/height
---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
-- new tiling box
---@param args tiling_args
@@ -81,7 +82,7 @@ local function tiling(args)
if inner_width % 2 == 0 then alternator = not alternator end
end
- return e.get()
+ return e.complete()
end
return tiling
diff --git a/graphics/events.lua b/graphics/events.lua
new file mode 100644
index 0000000..3391a18
--- /dev/null
+++ b/graphics/events.lua
@@ -0,0 +1,161 @@
+--
+-- Graphics Events and Event Handlers
+--
+
+local util = require("scada-common.util")
+
+local events = {}
+
+---@enum CLICK_BUTTON
+events.CLICK_BUTTON = {
+ GENERIC = 0,
+ LEFT_BUTTON = 1,
+ RIGHT_BUTTON = 2,
+ MID_BUTTON = 3
+}
+
+---@enum CLICK_TYPE
+events.CLICK_TYPE = {
+ TAP = 1, -- screen tap (complete click)
+ DOWN = 2, -- button down
+ UP = 3, -- button up (completed a click)
+ DRAG = 4, -- mouse dragged
+ SCROLL_DOWN = 5, -- scroll down
+ SCROLL_UP = 6 -- scroll up
+}
+
+-- create a new 2D coordinate
+---@param x integer
+---@param y integer
+---@return coordinate_2d
+local function _coord2d(x, y) return { x = x, y = y } end
+
+---@class mouse_interaction
+---@field monitor string
+---@field button CLICK_BUTTON
+---@field type CLICK_TYPE
+---@field initial coordinate_2d
+---@field current coordinate_2d
+
+local handler = {
+ -- left, right, middle button down tracking
+ button_down = {
+ _coord2d(0, 0),
+ _coord2d(0, 0),
+ _coord2d(0, 0)
+ }
+}
+
+-- create a new monitor touch mouse interaction event
+---@nodiscard
+---@param monitor string
+---@param x integer
+---@param y integer
+---@return mouse_interaction
+local function _monitor_touch(monitor, x, y)
+ return {
+ monitor = monitor,
+ button = events.CLICK_BUTTON.GENERIC,
+ type = events.CLICK_TYPE.TAP,
+ initial = _coord2d(x, y),
+ current = _coord2d(x, y)
+ }
+end
+
+-- create a new mouse button mouse interaction event
+---@nodiscard
+---@param button CLICK_BUTTON mouse button
+---@param type CLICK_TYPE click type
+---@param x1 integer initial x
+---@param y1 integer initial y
+---@param x2 integer current x
+---@param y2 integer current y
+---@return mouse_interaction
+local function _mouse_event(button, type, x1, y1, x2, y2)
+ return {
+ monitor = "terminal",
+ button = button,
+ type = type,
+ initial = _coord2d(x1, y1),
+ current = _coord2d(x2, y2)
+ }
+end
+
+-- create a new generic mouse interaction event
+---@nodiscard
+---@param type CLICK_TYPE
+---@param x integer
+---@param y integer
+---@return mouse_interaction
+function events.mouse_generic(type, x, y)
+ return {
+ monitor = "",
+ button = events.CLICK_BUTTON.GENERIC,
+ type = type,
+ initial = _coord2d(x, y),
+ current = _coord2d(x, y)
+ }
+end
+
+-- create a new transposed mouse interaction event using the event's monitor/button fields
+---@nodiscard
+---@param event mouse_interaction
+---@param elem_pos_x integer element's x position: new x = (event x - element x) + 1
+---@param elem_pos_y integer element's y position: new y = (event y - element y) + 1
+---@return mouse_interaction
+function events.mouse_transposed(event, elem_pos_x, elem_pos_y)
+ return {
+ monitor = event.monitor,
+ button = event.button,
+ type = event.type,
+ initial = _coord2d((event.initial.x - elem_pos_x) + 1, (event.initial.y - elem_pos_y) + 1),
+ current = _coord2d((event.current.x - elem_pos_x) + 1, (event.current.y - elem_pos_y) + 1)
+ }
+end
+
+-- check if an event qualifies as a click (tap or up)
+---@nodiscard
+---@param t CLICK_TYPE
+function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
+
+-- create a new mouse event to pass onto graphics renderer
+-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
+---@param event_type os_event OS event to handle
+---@param opt integer|string button, scroll direction, or monitor for monitor touch
+---@param x integer x coordinate
+---@param y integer y coordinate
+---@return mouse_interaction|nil
+function events.new_mouse_event(event_type, opt, x, y)
+ if event_type == "mouse_click" then
+ ---@cast opt 1|2|3
+ handler.button_down[opt] = _coord2d(x, y)
+ return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
+ elseif event_type == "mouse_up" then
+ ---@cast opt 1|2|3
+ local initial = handler.button_down[opt] ---@type coordinate_2d
+ return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
+ elseif event_type == "monitor_touch" then
+ ---@cast opt string
+ return _monitor_touch(opt, x, y)
+ elseif event_type == "mouse_drag" then
+ ---@cast opt 1|2|3
+ local initial = handler.button_down[opt] ---@type coordinate_2d
+ return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
+ elseif event_type == "mouse_scroll" then
+ ---@cast opt 1|-1
+ local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
+ return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
+ end
+end
+
+-- create a new key event to pass onto graphics renderer
+-- supports: char, key, and key_up
+---@param event_type os_event
+function events.new_key_event(event_type)
+ if event_type == "char" then
+ elseif event_type == "key" then
+ elseif event_type == "key_up" then
+ end
+end
+
+return events
diff --git a/graphics/flasher.lua b/graphics/flasher.lua
index 0a3d9ea..520fba7 100644
--- a/graphics/flasher.lua
+++ b/graphics/flasher.lua
@@ -2,7 +2,7 @@
-- Indicator Light Flasher
--
-local tcd = require("scada-common.tcallbackdsp")
+local tcd = require("scada-common.tcd")
local flasher = {}
diff --git a/imgen.py b/imgen.py
index 9a1e194..e52543c 100644
--- a/imgen.py
+++ b/imgen.py
@@ -108,10 +108,10 @@ f = open("install_manifest.json", "w")
json.dump(final_manifest, f)
f.close()
-if len(sys.argv) > 1 and sys.argv[1] == "shields":
+if len(sys.argv) > 1 and sys.argv[1] == "gh_actions":
# write all the JSON files for shields.io
for key, version in final_manifest["versions"].items():
- f = open("./shields/" + key + ".json", "w")
+ f = open("./deploy/" + key + ".json", "w")
if version.find("alpha") >= 0:
color = "yellow"
diff --git a/install_manifest.json b/install_manifest.json
index 5b53c17..249b201 100644
--- a/install_manifest.json
+++ b/install_manifest.json
@@ -1 +1 @@
-{"versions": {"installer": "v1.0", "bootloader": "0.2", "comms": "1.4.1", "reactor-plc": "v1.1.17", "rtu": "v1.0.5", "supervisor": "v0.15.7", "coordinator": "v0.13.8", "pocket": "alpha-v0.2.6"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/constants.lua", "scada-common/crypto.lua", "scada-common/types.lua", "scada-common/comms.lua", "scada-common/mqueue.lua", "scada-common/util.lua", "scada-common/psil.lua", "scada-common/crash.lua", "scada-common/rsio.lua", "scada-common/ppm.lua", "scada-common/tcallbackdsp.lua", "scada-common/log.lua"], "graphics": ["graphics/core.lua", "graphics/flasher.lua", "graphics/element.lua", "graphics/elements/div.lua", "graphics/elements/colormap.lua", "graphics/elements/textbox.lua", "graphics/elements/pipenet.lua", "graphics/elements/multipane.lua", "graphics/elements/displaybox.lua", "graphics/elements/rectangle.lua", "graphics/elements/tiling.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/animations/waiting.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/indicators/alight.lua"], "lockbox": ["lockbox/LICENSE", "lockbox/init.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/digest/sha2_224.lua", "lockbox/cipher/aes128.lua", "lockbox/cipher/aes192.lua", "lockbox/cipher/aes256.lua", "lockbox/cipher/mode/cfb.lua", "lockbox/cipher/mode/cbc.lua", "lockbox/cipher/mode/ofb.lua", "lockbox/cipher/mode/ctr.lua", "lockbox/mac/hmac.lua", "lockbox/kdf/pbkdf2.lua", "lockbox/padding/ansix923.lua", "lockbox/padding/isoiec7816.lua", "lockbox/padding/zero.lua", "lockbox/padding/pkcs7.lua", "lockbox/util/stream.lua", "lockbox/util/bit.lua", "lockbox/util/queue.lua", "lockbox/util/array.lua"], "reactor-plc": ["reactor-plc/config.lua", "reactor-plc/plc.lua", "reactor-plc/renderer.lua", "reactor-plc/databus.lua", "reactor-plc/startup.lua", "reactor-plc/threads.lua", "reactor-plc/panel/style.lua", "reactor-plc/panel/front_panel.lua"], "rtu": ["rtu/modbus.lua", "rtu/rtu.lua", "rtu/config.lua", "rtu/renderer.lua", "rtu/databus.lua", "rtu/startup.lua", "rtu/threads.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/turbinev_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/panel/style.lua", "rtu/panel/front_panel.lua"], "supervisor": ["supervisor/unit.lua", "supervisor/supervisor.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/facility.lua", "supervisor/unitlogic.lua", "supervisor/session/coordinator.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/svsessions.lua", "supervisor/session/pocket.lua", "supervisor/session/svqtypes.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/redstone.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/sps.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/iocontrol.lua", "coordinator/config.lua", "coordinator/renderer.lua", "coordinator/startup.lua", "coordinator/sounder.lua", "coordinator/process.lua", "coordinator/session/api.lua", "coordinator/session/apisessions.lua", "coordinator/ui/style.lua", "coordinator/ui/dialog.lua", "coordinator/ui/components/processctl.lua", "coordinator/ui/components/turbine.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua"], "pocket": ["pocket/config.lua", "pocket/renderer.lua", "pocket/startup.lua", "pocket/coreio.lua", "pocket/pocket.lua", "pocket/ui/style.lua", "pocket/ui/main.lua", "pocket/ui/components/reactor_page.lua", "pocket/ui/components/boiler_page.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/components/home_page.lua", "pocket/ui/components/unit_page.lua", "pocket/ui/components/turbine_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common", "graphics"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 5467, "system": 1991, "common": 90488, "graphics": 115520, "lockbox": 100797, "reactor-plc": 95520, "rtu": 100773, "supervisor": 283040, "coordinator": 196036, "pocket": 36123}}
\ No newline at end of file
+{"versions": {"installer": "v1.2", "bootloader": "0.2", "comms": "1.4.1", "reactor-plc": "v1.3.7", "rtu": "v1.2.7", "supervisor": "v0.16.7", "coordinator": "v0.15.7", "pocket": "alpha-v0.3.7"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/crypto.lua", "scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/tcd.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua"], "graphics": ["graphics/element.lua", "graphics/events.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/listbox.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/multipane.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/tabbar.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/cipher/aes128.lua", "lockbox/cipher/aes256.lua", "lockbox/cipher/aes192.lua", "lockbox/cipher/mode/ofb.lua", "lockbox/cipher/mode/cbc.lua", "lockbox/cipher/mode/ctr.lua", "lockbox/cipher/mode/cfb.lua", "lockbox/mac/hmac.lua", "lockbox/padding/ansix923.lua", "lockbox/padding/pkcs7.lua", "lockbox/padding/zero.lua", "lockbox/padding/isoiec7816.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/renderer.lua", "rtu/threads.lua", "rtu/rtu.lua", "rtu/databus.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/panel/front_panel.lua", "rtu/panel/style.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/renderer.lua", "supervisor/databus.lua", "supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/panel/pgi.lua", "supervisor/panel/front_panel.lua", "supervisor/panel/style.lua", "supervisor/panel/components/rtu_entry.lua", "supervisor/panel/components/pdg_entry.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/pocket.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/processctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/turbine.lua", "coordinator/session/api.lua", "coordinator/session/apisessions.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/config.lua", "pocket/coreio.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/pages/turbine_page.lua", "pocket/ui/pages/reactor_page.lua", "pocket/ui/pages/home_page.lua", "pocket/ui/pages/unit_page.lua", "pocket/ui/pages/boiler_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common", "graphics"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 5766, "system": 1991, "common": 91039, "graphics": 144028, "lockbox": 100797, "reactor-plc": 95846, "rtu": 101207, "supervisor": 310842, "coordinator": 197505, "pocket": 36280}}
\ No newline at end of file
diff --git a/pocket/renderer.lua b/pocket/renderer.lua
index 1ecf3ab..fa25bcd 100644
--- a/pocket/renderer.lua
+++ b/pocket/renderer.lua
@@ -70,9 +70,9 @@ end
function renderer.ui_ready() return ui.display ~= nil end
-- handle a mouse event
----@param event mouse_interaction
+---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
- if ui.display ~= nil then
+ if ui.display ~= nil and event ~= nil then
ui.display.handle_mouse(event)
end
end
diff --git a/pocket/startup.lua b/pocket/startup.lua
index c1e9ccc..1ac6a5a 100644
--- a/pocket/startup.lua
+++ b/pocket/startup.lua
@@ -4,20 +4,20 @@
require("/initenv").init_env()
-local crash = require("scada-common.crash")
-local log = require("scada-common.log")
-local ppm = require("scada-common.ppm")
-local tcallbackdsp = require("scada-common.tcallbackdsp")
-local util = require("scada-common.util")
+local crash = require("scada-common.crash")
+local log = require("scada-common.log")
+local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcd")
+local util = require("scada-common.util")
-local core = require("graphics.core")
+local core = require("graphics.core")
-local config = require("pocket.config")
-local coreio = require("pocket.coreio")
-local pocket = require("pocket.pocket")
-local renderer = require("pocket.renderer")
+local config = require("pocket.config")
+local coreio = require("pocket.coreio")
+local pocket = require("pocket.pocket")
+local renderer = require("pocket.renderer")
-local POCKET_VERSION = "alpha-v0.2.6"
+local POCKET_VERSION = "alpha-v0.3.7"
local println = util.println
local println_ts = util.println_ts
@@ -36,7 +36,6 @@ cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
-cfv.assert_type_bool(config.LOG_DEBUG)
assert(cfv.valid(), "bad config file: missing/invalid fields")
@@ -44,7 +43,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
-- log init
----------------------------------------
-log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG)
+log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================")
log.info("BOOTING pocket.startup " .. POCKET_VERSION)
@@ -121,54 +120,54 @@ local function main()
conn_wd.sv.feed()
conn_wd.api.feed()
log.debug("startup> conn watchdog started")
- end
- -- main event loop
- while ui_ok do
- local event, param1, param2, param3, param4, param5 = util.pull_event()
+ -- main event loop
+ while true do
+ local event, param1, param2, param3, param4, param5 = util.pull_event()
- -- handle event
- if event == "timer" then
- if loop_clock.is_clock(param1) then
- -- main loop tick
+ -- handle event
+ if event == "timer" then
+ if loop_clock.is_clock(param1) then
+ -- main loop tick
- -- relink if necessary
- pocket_comms.link_update()
+ -- relink if necessary
+ pocket_comms.link_update()
- loop_clock.start()
- elseif conn_wd.sv.is_timer(param1) then
- -- supervisor watchdog timeout
- log.info("supervisor server timeout")
- pocket_comms.close_sv()
- elseif conn_wd.api.is_timer(param1) then
- -- coordinator watchdog timeout
- log.info("coordinator api server timeout")
- pocket_comms.close_api()
- else
- -- a non-clock/main watchdog timer event
- -- notify timer callback dispatcher
- tcallbackdsp.handle(param1)
+ loop_clock.start()
+ elseif conn_wd.sv.is_timer(param1) then
+ -- supervisor watchdog timeout
+ log.info("supervisor server timeout")
+ pocket_comms.close_sv()
+ elseif conn_wd.api.is_timer(param1) then
+ -- coordinator watchdog timeout
+ log.info("coordinator api server timeout")
+ pocket_comms.close_api()
+ else
+ -- a non-clock/main watchdog timer event
+ -- notify timer callback dispatcher
+ tcd.handle(param1)
+ end
+ elseif event == "modem_message" then
+ -- got a packet
+ local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
+ pocket_comms.handle_packet(packet)
+ elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
+ -- handle a monitor touch event
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
+ end
+
+ -- check for termination request
+ if event == "terminate" or ppm.should_terminate() then
+ log.info("terminate requested, closing server connections...")
+ pocket_comms.close()
+ log.info("connections closed")
+ break
end
- elseif event == "modem_message" then
- -- got a packet
- local packet = pocket_comms.parse_packet(param1, param2, param3, param4, param5)
- pocket_comms.handle_packet(packet)
- elseif event == "mouse_click" then
- -- handle a monitor touch event
- renderer.handle_mouse(core.events.touch(param1, param2, param3))
end
- -- check for termination request
- if event == "terminate" or ppm.should_terminate() then
- log.info("terminate requested, closing server connections...")
- pocket_comms.close()
- log.info("connections closed")
- break
- end
+ renderer.close_ui()
end
- renderer.close_ui()
-
println_ts("exited")
log.info("exited")
end
diff --git a/pocket/ui/components/conn_waiting.lua b/pocket/ui/components/conn_waiting.lua
index cd08652..114d165 100644
--- a/pocket/ui/components/conn_waiting.lua
+++ b/pocket/ui/components/conn_waiting.lua
@@ -11,9 +11,9 @@ local TextBox = require("graphics.elements.textbox")
local WaitingAnim = require("graphics.elements.animations.waiting")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- create a waiting view
---@param parent graphics_element parent
@@ -25,7 +25,7 @@ local function init(parent, y, is_api)
-- bounding box div
local box = Div{parent=root,x=1,y=y,height=5}
- local waiting_x = math.floor(parent.width() / 2) - 1
+ local waiting_x = math.floor(parent.get_width() / 2) - 1
if is_api then
WaitingAnim{parent=box,x=waiting_x,y=1,fg_bg=cpair(colors.blue,style.root.bkg)}
diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua
index 5af9ce8..11331e4 100644
--- a/pocket/ui/main.lua
+++ b/pocket/ui/main.lua
@@ -8,11 +8,11 @@ local style = require("pocket.ui.style")
local conn_waiting = require("pocket.ui.components.conn_waiting")
-local home_page = require("pocket.ui.components.home_page")
-local unit_page = require("pocket.ui.components.unit_page")
-local reactor_page = require("pocket.ui.components.reactor_page")
-local boiler_page = require("pocket.ui.components.boiler_page")
-local turbine_page = require("pocket.ui.components.turbine_page")
+local home_page = require("pocket.ui.pages.home_page")
+local unit_page = require("pocket.ui.pages.unit_page")
+local reactor_page = require("pocket.ui.pages.reactor_page")
+local boiler_page = require("pocket.ui.pages.boiler_page")
+local turbine_page = require("pocket.ui.pages.turbine_page")
local core = require("graphics.core")
@@ -22,9 +22,9 @@ local TextBox = require("graphics.elements.textbox")
local Sidebar = require("graphics.elements.controls.sidebar")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- create new main view
---@param main graphics_element main displaybox
@@ -45,7 +45,7 @@ local function init(main)
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
- coreio.core_ps().subscribe("link_state", function (state)
+ root_pane.register(coreio.core_ps(), "link_state", function (state)
if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then
root_pane.set_value(1)
elseif state == coreio.LINK_STATE.SV_LINK_ONLY then
diff --git a/pocket/ui/components/boiler_page.lua b/pocket/ui/pages/boiler_page.lua
similarity index 84%
rename from pocket/ui/components/boiler_page.lua
rename to pocket/ui/pages/boiler_page.lua
index fd0eca1..ec13d59 100644
--- a/pocket/ui/components/boiler_page.lua
+++ b/pocket/ui/pages/boiler_page.lua
@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
--- local cpair = core.graphics.cpair
+-- local cpair = core.cpair
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-- new boiler page view
---@param root graphics_element parent
diff --git a/pocket/ui/components/home_page.lua b/pocket/ui/pages/home_page.lua
similarity index 84%
rename from pocket/ui/components/home_page.lua
rename to pocket/ui/pages/home_page.lua
index 5287cac..a31cae8 100644
--- a/pocket/ui/components/home_page.lua
+++ b/pocket/ui/pages/home_page.lua
@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
--- local cpair = core.graphics.cpair
+-- local cpair = core.cpair
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-- new home page view
---@param root graphics_element parent
diff --git a/pocket/ui/components/reactor_page.lua b/pocket/ui/pages/reactor_page.lua
similarity index 84%
rename from pocket/ui/components/reactor_page.lua
rename to pocket/ui/pages/reactor_page.lua
index 50b1939..c7a2e96 100644
--- a/pocket/ui/components/reactor_page.lua
+++ b/pocket/ui/pages/reactor_page.lua
@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
--- local cpair = core.graphics.cpair
+-- local cpair = core.cpair
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-- new reactor page view
---@param root graphics_element parent
diff --git a/pocket/ui/components/turbine_page.lua b/pocket/ui/pages/turbine_page.lua
similarity index 84%
rename from pocket/ui/components/turbine_page.lua
rename to pocket/ui/pages/turbine_page.lua
index 9fd7af5..527e419 100644
--- a/pocket/ui/components/turbine_page.lua
+++ b/pocket/ui/pages/turbine_page.lua
@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
--- local cpair = core.graphics.cpair
+-- local cpair = core.cpair
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-- new turbine page view
---@param root graphics_element parent
diff --git a/pocket/ui/components/unit_page.lua b/pocket/ui/pages/unit_page.lua
similarity index 84%
rename from pocket/ui/components/unit_page.lua
rename to pocket/ui/pages/unit_page.lua
index 2e24df3..a0718e6 100644
--- a/pocket/ui/components/unit_page.lua
+++ b/pocket/ui/pages/unit_page.lua
@@ -5,9 +5,9 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox")
--- local cpair = core.graphics.cpair
+-- local cpair = core.cpair
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-- new unit page view
---@param root graphics_element parent
diff --git a/pocket/ui/style.lua b/pocket/ui/style.lua
index b9a09fc..2fb7526 100644
--- a/pocket/ui/style.lua
+++ b/pocket/ui/style.lua
@@ -6,7 +6,7 @@ local core = require("graphics.core")
local style = {}
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- GLOBAL --
diff --git a/reactor-plc/databus.lua b/reactor-plc/databus.lua
index beee265..ba7a68e 100644
--- a/reactor-plc/databus.lua
+++ b/reactor-plc/databus.lua
@@ -8,14 +8,16 @@ local util = require("scada-common.util")
local databus = {}
+-- databus PSIL
+databus.ps = psil.create()
+
local dbus_iface = {
- ps = psil.create(),
rps_scram = function () log.debug("DBUS: unset rps_scram() called") end,
rps_reset = function () log.debug("DBUS: unset rps_reset() called") end
}
-- call to toggle heartbeat signal
-function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
+function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- link RPS command functions
---@param scram function reactor SCRAM function
@@ -35,42 +37,42 @@ function databus.rps_reset() dbus_iface.rps_reset() end
---@param plc_v string PLC version
---@param comms_v string comms version
function databus.tx_versions(plc_v, comms_v)
- dbus_iface.ps.publish("version", plc_v)
- dbus_iface.ps.publish("comms_version", comms_v)
+ databus.ps.publish("version", plc_v)
+ databus.ps.publish("comms_version", comms_v)
end
-- transmit unit ID across the bus
---@param id integer unit ID
function databus.tx_id(id)
- dbus_iface.ps.publish("unit_id", id)
+ databus.ps.publish("unit_id", id)
end
-- transmit hardware status across the bus
---@param plc_state plc_state
function databus.tx_hw_status(plc_state)
- dbus_iface.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
- dbus_iface.ps.publish("has_modem", not plc_state.no_modem)
- dbus_iface.ps.publish("degraded", plc_state.degraded)
- dbus_iface.ps.publish("init_ok", plc_state.init_ok)
+ databus.ps.publish("reactor_dev_state", util.trinary(plc_state.no_reactor, 1, util.trinary(plc_state.reactor_formed, 3, 2)))
+ databus.ps.publish("has_modem", not plc_state.no_modem)
+ databus.ps.publish("degraded", plc_state.degraded)
+ databus.ps.publish("init_ok", plc_state.init_ok)
end
-- transmit thread (routine) statuses
---@param thread string thread name
---@param ok boolean thread state
function databus.tx_rt_status(thread, ok)
- dbus_iface.ps.publish(util.c("routine__", thread), ok)
+ databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
---@param state integer
function databus.tx_link_state(state)
- dbus_iface.ps.publish("link_state", state)
+ databus.ps.publish("link_state", state)
end
-- transmit reactor enable state across the bus
---@param active boolean reactor active
function databus.tx_reactor_state(active)
- dbus_iface.ps.publish("reactor_active", active)
+ databus.ps.publish("reactor_active", active)
end
-- transmit RPS data across the bus
@@ -78,26 +80,26 @@ end
---@param status table RPS status
---@param emer_cool_active boolean RPS activated the emergency coolant
function databus.tx_rps(tripped, status, emer_cool_active)
- dbus_iface.ps.publish("rps_scram", tripped)
- dbus_iface.ps.publish("rps_damage", status[1])
- dbus_iface.ps.publish("rps_high_temp", status[2])
- dbus_iface.ps.publish("rps_low_ccool", status[3])
- dbus_iface.ps.publish("rps_high_waste", status[4])
- dbus_iface.ps.publish("rps_high_hcool", status[5])
- dbus_iface.ps.publish("rps_no_fuel", status[6])
- dbus_iface.ps.publish("rps_fault", status[7])
- dbus_iface.ps.publish("rps_timeout", status[8])
- dbus_iface.ps.publish("rps_manual", status[9])
- dbus_iface.ps.publish("rps_automatic", status[10])
- dbus_iface.ps.publish("rps_sysfail", status[11])
- dbus_iface.ps.publish("emer_cool", emer_cool_active)
+ databus.ps.publish("rps_scram", tripped)
+ databus.ps.publish("rps_damage", status[1])
+ databus.ps.publish("rps_high_temp", status[2])
+ databus.ps.publish("rps_low_ccool", status[3])
+ databus.ps.publish("rps_high_waste", status[4])
+ databus.ps.publish("rps_high_hcool", status[5])
+ databus.ps.publish("rps_no_fuel", status[6])
+ databus.ps.publish("rps_fault", status[7])
+ databus.ps.publish("rps_timeout", status[8])
+ databus.ps.publish("rps_manual", status[9])
+ databus.ps.publish("rps_automatic", status[10])
+ databus.ps.publish("rps_sysfail", status[11])
+ databus.ps.publish("emer_cool", emer_cool_active)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
- dbus_iface.ps.subscribe(field, func)
+ databus.ps.subscribe(field, func)
end
return databus
diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua
index 70c80ef..8e28a75 100644
--- a/reactor-plc/panel/front_panel.lua
+++ b/reactor-plc/panel/front_panel.lua
@@ -22,16 +22,16 @@ local LED = require("graphics.elements.indicators.led")
local LEDPair = require("graphics.elements.indicators.ledpair")
local RGBLED = require("graphics.elements.indicators.ledrgb")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
-local border = core.graphics.border
+local cpair = core.cpair
+local border = core.border
-- create new main view
---@param panel graphics_element main displaybox
local function init(panel)
local header = TextBox{parent=panel,y=1,text="REACTOR PLC - UNIT ?",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
- databus.rx_field("unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
+ header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("REACTOR PLC - UNIT ", id)) end)
--
-- system indicators
@@ -43,8 +43,8 @@ local function init(panel)
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
system.line_break()
- databus.rx_field("init_ok", init_ok.update)
- databus.rx_field("heartbeat", heartbeat.update)
+ init_ok.register(databus.ps, "init_ok", init_ok.update)
+ heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local reactor = LEDPair{parent=system,label="REACTOR",off=colors.red,c1=colors.yellow,c2=colors.green}
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
@@ -52,9 +52,9 @@ local function init(panel)
network.update(5)
system.line_break()
- databus.rx_field("reactor_dev_state", reactor.update)
- databus.rx_field("has_modem", modem.update)
- databus.rx_field("link_state", network.update)
+ reactor.register(databus.ps, "reactor_dev_state", reactor.update)
+ modem.register(databus.ps, "has_modem", modem.update)
+ network.register(databus.ps, "link_state", network.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
local rt_rps = LED{parent=system,label="RT RPS",colors=cpair(colors.green,colors.green_off)}
@@ -63,11 +63,11 @@ local function init(panel)
local rt_sctl = LED{parent=system,label="RT SPCTL",colors=cpair(colors.green,colors.green_off)}
system.line_break()
- databus.rx_field("routine__main", rt_main.update)
- databus.rx_field("routine__rps", rt_rps.update)
- databus.rx_field("routine__comms_tx", rt_cmtx.update)
- databus.rx_field("routine__comms_rx", rt_cmrx.update)
- databus.rx_field("routine__spctl", rt_sctl.update)
+ rt_main.register(databus.ps, "routine__main", rt_main.update)
+ rt_rps.register(databus.ps, "routine__rps", rt_rps.update)
+ rt_cmtx.register(databus.ps, "routine__comms_tx", rt_cmtx.update)
+ rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update)
+ rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update)
--
-- status & controls
@@ -80,7 +80,7 @@ local function init(panel)
-- only show emergency coolant LED if emergency coolant is configured for this device
if type(config.EMERGENCY_COOL) == "table" then
local emer_cool = LED{parent=status,x=2,width=14,label="EMER COOLANT",colors=cpair(colors.yellow,colors.yellow_off)}
- databus.rx_field("emer_cool", emer_cool.update)
+ emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
end
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,colors.lightGray,true),even_inner=true,fg_bg=cpair(colors.black,colors.ivory)}
@@ -92,8 +92,8 @@ local function init(panel)
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
- databus.rx_field("reactor_active", active.update)
- databus.rx_field("rps_scram", scram.update)
+ active.register(databus.ps, "reactor_active", active.update)
+ scram.register(databus.ps, "rps_scram", scram.update)
--
-- about footer
@@ -103,8 +103,8 @@ local function init(panel)
local fw_v = TextBox{parent=about,x=2,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=17,y=1,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
- databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
- databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
+ fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
+ comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- rps list
@@ -126,17 +126,17 @@ local function init(panel)
local rps_ccl = LED{parent=rps,label="LO CCOOLANT",colors=cpair(colors.red,colors.red_off)}
local rps_hcl = LED{parent=rps,label="HI HCOOLANT",colors=cpair(colors.red,colors.red_off)}
- databus.rx_field("rps_manual", rps_man.update)
- databus.rx_field("rps_automatic", rps_auto.update)
- databus.rx_field("rps_timeout", rps_tmo.update)
- databus.rx_field("rps_fault", rps_flt.update)
- databus.rx_field("rps_sysfail", rps_fail.update)
- databus.rx_field("rps_damage", rps_dmg.update)
- databus.rx_field("rps_high_temp", rps_tmp.update)
- databus.rx_field("rps_no_fuel", rps_nof.update)
- databus.rx_field("rps_high_waste", rps_wst.update)
- databus.rx_field("rps_low_ccool", rps_ccl.update)
- databus.rx_field("rps_high_hcool", rps_hcl.update)
+ rps_man.register(databus.ps, "rps_manual", rps_man.update)
+ rps_auto.register(databus.ps, "rps_automatic", rps_auto.update)
+ rps_tmo.register(databus.ps, "rps_timeout", rps_tmo.update)
+ rps_flt.register(databus.ps, "rps_fault", rps_flt.update)
+ rps_fail.register(databus.ps, "rps_sysfail", rps_fail.update)
+ rps_dmg.register(databus.ps, "rps_damage", rps_dmg.update)
+ rps_tmp.register(databus.ps, "rps_high_temp", rps_tmp.update)
+ rps_nof.register(databus.ps, "rps_no_fuel", rps_nof.update)
+ rps_wst.register(databus.ps, "rps_high_waste", rps_wst.update)
+ rps_ccl.register(databus.ps, "rps_low_ccool", rps_ccl.update)
+ rps_hcl.register(databus.ps, "rps_high_hcool", rps_hcl.update)
end
return init
diff --git a/reactor-plc/panel/style.lua b/reactor-plc/panel/style.lua
index 31039d4..996453c 100644
--- a/reactor-plc/panel/style.lua
+++ b/reactor-plc/panel/style.lua
@@ -6,7 +6,7 @@ local core = require("graphics.core")
local style = {}
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- GLOBAL --
diff --git a/reactor-plc/renderer.lua b/reactor-plc/renderer.lua
index 0700ebe..038918b 100644
--- a/reactor-plc/renderer.lua
+++ b/reactor-plc/renderer.lua
@@ -44,10 +44,8 @@ function renderer.close_ui()
-- stop blinking indicators
flasher.clear()
- -- hide to stop animation callbacks
- ui.display.hide()
-
- -- clear root UI elements
+ -- delete element tree
+ ui.display.delete()
ui.display = nil
-- restore colors
@@ -70,9 +68,9 @@ end
function renderer.ui_ready() return ui.display ~= nil end
-- handle a mouse event
----@param event mouse_interaction
+---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
- if ui.display ~= nil then
+ if ui.display ~= nil and event ~= nil then
ui.display.handle_mouse(event)
end
end
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index cf303c8..682e618 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
-local R_PLC_VERSION = "v1.3.0"
+local R_PLC_VERSION = "v1.4.0"
local println = util.println
local println_ts = util.println_ts
@@ -38,7 +38,6 @@ cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
-cfv.assert_type_bool(config.LOG_DEBUG)
assert(cfv.valid(), "bad config file: missing/invalid fields")
@@ -55,7 +54,7 @@ end
-- log init
----------------------------------------
-log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG)
+log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================")
log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION)
diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua
index 8470430..b9c986d 100644
--- a/reactor-plc/threads.lua
+++ b/reactor-plc/threads.lua
@@ -1,13 +1,13 @@
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local ppm = require("scada-common.ppm")
-local tcallbackdsp = require("scada-common.tcallbackdsp")
-local util = require("scada-common.util")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcd")
+local util = require("scada-common.util")
-local databus = require("reactor-plc.databus")
-local renderer = require("reactor-plc.renderer")
+local databus = require("reactor-plc.databus")
+local renderer = require("reactor-plc.renderer")
-local core = require("graphics.core")
+local core = require("graphics.core")
local threads = {}
@@ -157,7 +157,7 @@ function threads.thread__main(smem, init)
smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT)
elseif event == "timer" then
-- notify timer callback dispatcher if no other timer case claimed this event
- tcallbackdsp.handle(param1)
+ tcd.handle(param1)
elseif event == "peripheral_detach" then
-- peripheral disconnect
local type, device = ppm.handle_unmount(param1)
@@ -257,9 +257,9 @@ function threads.thread__main(smem, init)
-- update indicators
databus.tx_hw_status(plc_state)
- elseif event == "mouse_click" then
- -- handle a monitor touch event
- renderer.handle_mouse(core.events.click(param1, param2, param3))
+ elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
+ -- handle a mouse event
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
elseif event == "clock_start" then
-- start loop clock
loop_clock.start()
diff --git a/rtu/databus.lua b/rtu/databus.lua
index 8ef720e..3014367 100644
--- a/rtu/databus.lua
+++ b/rtu/databus.lua
@@ -7,9 +7,8 @@ local util = require("scada-common.util")
local databus = {}
-local dbus_iface = {
- ps = psil.create()
-}
+-- databus PSIL
+databus.ps = psil.create()
---@enum RTU_UNIT_HW_STATE
local RTU_UNIT_HW_STATE = {
@@ -22,54 +21,54 @@ local RTU_UNIT_HW_STATE = {
databus.RTU_UNIT_HW_STATE = RTU_UNIT_HW_STATE
-- call to toggle heartbeat signal
-function databus.heartbeat() dbus_iface.ps.toggle("heartbeat") end
+function databus.heartbeat() databus.ps.toggle("heartbeat") end
-- transmit firmware versions across the bus
---@param rtu_v string RTU version
---@param comms_v string comms version
function databus.tx_versions(rtu_v, comms_v)
- dbus_iface.ps.publish("version", rtu_v)
- dbus_iface.ps.publish("comms_version", comms_v)
+ databus.ps.publish("version", rtu_v)
+ databus.ps.publish("comms_version", comms_v)
end
-- transmit hardware status for modem connection state
---@param has_modem boolean
function databus.tx_hw_modem(has_modem)
- dbus_iface.ps.publish("has_modem", has_modem)
+ databus.ps.publish("has_modem", has_modem)
end
-- transmit unit hardware type across the bus
---@param uid integer unit ID
---@param type RTU_UNIT_TYPE
function databus.tx_unit_hw_type(uid, type)
- dbus_iface.ps.publish("unit_type_" .. uid, type)
+ databus.ps.publish("unit_type_" .. uid, type)
end
-- transmit unit hardware status across the bus
---@param uid integer unit ID
---@param status RTU_UNIT_HW_STATE
function databus.tx_unit_hw_status(uid, status)
- dbus_iface.ps.publish("unit_hw_" .. uid, status)
+ databus.ps.publish("unit_hw_" .. uid, status)
end
-- transmit thread (routine) statuses
---@param thread string thread name
---@param ok boolean thread state
function databus.tx_rt_status(thread, ok)
- dbus_iface.ps.publish(util.c("routine__", thread), ok)
+ databus.ps.publish(util.c("routine__", thread), ok)
end
-- transmit supervisor link state across the bus
---@param state integer
function databus.tx_link_state(state)
- dbus_iface.ps.publish("link_state", state)
+ databus.ps.publish("link_state", state)
end
-- link a function to receive data from the bus
---@param field string field name
---@param func function function to link
function databus.rx_field(field, func)
- dbus_iface.ps.subscribe(field, func)
+ databus.ps.subscribe(field, func)
end
return databus
diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua
index f6014da..606abbb 100644
--- a/rtu/panel/front_panel.lua
+++ b/rtu/panel/front_panel.lua
@@ -16,9 +16,9 @@ local TextBox = require("graphics.elements.textbox")
local LED = require("graphics.elements.indicators.led")
local RGBLED = require("graphics.elements.indicators.ledrgb")
-local TEXT_ALIGN = core.graphics.TEXT_ALIGN
+local TEXT_ALIGN = core.TEXT_ALIGN
-local cpair = core.graphics.cpair
+local cpair = core.cpair
local UNIT_TYPE_LABELS = {
"UNKNOWN",
@@ -44,27 +44,27 @@ local function init(panel, units)
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
- local on = LED{parent=system,label="POWER",colors=cpair(colors.green,colors.red)}
+ local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
on.update(true)
system.line_break()
- databus.rx_field("heartbeat", heartbeat.update)
+ heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
network.update(5)
system.line_break()
- databus.rx_field("has_modem", modem.update)
- databus.rx_field("link_state", network.update)
+ modem.register(databus.ps, "has_modem", modem.update)
+ network.register(databus.ps, "link_state", network.update)
local rt_main = LED{parent=system,label="RT MAIN",colors=cpair(colors.green,colors.green_off)}
local rt_comm = LED{parent=system,label="RT COMMS",colors=cpair(colors.green,colors.green_off)}
system.line_break()
- databus.rx_field("routine__main", rt_main.update)
- databus.rx_field("routine__comms", rt_comm.update)
+ rt_main.register(databus.ps, "routine__main", rt_main.update)
+ rt_comm.register(databus.ps, "routine__comms", rt_comm.update)
--
-- about label
@@ -74,8 +74,8 @@ local function init(panel, units)
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
- databus.rx_field("version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
- databus.rx_field("comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
+ fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
+ comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
--
-- unit status list
@@ -90,7 +90,7 @@ local function init(panel, units)
for i = 1, list_length do
TextBox{parent=threads,x=1,y=i,text=util.sprintf("%02d",i),height=1}
local rt_unit = LED{parent=threads,x=4,y=i,label="RT",colors=cpair(colors.green,colors.green_off)}
- databus.rx_field("routine__unit_" .. i, rt_unit.update)
+ rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3}
@@ -102,13 +102,13 @@ local function init(panel, units)
-- hardware status
local unit_hw = RGBLED{parent=unit_hw_statuses,y=i,label="",colors={colors.red,colors.orange,colors.yellow,colors.green}}
- databus.rx_field("unit_hw_" .. i, unit_hw.update)
+ unit_hw.register(databus.ps, "unit_hw_" .. i, unit_hw.update)
-- unit name identifier (type + index)
local name = util.c(UNIT_TYPE_LABELS[unit.type + 1], " ", unit.index)
local name_box = TextBox{parent=unit_hw_statuses,y=i,x=3,text=name,height=1}
- databus.rx_field("unit_type_" .. i, function (t)
+ name_box.register(databus.ps, "unit_type_" .. i, function (t)
name_box.set_value(util.c(UNIT_TYPE_LABELS[t + 1], " ", unit.index))
end)
diff --git a/rtu/panel/style.lua b/rtu/panel/style.lua
index 31039d4..996453c 100644
--- a/rtu/panel/style.lua
+++ b/rtu/panel/style.lua
@@ -6,7 +6,7 @@ local core = require("graphics.core")
local style = {}
-local cpair = core.graphics.cpair
+local cpair = core.cpair
-- GLOBAL --
diff --git a/rtu/renderer.lua b/rtu/renderer.lua
index f79f19e..17949ce 100644
--- a/rtu/renderer.lua
+++ b/rtu/renderer.lua
@@ -45,10 +45,8 @@ function renderer.close_ui()
-- stop blinking indicators
flasher.clear()
- -- hide to stop animation callbacks
- ui.display.hide()
-
- -- clear root UI elements
+ -- delete element tree
+ ui.display.delete()
ui.display = nil
-- restore colors
@@ -71,9 +69,9 @@ end
function renderer.ui_ready() return ui.display ~= nil end
-- handle a mouse event
----@param event mouse_interaction
+---@param event mouse_interaction|nil
function renderer.handle_mouse(event)
- if ui.display ~= nil then
+ if ui.display ~= nil and event ~= nil then
ui.display.handle_mouse(event)
end
end
diff --git a/rtu/startup.lua b/rtu/startup.lua
index 366af28..020c2aa 100644
--- a/rtu/startup.lua
+++ b/rtu/startup.lua
@@ -28,7 +28,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
-local RTU_VERSION = "v1.0.5"
+local RTU_VERSION = "v1.2.8"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@@ -49,7 +49,6 @@ cfv.assert_type_num(config.COMMS_TIMEOUT)
cfv.assert_min(config.COMMS_TIMEOUT, 2)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
-cfv.assert_type_bool(config.LOG_DEBUG)
cfv.assert_type_table(config.RTU_DEVICES)
cfv.assert_type_table(config.RTU_REDSTONE)
assert(cfv.valid(), "bad config file: missing/invalid fields")
@@ -58,7 +57,7 @@ assert(cfv.valid(), "bad config file: missing/invalid fields")
-- log init
----------------------------------------
-log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG)
+log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================")
log.info("BOOTING rtu.startup " .. RTU_VERSION)
@@ -458,9 +457,9 @@ local function main()
if not rtu_state.fp_ok then
renderer.close_ui()
println_ts(util.c("UI error: ", message))
- println("init> running without front panel")
+ println("startup> running without front panel")
log.error(util.c("GUI crashed with error ", message))
- log.info("init> running in headless mode without front panel")
+ log.info("startup> running in headless mode without front panel")
end
-- start connection watchdog
diff --git a/rtu/threads.lua b/rtu/threads.lua
index 9dac9f1..fdb82b3 100644
--- a/rtu/threads.lua
+++ b/rtu/threads.lua
@@ -1,6 +1,7 @@
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -82,6 +83,9 @@ function threads.thread__main(smem)
elseif event == "timer" and conn_watchdog.is_timer(param1) then
-- haven't heard from server recently? unlink
rtu_comms.unlink(rtu_state)
+ elseif event == "timer" then
+ -- notify timer callback dispatcher if no other timer case claimed this event
+ tcd.handle(param1)
elseif event == "peripheral_detach" then
-- handle loss of a device
local type, device = ppm.handle_unmount(param1)
@@ -229,9 +233,9 @@ function threads.thread__main(smem)
end
end
end
- elseif event == "mouse_click" then
- -- handle a monitor touch event
- renderer.handle_mouse(core.events.click(param1, param2, param3))
+ elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
+ -- handle a mouse event
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
diff --git a/scada-common/psil.lua b/scada-common/psil.lua
index 664d10d..13dcaa5 100644
--- a/scada-common/psil.lua
+++ b/scada-common/psil.lua
@@ -2,6 +2,8 @@
-- Publisher-Subscriber Interconnect Layer
--
+local util = require("scada-common.util")
+
local psil = {}
-- instantiate a new PSI layer
@@ -36,6 +38,15 @@ function psil.create()
table.insert(self.ic[key].subscribers, { notify = func })
end
+ -- unsubscribe a function from a given key
+ ---@param key string data key
+ ---@param func function function to unsubscribe
+ function public.unsubscribe(key, func)
+ if self.ic[key] ~= nil then
+ util.filter_table(self.ic[key].subscribers, function (s) return s.notify ~= func end)
+ end
+ end
+
-- publish data to a given key, passing it to all subscribers if it has changed
---@param key string data key
---@param value any data value
@@ -64,6 +75,9 @@ function psil.create()
end
end
+ -- clear the contents of the interconnect
+ function public.purge() self.ic = nil end
+
return public
end
diff --git a/scada-common/tcallbackdsp.lua b/scada-common/tcd.lua
similarity index 91%
rename from scada-common/tcallbackdsp.lua
rename to scada-common/tcd.lua
index 3f8f07a..f5ff0e5 100644
--- a/scada-common/tcallbackdsp.lua
+++ b/scada-common/tcd.lua
@@ -5,14 +5,14 @@
local log = require("scada-common.log")
local util = require("scada-common.util")
-local tcallbackdsp = {}
+local tcd = {}
local registry = {}
-- request a function to be called after the specified time
---@param time number seconds
---@param f function callback function
-function tcallbackdsp.dispatch(time, f)
+function tcd.dispatch(time, f)
local timer = util.start_timer(time)
registry[timer] = {
callback = f,
@@ -24,7 +24,7 @@ end
-- request a function to be called after the specified time, aborting any registered instances of that function reference
---@param time number seconds
---@param f function callback function
-function tcallbackdsp.dispatch_unique(time, f)
+function tcd.dispatch_unique(time, f)
-- cancel if already registered
for timer, entry in pairs(registry) do
if entry.callback == f then
@@ -47,7 +47,7 @@ end
-- abort a requested callback
---@param f function callback function
-function tcallbackdsp.abort(f)
+function tcd.abort(f)
for timer, entry in pairs(registry) do
if entry.callback == f then
-- cancel event and remove from registry (even if it fires it won't call)
@@ -59,7 +59,7 @@ end
-- lookup a timer event and execute the callback if found
---@param event integer timer event timer ID
-function tcallbackdsp.handle(event)
+function tcd.handle(event)
if registry[event] ~= nil then
local callback = registry[event].callback
-- clear first so that dispatch_unique call from inside callback won't throw a debug message
@@ -70,7 +70,7 @@ end
-- identify any overdo callbacks
-- prints to log debug output
-function tcallbackdsp.diagnostics()
+function tcd.diagnostics()
for timer, entry in pairs(registry) do
if entry.expiry < util.time_s() then
local overtime = util.time_s() - entry.expiry
@@ -82,4 +82,4 @@ function tcallbackdsp.diagnostics()
end
end
-return tcallbackdsp
+return tcd
diff --git a/scada-common/types.lua b/scada-common/types.lua
index 9beb1e6..8df01c1 100644
--- a/scada-common/types.lua
+++ b/scada-common/types.lua
@@ -39,6 +39,10 @@ function types.new_radiation_reading(r, u) return { radiation = r, unit = u } en
---@return radiation_reading
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end
+---@class coordinate_2d
+---@field x integer
+---@field y integer
+
---@class coordinate
---@field x integer
---@field y integer
diff --git a/supervisor/databus.lua b/supervisor/databus.lua
new file mode 100644
index 0000000..a1e947e
--- /dev/null
+++ b/supervisor/databus.lua
@@ -0,0 +1,174 @@
+--
+-- Data Bus - Central Communication Linking for Supervisor Front Panel
+--
+
+local psil = require("scada-common.psil")
+
+local pgi = require("supervisor.panel.pgi")
+
+local databus = {}
+
+-- databus PSIL
+databus.ps = psil.create()
+
+-- call to toggle heartbeat signal
+function databus.heartbeat() databus.ps.toggle("heartbeat") end
+
+-- transmit firmware versions across the bus
+---@param sv_v string supervisor version
+---@param comms_v string comms version
+function databus.tx_versions(sv_v, comms_v)
+ databus.ps.publish("version", sv_v)
+ databus.ps.publish("comms_version", comms_v)
+end
+
+-- transmit hardware status for modem connection state
+---@param has_modem boolean
+function databus.tx_hw_modem(has_modem)
+ databus.ps.publish("has_modem", has_modem)
+end
+
+-- transmit PLC firmware version and session connection state
+---@param reactor_id integer reactor unit ID
+---@param fw string firmware version
+---@param channel integer PLC remote port
+function databus.tx_plc_connected(reactor_id, fw, channel)
+ databus.ps.publish("plc_" .. reactor_id .. "_fw", fw)
+ databus.ps.publish("plc_" .. reactor_id .. "_conn", true)
+ databus.ps.publish("plc_" .. reactor_id .. "_chan", tostring(channel))
+end
+
+-- transmit PLC disconnected
+---@param reactor_id integer reactor unit ID
+function databus.tx_plc_disconnected(reactor_id)
+ databus.ps.publish("plc_" .. reactor_id .. "_fw", " ------- ")
+ databus.ps.publish("plc_" .. reactor_id .. "_conn", false)
+ databus.ps.publish("plc_" .. reactor_id .. "_chan", " --- ")
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt", 0)
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.lightGray)
+end
+
+-- transmit PLC session RTT
+---@param reactor_id integer reactor unit ID
+---@param rtt integer round trip time
+function databus.tx_plc_rtt(reactor_id, rtt)
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.green)
+ end
+end
+
+-- transmit RTU firmware version and session connection state
+---@param session_id integer RTU session
+---@param fw string firmware version
+---@param channel integer RTU remote port
+function databus.tx_rtu_connected(session_id, fw, channel)
+ databus.ps.publish("rtu_" .. session_id .. "_fw", fw)
+ databus.ps.publish("rtu_" .. session_id .. "_chan", tostring(channel))
+ pgi.create_rtu_entry(session_id)
+end
+
+-- transmit RTU disconnected
+---@param session_id integer RTU session
+function databus.tx_rtu_disconnected(session_id)
+ pgi.delete_rtu_entry(session_id)
+end
+
+-- transmit RTU session RTT
+---@param session_id integer RTU session
+---@param rtt integer round trip time
+function databus.tx_rtu_rtt(session_id, rtt)
+ databus.ps.publish("rtu_" .. session_id .. "_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.green)
+ end
+end
+
+-- transmit RTU session unit count
+---@param session_id integer RTU session
+---@param units integer unit count
+function databus.tx_rtu_units(session_id, units)
+ databus.ps.publish("rtu_" .. session_id .. "_units", units)
+end
+
+-- transmit coordinator firmware version and session connection state
+---@param fw string firmware version
+---@param channel integer coordinator remote port
+function databus.tx_crd_connected(fw, channel)
+ databus.ps.publish("crd_fw", fw)
+ databus.ps.publish("crd_conn", true)
+ databus.ps.publish("crd_chan", tostring(channel))
+end
+
+-- transmit coordinator disconnected
+function databus.tx_crd_disconnected()
+ databus.ps.publish("crd_fw", " ------- ")
+ databus.ps.publish("crd_conn", false)
+ databus.ps.publish("crd_chan", "---")
+ databus.ps.publish("crd_rtt", 0)
+ databus.ps.publish("crd_rtt_color", colors.lightGray)
+end
+
+-- transmit coordinator session RTT
+---@param rtt integer round trip time
+function databus.tx_crd_rtt(rtt)
+ databus.ps.publish("crd_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("crd_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("crd_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("crd_rtt_color", colors.green)
+ end
+end
+
+-- transmit PKT firmware version and PDG session connection state
+---@param session_id integer PDG session
+---@param fw string firmware version
+---@param channel integer PDG remote port
+function databus.tx_pdg_connected(session_id, fw, channel)
+ databus.ps.publish("pdg_" .. session_id .. "_fw", fw)
+ databus.ps.publish("pdg_" .. session_id .. "_chan", tostring(channel))
+ pgi.create_pdg_entry(session_id)
+end
+
+-- transmit PDG session disconnected
+---@param session_id integer PDG session
+function databus.tx_pdg_disconnected(session_id)
+ pgi.delete_pdg_entry(session_id)
+end
+
+-- transmit PDG session RTT
+---@param session_id integer PDG session
+---@param rtt integer round trip time
+function databus.tx_pdg_rtt(session_id, rtt)
+ databus.ps.publish("pdg_" .. session_id .. "_rtt", rtt)
+
+ if rtt > 700 then
+ databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red)
+ elseif rtt > 300 then
+ databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.yellow_hc)
+ else
+ databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.green)
+ end
+end
+
+-- link a function to receive data from the bus
+---@param field string field name
+---@param func function function to link
+function databus.rx_field(field, func)
+ databus.ps.subscribe(field, func)
+end
+
+return databus
diff --git a/supervisor/facility.lua b/supervisor/facility.lua
index f37de26..e3b8059 100644
--- a/supervisor/facility.lua
+++ b/supervisor/facility.lua
@@ -128,7 +128,7 @@ function facility.new(num_reactors, cooling_conf)
for i = 1, #self.prio_defs do
local units = self.prio_defs[i]
for u = 1, #units do
- all_ramped = all_ramped and units[u].a_ramp_complete()
+ all_ramped = all_ramped and units[u].auto_ramp_complete()
end
end
@@ -159,7 +159,7 @@ function facility.new(num_reactors, cooling_conf)
local u = units[id] ---@type reactor_unit
local ctl = u.get_control_inf()
- local lim_br100 = u.a_get_effective_limit()
+ local lim_br100 = u.auto_get_effective_limit()
if abort_on_fault and (lim_br100 ~= ctl.lim_br100) then
-- effective limit differs from set limit, unit is degraded
@@ -183,7 +183,7 @@ function facility.new(num_reactors, cooling_conf)
unallocated = math.max(0, unallocated - ctl.br100)
- if last ~= ctl.br100 then u.a_commit_br100(ramp) end
+ if last ~= ctl.br100 then u.auto_commit_br100(ramp) end
end
end
end
@@ -320,7 +320,7 @@ function facility.new(num_reactors, cooling_conf)
self.start_fail = START_STATUS.BLADE_MISMATCH
end
- if self.start_fail == START_STATUS.OK then u.a_engage() end
+ if self.start_fail == START_STATUS.OK then u.auto_engage() end
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br100 / 100.0)
end
@@ -340,7 +340,7 @@ function facility.new(num_reactors, cooling_conf)
-- use manual SCRAM since inactive was requested, and automatic SCRAM trips an alarm
for _, u in pairs(self.prio_defs[i]) do
u.scram()
- u.a_disengage()
+ u.auto_disengage()
end
end
@@ -601,7 +601,7 @@ function facility.new(num_reactors, cooling_conf)
-- SCRAM all units
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
- u.a_scram()
+ u.auto_scram()
end
end
@@ -653,7 +653,7 @@ function facility.new(num_reactors, cooling_conf)
-- reset PLC RPS trips if we should
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
- u.a_cond_rps_reset()
+ u.auto_cond_rps_reset()
end
end
end
diff --git a/supervisor/panel/components/pdg_entry.lua b/supervisor/panel/components/pdg_entry.lua
new file mode 100644
index 0000000..94cd385
--- /dev/null
+++ b/supervisor/panel/components/pdg_entry.lua
@@ -0,0 +1,48 @@
+--
+-- Pocket Diagnostics Connection Entry
+--
+
+local util = require("scada-common.util")
+
+local databus = require("supervisor.databus")
+
+local core = require("graphics.core")
+
+local Div = require("graphics.elements.div")
+local TextBox = require("graphics.elements.textbox")
+
+local DataIndicator = require("graphics.elements.indicators.data")
+
+local TEXT_ALIGN = core.TEXT_ALIGN
+
+local cpair = core.cpair
+
+-- create a pocket diagnostics list entry
+---@param parent graphics_element parent
+---@param id integer PDG session ID
+local function init(parent, id)
+ -- root div
+ local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
+ local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
+
+ local ps_prefix = "pdg_" .. id .. "_"
+
+ TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ local pdg_chan = TextBox{parent=entry,x=1,y=2,text=" :00000",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
+ TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ pdg_chan.register(databus.ps, ps_prefix .. "chan", function (channel) pdg_chan.set_value(util.sprintf(" :%05d", channel)) end)
+
+ TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
+ local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
+
+ TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
+ local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
+ pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)
+
+ return root
+end
+
+return init
diff --git a/supervisor/panel/components/rtu_entry.lua b/supervisor/panel/components/rtu_entry.lua
new file mode 100644
index 0000000..a4a5a4e
--- /dev/null
+++ b/supervisor/panel/components/rtu_entry.lua
@@ -0,0 +1,52 @@
+--
+-- RTU Connection Entry
+--
+
+local util = require("scada-common.util")
+
+local databus = require("supervisor.databus")
+
+local core = require("graphics.core")
+
+local Div = require("graphics.elements.div")
+local TextBox = require("graphics.elements.textbox")
+
+local DataIndicator = require("graphics.elements.indicators.data")
+
+local TEXT_ALIGN = core.TEXT_ALIGN
+
+local cpair = core.cpair
+
+-- create an RTU list entry
+---@param parent graphics_element parent
+---@param id integer RTU session ID
+local function init(parent, id)
+ -- root div
+ local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
+ local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=cpair(colors.black,colors.white)}
+
+ local ps_prefix = "rtu_" .. id .. "_"
+
+ TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ local rtu_chan = TextBox{parent=entry,x=1,y=2,text=" :00000",alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray),nav_active=cpair(colors.gray,colors.black)}
+ TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ rtu_chan.register(databus.ps, ps_prefix .. "chan", function (channel) rtu_chan.set_value(util.sprintf(" :%05d", channel)) end)
+
+ TextBox{parent=entry,x=10,y=2,text="UNITS:",width=7,height=1}
+ local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=cpair(colors.gray,colors.white)}
+ unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
+
+ TextBox{parent=entry,x=21,y=2,text="FW:",width=3,height=1}
+ local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
+
+ TextBox{parent=entry,x=36,y=2,text="RTT:",width=4,height=1}
+ local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
+ rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)
+
+ return root
+end
+
+return init
diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua
new file mode 100644
index 0000000..8cdcf76
--- /dev/null
+++ b/supervisor/panel/front_panel.lua
@@ -0,0 +1,160 @@
+--
+-- Main SCADA Coordinator GUI
+--
+
+local util = require("scada-common.util")
+
+local config = require("supervisor.config")
+local databus = require("supervisor.databus")
+
+local pgi = require("supervisor.panel.pgi")
+local style = require("supervisor.panel.style")
+
+local pdg_entry = require("supervisor.panel.components.pdg_entry")
+local rtu_entry = require("supervisor.panel.components.rtu_entry")
+
+local core = require("graphics.core")
+
+local Div = require("graphics.elements.div")
+local ListBox = require("graphics.elements.listbox")
+local MultiPane = require("graphics.elements.multipane")
+local TextBox = require("graphics.elements.textbox")
+
+local TabBar = require("graphics.elements.controls.tabbar")
+
+local LED = require("graphics.elements.indicators.led")
+local DataIndicator = require("graphics.elements.indicators.data")
+
+local TEXT_ALIGN = core.TEXT_ALIGN
+
+local cpair = core.cpair
+
+-- create new main view
+---@param panel graphics_element main displaybox
+local function init(panel)
+ TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
+
+ local page_div = Div{parent=panel,x=1,y=3}
+
+ --
+ -- system indicators
+ --
+
+ local main_page = Div{parent=page_div,x=1,y=1}
+
+ local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
+
+ local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
+ local heartbeat = LED{parent=system,label="HEARTBEAT",colors=cpair(colors.green,colors.green_off)}
+ on.update(true)
+ system.line_break()
+
+ heartbeat.register(databus.ps, "heartbeat", heartbeat.update)
+
+ local modem = LED{parent=system,label="MODEM",colors=cpair(colors.green,colors.green_off)}
+ system.line_break()
+
+ modem.register(databus.ps, "has_modem", modem.update)
+
+ --
+ -- about footer
+ --
+
+ local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=cpair(colors.lightGray,colors.ivory)}
+ local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
+ local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=TEXT_ALIGN.LEFT,height=1}
+
+ fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
+ comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
+
+ --
+ -- page handling
+ --
+
+ -- plc page
+
+ local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
+
+ for i = 1, config.NUM_REACTORS do
+ local ps_prefix = "plc_" .. i .. "_"
+ local plc_entry = Div{parent=plc_list,height=3,fg_bg=cpair(colors.black,colors.white)}
+
+ TextBox{parent=plc_entry,x=1,y=1,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ TextBox{parent=plc_entry,x=1,y=2,text="UNIT "..i,alignment=TEXT_ALIGN.CENTER,width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+ TextBox{parent=plc_entry,x=1,y=3,text="",width=8,height=1,fg_bg=cpair(colors.black,colors.lightGray)}
+
+ local conn = LED{parent=plc_entry,x=10,y=2,label="CONN",colors=cpair(colors.green,colors.green_off)}
+ conn.register(databus.ps, ps_prefix .. "conn", conn.update)
+
+ local plc_chan = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)}
+ plc_chan.register(databus.ps, ps_prefix .. "chan", plc_chan.set_value)
+
+ TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3,height=1}
+ local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
+
+ TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4,height=1}
+ local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
+ plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
+
+ plc_list.line_break()
+ end
+
+ -- rtu page
+
+ local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
+ local _ = Div{parent=rtu_list,height=1,hidden=true} -- padding
+
+ -- coordinator page
+
+ local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=cpair(colors.black,colors.white)}
+
+ local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)}
+ crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
+
+ TextBox{parent=crd_box,x=4,y=3,text="CHANNEL ",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)}
+ local crd_chan = TextBox{parent=crd_box,x=12,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)}
+ crd_chan.register(databus.ps, "crd_chan", crd_chan.set_value)
+
+ TextBox{parent=crd_box,x=22,y=2,text="FW:",width=3,height=1}
+ local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
+
+ TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4,height=1}
+ local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=cpair(colors.lightGray,colors.white)}
+ TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,height=1,fg_bg=cpair(colors.lightGray,colors.white)}
+ crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
+ crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
+
+ -- pocket diagnostics page
+
+ local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
+ local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
+ local _ = Div{parent=pdg_list,height=1,hidden=true} -- padding
+
+ -- assemble page panes
+
+ local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page }
+
+ local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
+
+ local tabs = {
+ { name = "SVR", color = cpair(colors.black, colors.ivory) },
+ { name = "PLC", color = cpair(colors.black, colors.ivory) },
+ { name = "RTU", color = cpair(colors.black, colors.ivory) },
+ { name = "CRD", color = cpair(colors.black, colors.ivory) },
+ { name = "PKT", color = cpair(colors.black, colors.ivory) },
+ }
+
+ TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=cpair(colors.black,colors.white)}
+
+ -- link RTU/PDG list management to PGI
+ pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
+end
+
+return init
diff --git a/supervisor/panel/pgi.lua b/supervisor/panel/pgi.lua
new file mode 100644
index 0000000..9065f72
--- /dev/null
+++ b/supervisor/panel/pgi.lua
@@ -0,0 +1,93 @@
+--
+-- Protected Graphics Interface
+--
+
+local log = require("scada-common.log")
+local util = require("scada-common.util")
+
+local pgi = {}
+
+local data = {
+ rtu_list = nil, ---@type nil|graphics_element
+ pdg_list = nil, ---@type nil|graphics_element
+ rtu_entry = nil, ---@type function
+ pdg_entry = nil, ---@type function
+ -- session entries
+ s_entries = { rtu = {}, pdg = {} }
+}
+
+-- link list boxes
+---@param rtu_list graphics_element RTU list element
+---@param rtu_entry function RTU entry constructor
+---@param pdg_list graphics_element pocket diagnostics list element
+---@param pdg_entry function pocket diagnostics entry constructor
+function pgi.link_elements(rtu_list, rtu_entry, pdg_list, pdg_entry)
+ data.rtu_list = rtu_list
+ data.pdg_list = pdg_list
+ data.rtu_entry = rtu_entry
+ data.pdg_entry = pdg_entry
+end
+
+-- unlink all fields, disabling the PGI
+function pgi.unlink()
+ data.rtu_list = nil
+ data.pdg_list = nil
+ data.rtu_entry = nil
+ data.pdg_entry = nil
+end
+
+-- add an RTU entry to the RTU list
+---@param session_id integer RTU session
+function pgi.create_rtu_entry(session_id)
+ if data.rtu_list ~= nil and data.rtu_entry ~= nil then
+ local success, result = pcall(data.rtu_entry, data.rtu_list, session_id)
+
+ if success then
+ data.s_entries.rtu[session_id] = result
+ else
+ log.error(util.c("PGI: failed to create RTU entry (", result, ")"), true)
+ end
+ end
+end
+
+-- delete an RTU entry from the RTU list
+---@param session_id integer RTU session
+function pgi.delete_rtu_entry(session_id)
+ if data.s_entries.rtu[session_id] ~= nil then
+ local success, result = pcall(data.s_entries.rtu[session_id].delete)
+ data.s_entries.rtu[session_id] = nil
+
+ if not success then
+ log.error(util.c("PGI: failed to delete RTU entry (", result, ")"), true)
+ end
+ end
+end
+
+-- add a PDG entry to the PDG list
+---@param session_id integer pocket diagnostics session
+function pgi.create_pdg_entry(session_id)
+ if data.pdg_list ~= nil and data.pdg_entry ~= nil then
+ local success, result = pcall(data.pdg_entry, data.pdg_list, session_id)
+
+ if success then
+ data.s_entries.pdg[session_id] = result
+ else
+ log.error(util.c("PGI: failed to create PDG entry (", result, ")"), true)
+ end
+ end
+end
+
+-- delete a PDG entry from the PDG list
+---@param session_id integer pocket diagnostics session
+function pgi.delete_pdg_entry(session_id)
+ if data.s_entries.pdg[session_id] ~= nil then
+ local success, result = pcall(data.s_entries.pdg[session_id].delete)
+ data.s_entries.pdg[session_id] = nil
+
+ if not success then
+ log.error(util.c("PGI: failed to delete PDG entry (", result, ")"), true)
+ end
+ end
+end
+
+return pgi
diff --git a/supervisor/panel/style.lua b/supervisor/panel/style.lua
new file mode 100644
index 0000000..0668ccc
--- /dev/null
+++ b/supervisor/panel/style.lua
@@ -0,0 +1,42 @@
+--
+-- Graphics Style Options
+--
+
+local core = require("graphics.core")
+
+local style = {}
+
+local cpair = core.cpair
+
+-- GLOBAL --
+
+-- remap global colors
+colors.ivory = colors.pink
+colors.yellow_hc = colors.purple
+colors.red_off = colors.brown
+colors.yellow_off = colors.magenta
+colors.green_off = colors.lime
+
+style.root = cpair(colors.black, colors.ivory)
+style.header = cpair(colors.black, colors.lightGray)
+
+style.colors = {
+ { c = colors.red, hex = 0xdf4949 }, -- RED ON
+ { c = colors.orange, hex = 0xffb659 },
+ { c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
+ { c = colors.lime, hex = 0x16665a }, -- GREEN OFF
+ { c = colors.green, hex = 0x6be551 }, -- GREEN ON
+ { c = colors.cyan, hex = 0x34bac8 },
+ { c = colors.lightBlue, hex = 0x6cc0f2 },
+ { c = colors.blue, hex = 0x0008fe }, -- LCD BLUE
+ { c = colors.purple, hex = 0xe3bc2a }, -- YELLOW HIGH CONTRAST
+ { c = colors.pink, hex = 0xdcd9ca }, -- IVORY
+ { c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
+ -- { c = colors.white, hex = 0xdcd9ca },
+ { c = colors.lightGray, hex = 0xb1b8b3 },
+ { c = colors.gray, hex = 0x575757 },
+ -- { c = colors.black, hex = 0x191919 },
+ { c = colors.brown, hex = 0x672223 } -- RED OFF
+}
+
+return style
diff --git a/supervisor/renderer.lua b/supervisor/renderer.lua
new file mode 100644
index 0000000..1bc70a4
--- /dev/null
+++ b/supervisor/renderer.lua
@@ -0,0 +1,84 @@
+--
+-- Graphics Rendering Control
+--
+
+local panel_view = require("supervisor.panel.front_panel")
+local pgi = require("supervisor.panel.pgi")
+local style = require("supervisor.panel.style")
+
+local flasher = require("graphics.flasher")
+
+local DisplayBox = require("graphics.elements.displaybox")
+
+local renderer = {}
+
+local ui = {
+ display = nil
+}
+
+-- start the UI
+function renderer.start_ui()
+ if ui.display == nil then
+ -- reset terminal
+ term.setTextColor(colors.white)
+ term.setBackgroundColor(colors.black)
+ term.clear()
+ term.setCursorPos(1, 1)
+
+ -- set overridden colors
+ for i = 1, #style.colors do
+ term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
+ end
+
+ -- init front panel view
+ ui.display = DisplayBox{window=term.current(),fg_bg=style.root}
+ panel_view(ui.display)
+
+ -- start flasher callback task
+ flasher.run()
+ end
+end
+
+-- close out the UI
+function renderer.close_ui()
+ if ui.display ~= nil then
+ -- stop blinking indicators
+ flasher.clear()
+
+ -- disable PGI
+ pgi.unlink()
+
+ -- hide to stop animation callbacks
+ ui.display.hide()
+
+ -- clear root UI elements
+ ui.display = nil
+
+ -- restore colors
+ for i = 1, #style.colors do
+ local r, g, b = term.nativePaletteColor(style.colors[i].c)
+ term.setPaletteColor(style.colors[i].c, r, g, b)
+ end
+
+ -- reset terminal
+ term.setTextColor(colors.white)
+ term.setBackgroundColor(colors.black)
+ term.clear()
+ term.setCursorPos(1, 1)
+ end
+end
+
+-- is the UI ready?
+---@nodiscard
+---@return boolean ready
+function renderer.ui_ready() return ui.display ~= nil end
+
+-- handle a mouse event
+---@param event mouse_interaction|nil
+function renderer.handle_mouse(event)
+ if ui.display ~= nil and event ~= nil then
+ ui.display.handle_mouse(event)
+ end
+end
+
+return renderer
diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua
index b093d87..36a5241 100644
--- a/supervisor/session/coordinator.lua
+++ b/supervisor/session/coordinator.lua
@@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local databus = require("supervisor.databus")
+
local svqtypes = require("supervisor.session.svqtypes")
local coordinator = {}
@@ -18,8 +20,6 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local SV_Q_DATA = svqtypes.SV_Q_DATA
-local println = util.println
-
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
@@ -49,7 +49,11 @@ local PERIODICS = {
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param facility facility facility data table
-function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
+---@param fp_ok boolean if the front panel UI is running
+function coordinator.new_session(id, in_queue, out_queue, timeout, facility, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local log_header = "crdn_session(" .. id .. "): "
local self = {
@@ -84,6 +88,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_crd_disconnected()
end
-- send a CRDN packet
@@ -205,6 +210,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
-- log.debug(log_header .. "COORD RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "COORD TT = " .. (srv_now - coord_send) .. "ms")
+
+ databus.tx_crd_rtt(self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua
index cca8a26..1436534 100644
--- a/supervisor/session/plc.lua
+++ b/supervisor/session/plc.lua
@@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local databus = require("supervisor.databus")
+
local svqtypes = require("supervisor.session.svqtypes")
local plc = {}
@@ -14,8 +16,6 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
local UNIT_COMMAND = comms.UNIT_COMMAND
-local println = util.println
-
-- retry time constants in ms
local INITIAL_WAIT = 1500
local INITIAL_AUTO_WAIT = 1000
@@ -49,7 +49,11 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
-function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
+---@param fp_ok boolean if the front panel UI is running
+function plc.new_session(id, reactor_id, in_queue, out_queue, timeout, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local log_header = "plc_session(" .. id .. "): "
local self = {
@@ -235,6 +239,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_plc_disconnected(reactor_id)
end
-- send an RPLC packet
@@ -485,6 +490,8 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
-- log.debug(log_header .. "PLC RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "PLC TT = " .. (srv_now - plc_send) .. "ms")
+
+ databus.tx_plc_rtt(reactor_id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua
index 9cf0a5d..ba6d179 100644
--- a/supervisor/session/pocket.lua
+++ b/supervisor/session/pocket.lua
@@ -1,15 +1,14 @@
-local comms = require("scada-common.comms")
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local util = require("scada-common.util")
+local comms = require("scada-common.comms")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local util = require("scada-common.util")
+local databus = require("supervisor.databus")
local pocket = {}
local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
-local println = util.println
-
-- retry time constants in ms
-- local INITIAL_WAIT = 1500
-- local RETRY_PERIOD = 1000
@@ -33,8 +32,12 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
-function pocket.new_session(id, in_queue, out_queue, timeout)
- local log_header = "diag_session(" .. id .. "): "
+---@param fp_ok boolean if the front panel UI is running
+function pocket.new_session(id, in_queue, out_queue, timeout, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
+ local log_header = "pdg_session(" .. id .. "): "
local self = {
-- connection properties
@@ -55,18 +58,19 @@ function pocket.new_session(id, in_queue, out_queue, timeout)
acks = {
},
-- session database
- ---@class diag_db
+ ---@class pdg_db
sDB = {
}
}
- ---@class diag_session
+ ---@class pdg_session
local public = {}
-- mark this diagnostics session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_pdg_disconnected(id)
end
-- send a SCADA management packet
@@ -106,16 +110,18 @@ function pocket.new_session(id, in_queue, out_queue, timeout)
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
- -- local diag_send = pkt.data[2]
+ -- local pdg_send = pkt.data[2]
local srv_now = util.time()
self.last_rtt = srv_now - srv_start
if self.last_rtt > 750 then
- log.warning(log_header .. "DIAG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
+ log.warning(log_header .. "PDG KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
end
- -- log.debug(log_header .. "DIAG RTT = " .. self.last_rtt .. "ms")
- -- log.debug(log_header .. "DIAG TT = " .. (srv_now - diag_send) .. "ms")
+ -- log.debug(log_header .. "PDG RTT = " .. self.last_rtt .. "ms")
+ -- log.debug(log_header .. "PDG TT = " .. (srv_now - pdg_send) .. "ms")
+
+ databus.tx_pdg_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua
index 13460f6..2f7091c 100644
--- a/supervisor/session/rtu.lua
+++ b/supervisor/session/rtu.lua
@@ -4,6 +4,8 @@ local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local databus = require("supervisor.databus")
+
local svqtypes = require("supervisor.session.svqtypes")
-- supervisor rtu sessions (svrs)
@@ -22,8 +24,6 @@ local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
-local println = util.println
-
local PERIODICS = {
KEEP_ALIVE = 2000
}
@@ -36,7 +36,11 @@ local PERIODICS = {
---@param timeout number communications timeout
---@param advertisement table RTU device advertisement
---@param facility facility facility data table
-function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facility)
+---@param fp_ok boolean if the front panel UI is running
+function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facility, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
local log_header = "rtu_session(" .. id .. "): "
local self = {
@@ -66,6 +70,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- parse the recorded advertisement and create unit sub-sessions
local function _handle_advertisement()
+ local unit_count = 0
+
_reset_config()
for i = 1, #self.fac_units do
@@ -171,24 +177,26 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
end
if unit ~= nil then
- table.insert(self.units, unit)
+ self.units[i] = unit
+ unit_count = unit_count + 1
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
_reset_config()
log.error(util.c(log_header, "bad advertisement: error occured while creating a unit (type is ", type_string, ")"))
break
end
end
+
+ databus.tx_rtu_units(id, unit_count)
end
-- mark this RTU session as closed, stop watchdog
local function _close()
self.conn_watchdog.cancel()
self.connected = false
+ databus.tx_rtu_disconnected(id)
-- mark all RTU unit sessions as closed so the reactor unit knows
- for i = 1, #self.units do
- self.units[i].close()
- end
+ for _, unit in pairs(self.units) do unit.close() end
end
-- send a MODBUS packet
@@ -256,6 +264,8 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- log.debug(log_header .. "RTU RTT = " .. self.last_rtt .. "ms")
-- log.debug(log_header .. "RTU TT = " .. (srv_now - rtu_send) .. "ms")
+
+ databus.tx_rtu_rtt(id, self.last_rtt)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
@@ -351,9 +361,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
local time_now = util.time()
- for i = 1, #self.units do
- self.units[i].update(time_now)
- end
+ for _, unit in pairs(self.units) do unit.update(time_now) end
----------------------
-- update periodics --
diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua
index 60db434..a534142 100644
--- a/supervisor/session/svsessions.lua
+++ b/supervisor/session/svsessions.lua
@@ -3,6 +3,7 @@ local mqueue = require("scada-common.mqueue")
local util = require("scada-common.util")
local config = require("supervisor.config")
+local databus = require("supervisor.databus")
local facility = require("supervisor.facility")
local svqtypes = require("supervisor.session.svqtypes")
@@ -22,24 +23,26 @@ local CRD_S_DATA = coordinator.CRD_S_DATA
local svsessions = {}
+---@enum SESSION_TYPE
local SESSION_TYPE = {
RTU_SESSION = 0, -- RTU gateway
PLC_SESSION = 1, -- reactor PLC
COORD_SESSION = 2, -- coordinator
- DIAG_SESSION = 3 -- pocket diagnostics
+ PDG_SESSION = 3 -- pocket diagnostics
}
svsessions.SESSION_TYPE = SESSION_TYPE
local self = {
modem = nil, ---@type table|nil
+ fp_ok = false,
num_reactors = 0,
facility = nil, ---@type facility|nil
- sessions = { rtu = {}, plc = {}, coord = {}, diag = {} },
- next_ids = { rtu = 0, plc = 0, coord = 0, diag = 0 }
+ sessions = { rtu = {}, plc = {}, coord = {}, pdg = {} },
+ next_ids = { rtu = 0, plc = 0, coord = 0, pdg = 0 }
}
----@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|diag_session_struct
+---@alias sv_session_structs plc_session_struct|rtu_session_struct|coord_session_struct|pdg_session_struct
-- PRIVATE FUNCTIONS --
@@ -194,11 +197,13 @@ end
-- PUBLIC FUNCTIONS --
-- initialize svsessions
----@param modem table
----@param num_reactors integer
----@param cooling_conf table
-function svsessions.init(modem, num_reactors, cooling_conf)
+---@param modem table modem device
+---@param fp_ok boolean front panel active
+---@param num_reactors integer number of reactors
+---@param cooling_conf table cooling configuration definition
+function svsessions.init(modem, fp_ok, num_reactors, cooling_conf)
self.modem = modem
+ self.fp_ok = fp_ok
self.num_reactors = num_reactors
self.facility = facility.new(num_reactors, cooling_conf)
end
@@ -245,12 +250,11 @@ end
-- find a pocket diagnostics session by the remote port
---@nodiscard
---@param remote_port integer
----@return diag_session_struct|nil
+---@return pdg_session_struct|nil
function svsessions.find_pdg_session(remote_port)
-- check diagnostic sessions
local session = _find_session(self.sessions.diag, remote_port)
- ---@cast session diag_session_struct|nil
-
+ ---@cast session pdg_session_struct|nil
return session
end
@@ -299,13 +303,17 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
instance = nil ---@type plc_session
}
- plc_s.instance = plc.new_session(self.next_ids.plc, for_reactor, plc_s.in_queue, plc_s.out_queue, config.PLC_TIMEOUT)
+ plc_s.instance = plc.new_session(self.next_ids.plc, for_reactor, plc_s.in_queue, plc_s.out_queue,
+ config.PLC_TIMEOUT, self.fp_ok)
table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units()
units[for_reactor].link_plc_session(plc_s)
- log.debug(util.c("established new PLC session to ", remote_port, " with ID ", self.next_ids.plc, " for reactor ", for_reactor))
+ log.debug(util.c("established new PLC session to ", remote_port, " with ID ", self.next_ids.plc,
+ " for reactor ", for_reactor))
+
+ databus.tx_plc_connected(for_reactor, version, remote_port)
self.next_ids.plc = self.next_ids.plc + 1
@@ -337,11 +345,14 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement
instance = nil ---@type rtu_session
}
- rtu_s.instance = rtu.new_session(self.next_ids.rtu, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement, self.facility)
+ rtu_s.instance = rtu.new_session(self.next_ids.rtu, rtu_s.in_queue, rtu_s.out_queue, config.RTU_TIMEOUT, advertisement,
+ self.facility, self.fp_ok)
table.insert(self.sessions.rtu, rtu_s)
log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_ids.rtu)
+ databus.tx_rtu_connected(self.next_ids.rtu, version, remote_port)
+
self.next_ids.rtu = self.next_ids.rtu + 1
-- success
@@ -368,11 +379,14 @@ function svsessions.establish_coord_session(local_port, remote_port, version)
instance = nil ---@type coord_session
}
- coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT, self.facility)
+ coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT,
+ self.facility, self.fp_ok)
table.insert(self.sessions.coord, coord_s)
log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_ids.coord)
+ databus.tx_crd_connected(version, remote_port)
+
self.next_ids.coord = self.next_ids.coord + 1
-- success
@@ -389,9 +403,9 @@ end
---@param remote_port integer
---@param version string
---@return integer|false session_id
-function svsessions.establish_diag_session(local_port, remote_port, version)
- ---@class diag_session_struct
- local diag_s = {
+function svsessions.establish_pdg_session(local_port, remote_port, version)
+ ---@class pdg_session_struct
+ local pdg_s = {
s_type = "pkt",
open = true,
version = version,
@@ -399,18 +413,20 @@ function svsessions.establish_diag_session(local_port, remote_port, version)
r_port = remote_port,
in_queue = mqueue.new(),
out_queue = mqueue.new(),
- instance = nil ---@type diag_session
+ instance = nil ---@type pdg_session
}
- diag_s.instance = pocket.new_session(self.next_ids.diag, diag_s.in_queue, diag_s.out_queue, config.PKT_TIMEOUT)
- table.insert(self.sessions.diag, diag_s)
+ pdg_s.instance = pocket.new_session(self.next_ids.pdg, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok)
+ table.insert(self.sessions.pdg, pdg_s)
- log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.diag)
+ log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.pdg)
- self.next_ids.diag = self.next_ids.diag + 1
+ databus.tx_pdg_connected(self.next_ids.pdg, version, remote_port)
+
+ self.next_ids.pdg = self.next_ids.pdg + 1
-- success
- return diag_s.instance.get_id()
+ return pdg_s.instance.get_id()
end
-- attempt to identify which session's watchdog timer fired
diff --git a/supervisor/startup.lua b/supervisor/startup.lua
index 156a8ec..7d56895 100644
--- a/supervisor/startup.lua
+++ b/supervisor/startup.lua
@@ -5,16 +5,22 @@
require("/initenv").init_env()
local crash = require("scada-common.crash")
+local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcd")
local util = require("scada-common.util")
+local core = require("graphics.core")
+
local config = require("supervisor.config")
+local databus = require("supervisor.databus")
+local renderer = require("supervisor.renderer")
local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
-local SUPERVISOR_VERSION = "v0.16.0"
+local SUPERVISOR_VERSION = "v0.17.0"
local println = util.println
local println_ts = util.println_ts
@@ -43,7 +49,6 @@ cfv.assert_type_int(config.NUM_REACTORS)
cfv.assert_type_table(config.REACTOR_COOLING)
cfv.assert_type_str(config.LOG_PATH)
cfv.assert_type_int(config.LOG_MODE)
-cfv.assert_type_bool(config.LOG_DEBUG)
assert(cfv.valid(), "bad config file: missing/invalid fields")
@@ -65,7 +70,7 @@ end
-- log init
----------------------------------------
-log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG)
+log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
log.info("========================================")
log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION)
@@ -83,6 +88,9 @@ local function main()
-- startup
----------------------------------------
+ -- record firmware versions and ID
+ databus.tx_versions(SUPERVISOR_VERSION, comms.version)
+
-- mount connected devices
ppm.mount_all()
@@ -93,7 +101,20 @@ local function main()
return
end
- -- start comms
+ databus.tx_hw_modem(true)
+
+ -- start UI
+ local fp_ok, message = pcall(renderer.start_ui)
+
+ if not fp_ok then
+ renderer.close_ui()
+ println_ts(util.c("UI error: ", message))
+ log.error(util.c("GUI crashed with error ", message))
+ else
+ -- redefine println_ts local to not print as we have the front panel running
+ println_ts = function (_) end
+ end
+
---@class sv_channel_list
local channels = {
SVR = config.SVR_CHANNEL,
@@ -102,8 +123,10 @@ local function main()
CRD = config.CRD_CHANNEL,
PKT = config.PKT_CHANNEL
}
+
+ -- start comms
local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem,
- channels, config.TRUSTED_RANGE)
+ channels, config.TRUSTED_RANGE, fp_ok)
-- base loop clock (6.67Hz, 3 ticks)
local MAIN_CLOCK = 0.15
@@ -112,6 +135,9 @@ local function main()
-- start clock
loop_clock.start()
+ -- halve the rate heartbeat LED flash
+ local heartbeat_toggle = true
+
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -126,6 +152,7 @@ local function main()
if device == modem then
println_ts("wireless modem disconnected!")
log.warning("comms modem disconnected")
+ databus.tx_hw_modem(false)
else
log.warning("non-comms modem disconnected")
end
@@ -143,6 +170,8 @@ local function main()
println_ts("wireless modem reconnected.")
log.info("comms modem reconnected")
+
+ databus.tx_hw_modem(true)
else
log.info("wired modem reconnected")
end
@@ -151,6 +180,9 @@ local function main()
elseif event == "timer" and loop_clock.is_clock(param1) then
-- main loop tick
+ if heartbeat_toggle then databus.heartbeat() end
+ heartbeat_toggle = not heartbeat_toggle
+
-- iterate sessions
svsessions.iterate_all()
@@ -161,10 +193,16 @@ local function main()
elseif event == "timer" then
-- a non-clock timer event, check watchdogs
svsessions.check_all_watchdogs(param1)
+
+ -- notify timer callback dispatcher
+ tcd.handle(param1)
elseif event == "modem_message" then
-- got a packet
local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5)
superv_comms.handle_packet(packet)
+ elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
+ -- handle a mouse event
+ renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
end
-- check for termination request
@@ -177,8 +215,15 @@ local function main()
end
end
- println_ts("exited")
+ renderer.close_ui()
+
+ util.println_ts("exited")
log.info("exited")
end
-if not xpcall(main, crash.handler) then crash.exit() else log.close() end
+if not xpcall(main, crash.handler) then
+ pcall(renderer.close_ui)
+ crash.exit()
+else
+ log.close()
+end
diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua
index d467b97..5a3ec8c 100644
--- a/supervisor/supervisor.lua
+++ b/supervisor/supervisor.lua
@@ -11,8 +11,6 @@ local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
-local println = util.println
-
-- supervisory controller communications
---@nodiscard
---@param _version string supervisor version
@@ -21,8 +19,13 @@ local println = util.println
---@param modem table modem device
---@param channels sv_channel_list network channels
---@param range integer trusted device connection range
+---@param fp_ok boolean if the front panel UI is running
---@diagnostic disable-next-line: unused-local
-function supervisor.comms(_version, num_reactors, cooling_conf, modem, channels, range)
+function supervisor.comms(_version, num_reactors, cooling_conf, modem, channels, range, fp_ok)
+ -- print a log message to the terminal as long as the UI isn't running
+ local function println(message) if not fp_ok then util.println_ts(message) end end
+
+ -- channel list
local svr_channel = channels.SVR
local plc_channel = channels.PLC
local rtu_channel = channels.RTU
@@ -45,8 +48,8 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, channels,
_conf_channels()
- -- link modem to svsessions
- svsessions.init(modem, num_reactors, cooling_conf)
+ -- pass modem, status, and config data to svsessions
+ svsessions.init(modem, fp_ok, num_reactors, cooling_conf)
-- send an establish request response
---@param packet scada_packet
@@ -325,7 +328,7 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, channels,
end
elseif dev_type == DEVICE_TYPE.PKT then
-- this is an attempt to establish a new pocket diagnostic session
- local s_id = svsessions.establish_diag_session(l_chan, r_chan, firmware_v)
+ local s_id = svsessions.establish_pdg_session(l_chan, r_chan, firmware_v)
println(util.c("PKT (", firmware_v, ") [:", r_chan, "] \xbb connected"))
log.info(util.c("SVCTL_ESTABLISH: pocket (", firmware_v, ") [:", r_chan, "] connected with session ID ", s_id))
diff --git a/supervisor/unit.lua b/supervisor/unit.lua
index 0cccc48..0d7246a 100644
--- a/supervisor/unit.lua
+++ b/supervisor/unit.lua
@@ -506,7 +506,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
--#region
-- engage automatic control
- function public.a_engage()
+ function public.auto_engage()
self.auto_engaged = true
if self.plc_i ~= nil then
self.plc_i.auto_lock(true)
@@ -514,7 +514,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end
-- disengage automatic control
- function public.a_disengage()
+ function public.auto_disengage()
self.auto_engaged = false
if self.plc_i ~= nil then
self.plc_i.auto_lock(false)
@@ -526,7 +526,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- if it is degraded or not ready, the limit will be 0
---@nodiscard
---@return integer lim_br100
- function public.a_get_effective_limit()
+ function public.auto_get_effective_limit()
if (not self.db.control.ready) or self.db.control.degraded or self.plc_cache.rps_trip then
self.db.control.br100 = 0
return 0
@@ -537,7 +537,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- set the automatic burn rate based on the last set burn rate in 100ths
---@param ramp boolean true to ramp to rate, false to set right away
- function public.a_commit_br100(ramp)
+ function public.auto_commit_br100(ramp)
if self.auto_engaged then
if self.plc_i ~= nil then
self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp)
@@ -550,16 +550,16 @@ function unit.new(reactor_id, num_boilers, num_turbines)
-- check if ramping is complete (burn rate is same as target)
---@nodiscard
---@return boolean complete
- function public.a_ramp_complete()
+ function public.auto_ramp_complete()
if self.plc_i ~= nil then
return self.plc_i.is_ramp_complete() or
(self.plc_i.get_status().act_burn_rate == 0 and self.db.control.br100 == 0) or
- public.a_get_effective_limit() == 0
+ public.auto_get_effective_limit() == 0
else return true end
end
-- perform an automatic SCRAM
- function public.a_scram()
+ function public.auto_scram()
if self.plc_s ~= nil then
self.db.control.br100 = 0
self.plc_s.in_queue.push_command(PLC_S_CMDS.ASCRAM)
@@ -567,7 +567,7 @@ function unit.new(reactor_id, num_boilers, num_turbines)
end
-- queue a command to clear timeout/auto-scram if set
- function public.a_cond_rps_reset()
+ function public.auto_cond_rps_reset()
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then
local rps = self.plc_i.get_rps()
if rps.timeout or rps.automatic then
diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua
index ca97181..fab67f3 100644
--- a/supervisor/unitlogic.lua
+++ b/supervisor/unitlogic.lua
@@ -549,7 +549,7 @@ function logic.update_auto_safety(public, self)
end
if alarmed and not self.plc_cache.rps_status.automatic then
- public.a_scram()
+ public.auto_scram()
end
self.auto_was_alarmed = alarmed