diff --git a/.github/workflows/manifest.yml b/.github/workflows/manifest.yml new file mode 100644 index 0000000..73586d1 --- /dev/null +++ b/.github/workflows/manifest.yml @@ -0,0 +1,95 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy Installation Manifests and Versions + +on: + workflow_dispatch: + push: + branches: + - main + - latest + - devel + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Setup Python + uses: actions/setup-python@v3.1.3 + # Generate manifest + shields files for main branch + - name: Checkout main + id: checkout-main + uses: actions/checkout@v3 + with: + ref: 'main' + clean: false + - name: Create outputs folders + if: success() || failure() + shell: bash + run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/latest deploy/manifests/devel + - name: Generate manifest and shields for main branch + id: manifest-main + if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }} + run: python imgen.py shields + - name: Save main's manifest + if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }} + run: mv install_manifest.json deploy/manifests/main + # Generate manifest for latest branch + - name: Checkout latest + id: checkout-latest + if: success() || failure() + uses: actions/checkout@v3 + with: + ref: 'latest' + clean: false + - name: Generate manifest for latest + id: manifest-latest + if: ${{ (success() || failure()) && steps.checkout-latest.outcome == 'success' }} + run: python imgen.py + - name: Save latest's manifest + if: ${{ (success() || failure()) && steps.manifest-latest.outcome == 'success' }} + run: mv install_manifest.json deploy/manifests/latest + # Generate manifest for devel branch + - name: Checkout devel + id: checkout-devel + if: success() || failure() + uses: actions/checkout@v3 + with: + ref: 'devel' + clean: false + - name: Generate manifest for devel + id: manifest-devel + if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }} + run: python imgen.py + - name: Save devel's manifest + if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }} + run: mv install_manifest.json deploy/manifests/devel + # All artifacts ready now, upload deploy directory + - name: Upload artifacts + id: upload-artifacts + if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }} + uses: actions/upload-pages-artifact@v1 + with: + # Upload manifest JSON + path: 'deploy/' + - name: Deploy to GitHub Pages + if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }} + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.github/workflows/shields.yml b/.github/workflows/shields.yml deleted file mode 100644 index 50698af..0000000 --- a/.github/workflows/shields.yml +++ /dev/null @@ -1,47 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy Component 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: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - 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 - uses: actions/upload-pages-artifact@v1 - with: - # Upload shields JSON - path: 'shields/' - - 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/config.lua b/coordinator/config.lua index 3196e80..ecb7599 100644 --- a/coordinator/config.lua +++ b/coordinator/config.lua @@ -1,11 +1,11 @@ local config = {} --- port of the SCADA supervisor -config.SCADA_SV_PORT = 16100 --- port to listen to incoming packets from supervisor -config.SCADA_SV_CTL_LISTEN = 16101 --- listen port for SCADA coordinator API access -config.SCADA_API_LISTEN = 16200 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- coordinator comms channel +config.CRD_CHANNEL = 16243 +-- pocket comms channel +config.PKT_CHANNEL = 16244 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index d7e58f7..f6e8038 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -213,14 +213,15 @@ end ---@nodiscard ---@param version string coordinator version ---@param modem table modem device ----@param sv_port integer port of configured supervisor ----@param sv_listen integer listening port for supervisor replys ----@param api_listen integer listening port for pocket API +---@param crd_channel integer port of configured supervisor +---@param svr_channel integer listening port for supervisor replys +---@param pkt_channel integer listening port for pocket API ---@param range integer trusted device connection range ---@param sv_watchdog watchdog -function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range, sv_watchdog) +function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) local self = { sv_linked = false, + sv_addr = comms.BROADCAST, sv_seq_num = 0, sv_r_seq_num = nil, sv_config_err = false, @@ -236,8 +237,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(sv_listen) - modem.open(api_listen) + modem.open(crd_channel) end _conf_channels() @@ -261,23 +261,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range end pkt.make(msg_type, msg) - s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) - modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable()) + modem.transmit(svr_channel, crd_channel, s_pkt.raw_sendable()) self.sv_seq_num = self.sv_seq_num + 1 end -- send an API establish request response - ---@param dest integer - ---@param msg table - local function _send_api_establish_ack(seq_id, dest, msg) + ---@param packet scada_packet + ---@param ack ESTABLISH_ACK + local function _send_api_establish_ack(packet, ack) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack }) + s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(dest, api_listen, s_pkt.raw_sendable()) + modem.transmit(pkt_channel, crd_channel, s_pkt.raw_sendable()) + self.last_api_est_acks[packet.src_addr()] = ack end -- attempt connection establishment @@ -307,7 +308,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- close the connection to the server function public.close() sv_watchdog.cancel() + self.sv_addr = comms.BROADCAST self.sv_linked = false + self.sv_r_seq_num = nil _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {}) end @@ -335,12 +338,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 @@ -435,15 +439,18 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range ---@param packet mgmt_frame|crdn_frame|capi_frame|nil function public.handle_packet(packet) if packet ~= nil then - local l_port = packet.scada_frame.local_port() - local r_port = packet.scada_frame.remote_port() + local l_chan = packet.scada_frame.local_channel() + local r_chan = packet.scada_frame.remote_channel() + local src_addr = packet.scada_frame.src_addr() local protocol = packet.scada_frame.protocol() - if l_port == api_listen then + if l_chan ~= crd_channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == pkt_channel then if protocol == PROTOCOL.COORD_API then ---@cast packet capi_frame -- look for an associated session - local session = apisessions.find_session(r_port) + local session = apisessions.find_session(src_addr) -- API packet if session ~= nil then @@ -456,7 +463,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame -- look for an associated session - local session = apisessions.find_session(r_port) + local session = apisessions.find_session(src_addr) -- SCADA management packet if session ~= nil then @@ -464,8 +471,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range session.in_queue.push_packet(packet) elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session - local next_seq_id = packet.scada_frame.seq_num() + 1 - -- validate packet and continue if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then local comms_v = packet.data[1] @@ -473,42 +478,43 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range local dev_type = packet.data[3] if comms_v ~= comms.version then - if self.last_api_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then + if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - self.last_api_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION end - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) elseif dev_type == DEVICE_TYPE.PKT then -- pocket linking request - local id = apisessions.establish_session(l_port, r_port, firmware_v) - println(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) - coordinator.log_comms(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id)) + local id = apisessions.establish_session(src_addr, firmware_v) + println(util.c("[API] pocket (", firmware_v, ") [@", src_addr, "] \xbb connected")) + coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id)) - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) - self.last_api_est_acks[r_port] = ESTABLISH_ACK.ALLOW + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW) else - log.debug(util.c("illegal establish packet for device ", dev_type, " on API listening channel")) - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel")) + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY) end else log.debug("invalid establish packet (on API listening channel)") - _send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + _send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY) end else -- any other packet should be session related, discard it - log.debug(util.c(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session")) + log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr)) end else - log.debug("illegal packet type " .. protocol .. " on api listening channel", true) + log.debug("illegal packet type " .. protocol .. " on pocket channel", true) end - elseif l_port == sv_listen then + elseif r_chan == svr_channel then -- check sequence number if self.sv_r_seq_num == nil then self.sv_r_seq_num = packet.scada_frame.seq_num() elseif self.connected and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return + elseif self.sv_linked and src_addr ~= self.sv_addr then + log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?") + return else self.sv_r_seq_num = packet.scada_frame.seq_num() end @@ -659,6 +665,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range -- init io controller iocontrol.init(conf, public) + self.sv_addr = src_addr self.sv_linked = true self.sv_config_err = false else @@ -704,10 +711,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range local trip_time = util.time() - timestamp if trip_time > 750 then - log.warning("coord KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") + log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") end - -- log.debug("coord RTT = " .. trip_time .. "ms") + -- log.debug("coordinator RTT = " .. trip_time .. "ms") iocontrol.get_db().facility.ps.publish("sv_ping", trip_time) @@ -718,7 +725,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range elseif packet.type == SCADA_MGMT_TYPE.CLOSE then -- handle session close sv_watchdog.cancel() + self.sv_addr = comms.BROADCAST self.sv_linked = false + self.sv_r_seq_num = nil println_ts("server connection closed by remote host") log.info("server connection closed by remote host") else @@ -731,7 +740,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true) end else - log.debug("received packet on unconfigured channel " .. l_port, true) + log.debug("received packet for unknown channel " .. r_chan, true) end end end diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index c6d5a20..17988f5 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -5,7 +5,7 @@ local util = require("scada-common.util") local config = require("coordinator.config") -local api = require("coordinator.session.api") +local pocket = require("coordinator.session.pocket") local apisessions = {} @@ -18,7 +18,7 @@ local self = { -- PRIVATE FUNCTIONS -- -- handle a session output queue ----@param session api_session_struct +---@param session pkt_session_struct local function _api_handle_outq(session) -- record handler start time local handle_start = util.time() @@ -31,7 +31,7 @@ local function _api_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -41,15 +41,15 @@ local function _api_handle_outq(session) -- max 100ms spent processing queue if util.time() - handle_start > 100 then - log.warning("API out queue handler exceeded 100ms queue process limit") - log.warning(util.c("offending session: port ", session.r_port)) + log.warning("[API] out queue handler exceeded 100ms queue process limit") + log.warning(util.c("[API] offending session: ", session)) break end end end -- cleanly close a session ----@param session api_session_struct +---@param session pkt_session_struct local function _shutdown(session) session.open = false session.instance.close() @@ -58,11 +58,11 @@ local function _shutdown(session) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) end end - log.debug(util.c("closed API session ", session.instance.get_id(), " on remote port ", session.r_port)) + log.debug(util.c("[API] closed session ", session)) end -- PUBLIC FUNCTIONS -- @@ -81,54 +81,60 @@ end -- find a session by remote port ---@nodiscard ----@param port integer ----@return api_session_struct|nil -function apisessions.find_session(port) +---@param source_addr integer +---@return pkt_session_struct|nil +function apisessions.find_session(source_addr) for i = 1, #self.sessions do - if self.sessions[i].r_port == port then return self.sessions[i] end + if self.sessions[i].s_addr == source_addr then return self.sessions[i] end end return nil end -- establish a new API session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param version string ---@return integer session_id -function apisessions.establish_session(local_port, remote_port, version) - ---@class api_session_struct - local api_s = { +function apisessions.establish_session(source_addr, version) + ---@class pkt_session_struct + local pkt_s = { open = true, version = version, - l_port = local_port, - r_port = remote_port, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), - instance = nil ---@type api_session + instance = nil ---@type pkt_session } - api_s.instance = api.new_session(self.next_id, api_s.in_queue, api_s.out_queue, config.API_TIMEOUT) - table.insert(self.sessions, api_s) + local id = self.next_id - log.debug(util.c("established new API session to ", remote_port, " with ID ", self.next_id)) + pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, config.API_TIMEOUT) + table.insert(self.sessions, pkt_s) - self.next_id = self.next_id + 1 + local mt = { + ---@param s pkt_session_struct + __tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end + } + + setmetatable(pkt_s, mt) + + log.debug(util.c("[API] established new session: ", pkt_s)) + + self.next_id = id + 1 -- success - return api_s.instance.get_id() + return pkt_s.instance.get_id() end -- attempt to identify which session's watchdog timer fired ---@param timer_event number function apisessions.check_all_watchdogs(timer_event) for i = 1, #self.sessions do - local session = self.sessions[i] ---@type api_session_struct + local session = self.sessions[i] ---@type pkt_session_struct if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then - log.debug(util.c("watchdog closing API session ", session.instance.get_id(), - " on remote port ", session.r_port, "...")) + log.debug(util.c("[API] watchdog closing session ", session, "...")) _shutdown(session) end end @@ -138,7 +144,7 @@ end -- iterate all the API sessions function apisessions.iterate_all() for i = 1, #self.sessions do - local session = self.sessions[i] ---@type api_session_struct + local session = self.sessions[i] ---@type pkt_session_struct if session.open and session.instance.iterate() then _api_handle_outq(session) @@ -152,10 +158,9 @@ end function apisessions.free_all_closed() local f = function (session) return session.open end - ---@param session api_session_struct + ---@param session pkt_session_struct local on_delete = function (session) - log.debug(util.c("free'ing closed API session ", session.instance.get_id(), - " on remote port ", session.r_port)) + log.debug(util.c("[API] free'ing closed session ", session)) end util.filter_table(self.sessions, f, on_delete) @@ -164,7 +169,7 @@ end -- close all open connections function apisessions.close_all() for i = 1, #self.sessions do - local session = self.sessions[i] ---@type api_session_struct + local session = self.sessions[i] ---@type pkt_session_struct if session.open then _shutdown(session) end end diff --git a/coordinator/session/api.lua b/coordinator/session/pocket.lua similarity index 89% rename from coordinator/session/api.lua rename to coordinator/session/pocket.lua index 8f21483..ddabdda 100644 --- a/coordinator/session/api.lua +++ b/coordinator/session/pocket.lua @@ -3,7 +3,7 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") -local api = {} +local pocket = {} local PROTOCOL = comms.PROTOCOL -- local CAPI_TYPE = comms.CAPI_TYPE @@ -21,8 +21,8 @@ local API_S_CMDS = { local API_S_DATA = { } -api.API_S_CMDS = API_S_CMDS -api.API_S_DATA = API_S_DATA +pocket.API_S_CMDS = API_S_CMDS +pocket.API_S_DATA = API_S_DATA local PERIODICS = { KEEP_ALIVE = 2000 @@ -31,11 +31,12 @@ local PERIODICS = { -- pocket API session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@param timeout number communications timeout -function api.new_session(id, in_queue, out_queue, timeout) - local log_header = "api_session(" .. id .. "): " +function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) + local log_header = "pkt_session(" .. id .. "): " local self = { -- connection properties @@ -61,10 +62,10 @@ function api.new_session(id, in_queue, out_queue, timeout) } } - ---@class api_session + ---@class pkt_session local public = {} - -- mark this API session as closed, stop watchdog + -- mark this pocket session as closed, stop watchdog local function _close() self.conn_watchdog.cancel() self.connected = false @@ -92,7 +93,7 @@ function api.new_session(id, in_queue, out_queue, timeout) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -117,8 +118,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 @@ -136,11 +135,11 @@ function api.new_session(id, in_queue, out_queue, timeout) self.last_rtt = srv_now - srv_start if self.last_rtt > 750 then - log.warning(log_header .. "API KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") + log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)") end - -- log.debug(log_header .. "API RTT = " .. self.last_rtt .. "ms") - -- log.debug(log_header .. "API TT = " .. (srv_now - api_send) .. "ms") + -- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms") + -- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms") else log.debug(log_header .. "SCADA keep alive packet length mismatch") end @@ -173,7 +172,7 @@ function api.new_session(id, in_queue, out_queue, timeout) function public.close() _close() _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {}) - println("connection to API session " .. id .. " closed by server") + println("connection to pocket session " .. id .. " closed by server") log.info(log_header .. "session closed by server") end @@ -212,7 +211,7 @@ function api.new_session(id, in_queue, out_queue, timeout) -- exit if connection was closed if not self.connected then - println("connection to API session " .. id .. " closed by remote host") + println("connection to pocket session " .. id .. " closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected end @@ -248,4 +247,4 @@ function api.new_session(id, in_queue, out_queue, timeout) return public end -return api +return pocket diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 367c1ca..5d4fec8 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.15.2" +local COORDINATOR_VERSION = "v0.16.0" local println = util.println local println_ts = util.println_ts @@ -37,9 +37,9 @@ local log_comms_connecting = coordinator.log_comms_connecting local cfv = util.new_validator() -cfv.assert_port(config.SCADA_SV_PORT) -cfv.assert_port(config.SCADA_SV_CTL_LISTEN) -cfv.assert_port(config.SCADA_API_LISTEN) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.CRD_CHANNEL) +cfv.assert_channel(config.PKT_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.SV_TIMEOUT) cfv.assert_min(config.SV_TIMEOUT, 2) @@ -148,8 +148,8 @@ local function main() log.debug("startup> conn watchdog created") -- start comms, open all channels - local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_CTL_LISTEN, - config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog) + local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.CRD_CHANNEL, config.SVR_CHANNEL, + config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") @@ -163,7 +163,7 @@ local function main() -- attempt to connect to the supervisor or exit local function init_connect_sv() - local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT) + local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_CHANNEL) -- attempt to establish a connection with the supervisory computer if not coord_comms.sv_connect(60, tick_waiting, task_done) then @@ -334,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 diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua index a238a8c..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") @@ -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 diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua index bd341bf..3af8c83 100644 --- a/coordinator/ui/components/unit_overview.lua +++ b/coordinator/ui/components/unit_overview.lua @@ -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 a758b24..a7b3c86 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -32,7 +32,7 @@ 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} ping.register(facility.ps, "sv_ping", ping.update) datetime.register(facility.ps, "date_time", datetime.set_value) @@ -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/graphics/element.lua b/graphics/element.lua index b865af0..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 @@ -46,6 +45,7 @@ local element = {} ---|colormap_args ---|displaybox_args ---|div_args +---|listbox_args ---|multipane_args ---|pipenet_args ---|rectangle_args @@ -62,26 +62,26 @@ local element = {} ---@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 }, ---@type coordinate_2d - child_offset = { x = 0, y = 0 }, 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 + 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) + frame = core.gframe(1, 1, 1, 1), + children = {} } local name_brief = "graphics.element{" .. self.elem_type .. "}: " @@ -101,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 @@ -114,36 +112,18 @@ 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 @@ -153,7 +133,7 @@ function element.new(args) 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 @@ -198,15 +178,15 @@ function element.new(args) -- luacheck: push ignore ---@diagnostic disable: unused-local, unused-vararg - -- dynamically insert a child element - ---@param id string|integer element identifier - ---@param elem graphics_element element - function protected.insert(id, elem) + -- 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 - -- dynamically remove a child element - ---@param id string|integer element identifier - function protected.remove(id) + -- handle a child element having been removed + ---@param id element_id element identifier + function protected.on_removed(id) end -- handle a mouse event @@ -279,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 -- ----------- @@ -294,7 +282,8 @@ function element.new(args) -- 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 @@ -305,11 +294,21 @@ 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 -- delete this element (hide and unsubscribe from PSIL) function public.delete() - -- hide + stop animations + 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 @@ -319,9 +318,14 @@ function element.new(args) end -- delete all children - for k, v in pairs(self.children) do + for k, v in pairs(protected.children) do v.delete() - self.children[k] = nil + protected.children[k] = nil + end + + if args.parent ~= nil then + -- remove self from parent + args.parent.__remove_child(self.id) end end @@ -330,41 +334,53 @@ function element.new(args) -- 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(id) return self.children[id] end + function public.get_child(id) return protected.children[id] end -- remove a child element ---@param id element_id function public.remove(id) - if self.children[id] ~= nil then - self.children[id].delete() - self.children[id] = nil + if protected.children[id] ~= nil then + protected.children[id].delete() + protected.on_removed(id) + protected.children[id] = nil end end @@ -373,37 +389,18 @@ function element.new(args) ---@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 end - -- DYNAMIC CHILD ELEMENTS -- - - -- insert an element as a contained child
- -- this is intended to be used dynamically, and depends on the target element type.
- -- not all elements support dynamic children. - ---@param id string|integer element identifier - ---@param elem graphics_element element - function public.insert_element(id, elem) - protected.insert(id, elem) - end - - -- remove an element from contained children
- -- this is intended to be used dynamically, and depends on the target element type.
- -- not all elements support dynamic children. - ---@param id string|integer element identifier - function public.remove_element(id) - protected.remove(id) - end - -- AUTO-PLACEMENT -- -- skip a line for automatically placed elements @@ -437,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 @@ -519,7 +516,7 @@ function element.new(args) -- handle the mouse event then pass to children protected.handle_mouse(event_T) - for _, child in pairs(self.children) do child.handle_mouse(event_T) end + for _, child in pairs(protected.children) do child.handle_mouse(event_T) end end end @@ -545,27 +542,61 @@ function element.new(args) ps.subscribe(key, func) end - -- VISIBILITY -- + -- VISIBILITY & ANIMATIONS -- - -- show the element - function public.show() + -- 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) - protected.start_anim() - for _, child in pairs(self.children) do child.show() end + if animate ~= false then public.animate_all() end end - -- hide the element + -- 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 4dca5c4..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 @@ -198,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 e44bad0..279c9a7 100644 --- a/graphics/elements/controls/multi_button.lua +++ b/graphics/elements/controls/multi_button.lua @@ -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 @@ -130,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 27be991..e6f4920 100644 --- a/graphics/elements/controls/push_button.lua +++ b/graphics/elements/controls/push_button.lua @@ -1,6 +1,6 @@ -- 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") @@ -19,6 +19,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE ---@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 @@ -120,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 050bf39..e3edf24 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -15,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 @@ -103,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 45771b2..d43317a 100644 --- a/graphics/elements/controls/sidebar.lua +++ b/graphics/elements/controls/sidebar.lua @@ -1,6 +1,6 @@ -- 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") @@ -20,6 +20,7 @@ local CLICK_TYPE = core.events.CLICK_TYPE ---@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 @@ -115,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 6b88c0e..767d97b 100644 --- a/graphics/elements/controls/spinbox_numeric.lua +++ b/graphics/elements/controls/spinbox_numeric.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 spinbox control (minimum value is 0) ---@param args spinbox_args @@ -188,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 645bf8a..6d2e09c 100644 --- a/graphics/elements/controls/switch_button.lua +++ b/graphics/elements/controls/switch_button.lua @@ -15,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 @@ -86,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 index 6249951..da4738b 100644 --- a/graphics/elements/controls/tabbar.lua +++ b/graphics/elements/controls/tabbar.lua @@ -21,6 +21,7 @@ local element = require("graphics.element") ---@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 @@ -124,7 +125,7 @@ local function tabbar(args) -- initial draw draw() - return e.get() + 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 05434a3..127a8a3 100644 --- a/graphics/elements/indicators/coremap.lua +++ b/graphics/elements/indicators/coremap.lua @@ -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 8a1d29b..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 @@ -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 9066deb..e72571b 100644 --- a/graphics/elements/textbox.lua +++ b/graphics/elements/textbox.lua @@ -18,6 +18,7 @@ local TEXT_ALIGN = core.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/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..1456f20 100644 --- a/imgen.py +++ b/imgen.py @@ -111,7 +111,7 @@ f.close() if len(sys.argv) > 1 and sys.argv[1] == "shields": # 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 d1b808d..1a6bde2 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.3.2", "rtu": "v1.2.2", "supervisor": "v0.15.9", "coordinator": "v0.15.2", "pocket": "alpha-v0.3.2"}, "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/tcallbackdsp.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.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/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/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.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/turbine_page.lua", "pocket/ui/components/reactor_page.lua", "pocket/ui/components/home_page.lua", "pocket/ui/components/unit_page.lua", "pocket/ui/components/boiler_page.lua", "pocket/ui/components/conn_waiting.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics"], "rtu": ["system", "common", "graphics"], "supervisor": ["system", "common"], "coordinator": ["system", "common", "graphics"], "pocket": ["system", "common", "graphics"]}, "sizes": {"manifest": 5530, "system": 1991, "common": 91102, "graphics": 129460, "lockbox": 100797, "reactor-plc": 95896, "rtu": 100982, "supervisor": 283073, "coordinator": 197508, "pocket": 36200}} \ No newline at end of file +{"versions": {"installer": "v1.2", "bootloader": "0.2", "comms": "2.0.0", "reactor-plc": "v1.4.5", "rtu": "v1.3.5", "supervisor": "v0.17.5", "coordinator": "v0.16.0", "pocket": "alpha-v0.4.4"}, "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/pocket.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": 5769, "system": 1991, "common": 92736, "graphics": 144028, "lockbox": 100797, "reactor-plc": 97335, "rtu": 102240, "supervisor": 315402, "coordinator": 197991, "pocket": 37581}} \ No newline at end of file diff --git a/pocket/config.lua b/pocket/config.lua index 27e1489..0c35b59 100644 --- a/pocket/config.lua +++ b/pocket/config.lua @@ -1,11 +1,11 @@ local config = {} --- port of the SCADA supervisor -config.SCADA_SV_PORT = 16100 --- port for SCADA coordinator API access -config.SCADA_API_PORT = 16200 --- port to listen to incoming packets FROM servers -config.LISTEN_PORT = 16201 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- coordinator comms channel +config.CRD_CHANNEL = 16243 +-- pocket comms channel +config.PKT_CHANNEL = 16244 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/pocket/pocket.lua b/pocket/pocket.lua index cc4d870..0281e92 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -18,22 +18,24 @@ local pocket = {} ---@nodiscard ---@param version string pocket version ---@param modem table modem device ----@param local_port integer local pocket port ----@param sv_port integer port of supervisor ----@param api_port integer port of coordinator API +---@param pkt_channel integer pocket comms channel +---@param svr_channel integer supervisor access channel +---@param crd_channel integer coordinator access channel ---@param range integer trusted device connection range ---@param sv_watchdog watchdog ---@param api_watchdog watchdog -function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_watchdog, api_watchdog) +function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) local self = { sv = { linked = false, + addr = comms.BROADCAST, seq_num = 0, r_seq_num = nil, ---@type nil|integer last_est_ack = ESTABLISH_ACK.ALLOW }, api = { linked = false, + addr = comms.BROADCAST, seq_num = 0, r_seq_num = nil, ---@type nil|integer last_est_ack = ESTABLISH_ACK.ALLOW @@ -48,7 +50,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(local_port) + modem.open(pkt_channel) end _conf_channels() @@ -61,9 +63,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w local pkt = comms.mgmt_packet() pkt.make(msg_type, msg) - s_pkt.make(self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - modem.transmit(sv_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, pkt_channel, s_pkt.raw_sendable()) self.sv.seq_num = self.sv.seq_num + 1 end @@ -75,9 +77,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w local pkt = comms.mgmt_packet() pkt.make(msg_type, msg) - s_pkt.make(self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) + s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - modem.transmit(api_port, local_port, s_pkt.raw_sendable()) + modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) self.api.seq_num = self.api.seq_num + 1 end @@ -89,9 +91,9 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- local pkt = comms.capi_packet() -- pkt.make(msg_type, msg) - -- s_pkt.make(self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) + -- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) - -- modem.transmit(api_port, local_port, s_pkt.raw_sendable()) + -- modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) -- self.api.seq_num = self.api.seq_num + 1 -- end @@ -133,6 +135,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w function public.close_sv() sv_watchdog.cancel() self.sv.linked = false + self.sv.r_seq_num = nil + self.sv.addr = comms.BROADCAST _send_sv(SCADA_MGMT_TYPE.CLOSE, {}) end @@ -140,6 +144,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w function public.close_api() api_watchdog.cancel() self.api.linked = false + self.api.r_seq_num = nil + self.api.addr = comms.BROADCAST _send_crd(SCADA_MGMT_TYPE.CLOSE, {}) end @@ -214,18 +220,23 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w ---@param packet mgmt_frame|capi_frame|nil function public.handle_packet(packet) if packet ~= nil then - local l_port = packet.scada_frame.local_port() - local r_port = packet.scada_frame.remote_port() + local l_chan = packet.scada_frame.local_channel() + local r_chan = packet.scada_frame.remote_channel() local protocol = packet.scada_frame.protocol() + local src_addr = packet.scada_frame.src_addr() - if l_port ~= local_port then - log.debug("received packet on unconfigured channel " .. l_port, true) - elseif r_port == api_port then + if l_chan ~= pkt_channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == crd_channel then -- check sequence number if self.api.r_seq_num == nil then self.api.r_seq_num = packet.scada_frame.seq_num() elseif self.connected and ((self.api.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then - log.warning("sequence out-of-order: last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + log.warning("sequence out-of-order (API): last = " .. self.api.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + return + elseif self.api.linked and (src_addr ~= self.api.addr) then + log.debug("received packet from unknown computer " .. src_addr .. " while linked (API expected " .. self.api.addr .. + "); channel in use by another system?") return else self.api.r_seq_num = packet.scada_frame.seq_num() @@ -247,6 +258,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w log.info("coordinator connection established") self.establish_delay_counter = 0 self.api.linked = true + self.api.addr = src_addr if self.sv.linked then coreio.report_link_state(LINK_STATE.LINKED) @@ -294,6 +306,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- handle session close api_watchdog.cancel() self.api.linked = false + self.api.r_seq_num = nil + self.api.addr = comms.BROADCAST log.info("coordinator server connection closed by remote host") else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator") @@ -304,12 +318,16 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w else log.debug("illegal packet type " .. protocol .. " from coordinator", true) end - elseif r_port == sv_port then + elseif r_chan == svr_channel then -- check sequence number if self.sv.r_seq_num == nil then self.sv.r_seq_num = packet.scada_frame.seq_num() elseif self.connected and ((self.sv.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then - log.warning("sequence out-of-order: last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + log.warning("sequence out-of-order (SVR): last = " .. self.sv.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + return + elseif self.sv.linked and (src_addr ~= self.sv.addr) then + log.debug("received packet from unknown computer " .. src_addr .. " while linked (SVR expected " .. self.sv.addr .. + "); channel in use by another system?") return else self.sv.r_seq_num = packet.scada_frame.seq_num() @@ -330,6 +348,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w log.info("supervisor connection established") self.establish_delay_counter = 0 self.sv.linked = true + self.sv.addr = src_addr if self.api.linked then coreio.report_link_state(LINK_STATE.LINKED) @@ -377,6 +396,8 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w -- handle session close sv_watchdog.cancel() self.sv.linked = false + self.sv.r_seq_num = nil + self.sv.addr = comms.BROADCAST log.info("supervisor server connection closed by remote host") else log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor") @@ -388,7 +409,7 @@ function pocket.comms(version, modem, local_port, sv_port, api_port, range, sv_w log.debug("illegal packet type " .. protocol .. " from supervisor", true) end else - log.debug("received packet from unconfigured channel " .. r_port, true) + log.debug("received packet from unconfigured channel " .. r_chan, true) end end end diff --git a/pocket/startup.lua b/pocket/startup.lua index 075919d..0683af6 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.3.2" +local POCKET_VERSION = "alpha-v0.4.4" local println = util.println local println_ts = util.println_ts @@ -28,9 +28,9 @@ local println_ts = util.println_ts local cfv = util.new_validator() -cfv.assert_port(config.SCADA_SV_PORT) -cfv.assert_port(config.SCADA_API_PORT) -cfv.assert_port(config.LISTEN_PORT) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.CRD_CHANNEL) +cfv.assert_channel(config.PKT_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) @@ -89,8 +89,8 @@ local function main() log.debug("startup> conn watchdogs created") -- start comms, open all channels - local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.LISTEN_PORT, config.SCADA_SV_PORT, - config.SCADA_API_PORT, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) + local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.PKT_CHANNEL, config.SVR_CHANNEL, + config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) log.debug("startup> comms init") -- base loop clock (2Hz, 10 ticks) @@ -120,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" 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 + 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 9bbbfc0..114d165 100644 --- a/pocket/ui/components/conn_waiting.lua +++ b/pocket/ui/components/conn_waiting.lua @@ -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 c143b0b..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") diff --git a/pocket/ui/components/boiler_page.lua b/pocket/ui/pages/boiler_page.lua similarity index 100% rename from pocket/ui/components/boiler_page.lua rename to pocket/ui/pages/boiler_page.lua diff --git a/pocket/ui/components/home_page.lua b/pocket/ui/pages/home_page.lua similarity index 100% rename from pocket/ui/components/home_page.lua rename to pocket/ui/pages/home_page.lua diff --git a/pocket/ui/components/reactor_page.lua b/pocket/ui/pages/reactor_page.lua similarity index 100% rename from pocket/ui/components/reactor_page.lua rename to pocket/ui/pages/reactor_page.lua diff --git a/pocket/ui/components/turbine_page.lua b/pocket/ui/pages/turbine_page.lua similarity index 100% rename from pocket/ui/components/turbine_page.lua rename to pocket/ui/pages/turbine_page.lua diff --git a/pocket/ui/components/unit_page.lua b/pocket/ui/pages/unit_page.lua similarity index 100% rename from pocket/ui/components/unit_page.lua rename to pocket/ui/pages/unit_page.lua diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua index e402bbb..3462b2c 100644 --- a/reactor-plc/config.lua +++ b/reactor-plc/config.lua @@ -9,10 +9,10 @@ config.REACTOR_ID = 1 -- when emergency coolant is needed due to low coolant -- config.EMERGENCY_COOL = { side = "right", color = nil } --- port to send packets TO server -config.SERVER_PORT = 16000 --- port to listen to incoming packets FROM server -config.LISTEN_PORT = 14001 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- PLC comms channel +config.PLC_CHANNEL = 16241 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/reactor-plc/panel/front_panel.lua b/reactor-plc/panel/front_panel.lua index 8e28a75..12c8266 100644 --- a/reactor-plc/panel/front_panel.lua +++ b/reactor-plc/panel/front_panel.lua @@ -2,6 +2,7 @@ -- Main SCADA Coordinator GUI -- +local types = require("scada-common.types") local util = require("scada-common.util") local config = require("reactor-plc.config") @@ -49,7 +50,7 @@ local function init(panel) 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)} local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}} - network.update(5) + network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() reactor.register(databus.ps, "reactor_dev_state", reactor.update) @@ -69,6 +70,10 @@ local function init(panel) rt_cmrx.register(databus.ps, "routine__comms_rx", rt_cmrx.update) rt_sctl.register(databus.ps, "routine__spctl", rt_sctl.update) +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=5,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + -- -- status & controls -- diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 4f04138..02e592a 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -446,14 +446,15 @@ end ---@param id integer reactor ID ---@param version string PLC version ---@param modem table modem device ----@param local_port integer local listening port ----@param server_port integer remote server port +---@param plc_channel integer PLC comms channel +---@param svr_channel integer supervisor server channel ---@param range integer trusted device connection range ---@param reactor table reactor device ---@param rps rps RPS reference ---@param conn_watchdog watchdog watchdog reference -function plc.comms(id, version, modem, local_port, server_port, range, reactor, rps, conn_watchdog) +function plc.comms(id, version, modem, plc_channel, svr_channel, range, reactor, rps, conn_watchdog) local self = { + sv_addr = comms.BROADCAST, seq_num = 0, r_seq_num = nil, scrammed = false, @@ -472,7 +473,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(local_port) + modem.open(plc_channel) end _conf_channels() @@ -485,9 +486,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local r_pkt = comms.rplc_packet() r_pkt.make(id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -499,9 +500,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, plc_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -667,9 +668,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- unlink from the server function public.unlink() + self.sv_addr = comms.BROADCAST self.linked = false self.r_seq_num = nil self.status_cache = nil + databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED) end -- close the connection to the server @@ -731,7 +734,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, end end - -- parse an RPLC packet + -- parse a packet ---@nodiscard ---@param side string ---@param sender integer @@ -760,14 +763,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, pkt = mgmt_pkt.get() end else - log.debug("illegal packet type " .. s_pkt.protocol(), true) + log.debug("unsupported packet type " .. s_pkt.protocol(), true) end end return pkt end - -- handle an RPLC packet + -- handle RPLC and MGMT packets ---@param packet rplc_frame|mgmt_frame packet frame ---@param plc_state plc_state PLC state ---@param setpoints setpoints setpoint control table @@ -775,16 +778,22 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- print a log message to the terminal as long as the UI isn't running local function println_ts(message) if not plc_state.fp_ok then util.println_ts(message) end end - local l_port = packet.scada_frame.local_port() + local protocol = packet.scada_frame.protocol() + local l_chan = packet.scada_frame.local_channel() + local src_addr = packet.scada_frame.src_addr() -- handle packets now that we have prints setup - if l_port == local_port then + if l_chan == plc_channel then -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() elseif self.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return + elseif self.linked and (src_addr ~= self.sv_addr) then + log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. + "); channel in use by another system?") + return else self.r_seq_num = packet.scada_frame.seq_num() end @@ -792,11 +801,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, -- feed the watchdog first so it doesn't uhh...eat our packets :) conn_watchdog.feed() - local protocol = packet.scada_frame.protocol() - -- handle packet if protocol == PROTOCOL.RPLC then ---@cast packet rplc_frame + -- if linked, only accept packets from configured supervisor if self.linked then if packet.type == RPLC_TYPE.STATUS then -- request of full status, clear cache first @@ -933,6 +941,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, end elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame + -- if linked, only accept packets from configured supervisor if self.linked then if packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- link request confirmation @@ -945,22 +954,26 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, self.status_cache = nil _send_struct() public.send_status(plc_state.no_reactor, plc_state.reactor_formed) - log.debug("re-sent initial status data") - elseif est_ack == ESTABLISH_ACK.DENY then - println_ts("received unsolicited link denial, unlinking") - log.warning("unsolicited establish request denied") - elseif est_ack == ESTABLISH_ACK.COLLISION then - println_ts("received unsolicited link collision, unlinking") - log.warning("unsolicited establish request collision") - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - println_ts("received unsolicited link version mismatch, unlinking") - log.warning("unsolicited establish request version mismatch") + log.debug("re-sent initial status data due to re-establish") else - println_ts("invalid unsolicited link response") - log.debug("unsolicited unknown establish request response") - end + if est_ack == ESTABLISH_ACK.DENY then + println_ts("received unsolicited link denial, unlinking") + log.warning("unsolicited establish request denied") + elseif est_ack == ESTABLISH_ACK.COLLISION then + println_ts("received unsolicited link collision, unlinking") + log.warning("unsolicited establish request collision") + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + println_ts("received unsolicited link version mismatch, unlinking") + log.warning("unsolicited establish request version mismatch") + else + println_ts("invalid unsolicited link response") + log.debug("unsolicited unknown establish request response") + end - self.linked = est_ack == ESTABLISH_ACK.ALLOW + -- unlink + self.sv_addr = comms.BROADCAST + self.linked = false + end -- clear this since this is for something that was unsolicited self.last_est_ack = ESTABLISH_ACK.ALLOW @@ -980,7 +993,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, log.warning("PLC KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)") end - -- log.debug("RPLC RTT = " .. trip_time .. "ms") + -- log.debug("PLC RTT = " .. trip_time .. "ms") _send_keep_alive_ack(timestamp) else @@ -1002,9 +1015,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, if est_ack == ESTABLISH_ACK.ALLOW then println_ts("linked!") - log.info("supervisor establish request approved, PLC is linked") + log.info("supervisor establish request approved, linked to SV (CID#" .. src_addr .. ")") - -- reset remote sequence number and cache + -- link + reset remote sequence number and cache + self.sv_addr = src_addr + self.linked = true self.r_seq_num = nil self.status_cache = nil @@ -1012,23 +1027,28 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, public.send_status(plc_state.no_reactor, plc_state.reactor_formed) log.debug("sent initial status data") - elseif self.last_est_ack ~= est_ack then - if est_ack == ESTABLISH_ACK.DENY then - println_ts("link request denied, retrying...") - log.info("supervisor establish request denied, retrying") - elseif est_ack == ESTABLISH_ACK.COLLISION then - println_ts("reactor PLC ID collision (check config), retrying...") - log.warning("establish request collision, retrying") - elseif est_ack == ESTABLISH_ACK.BAD_VERSION then - println_ts("supervisor version mismatch (try updating), retrying...") - log.warning("establish request version mismatch, retrying") - else - println_ts("invalid link response, bad channel? retrying...") - log.error("unknown establish request response, retrying") + else + if self.last_est_ack ~= est_ack then + if est_ack == ESTABLISH_ACK.DENY then + println_ts("link request denied, retrying...") + log.info("supervisor establish request denied, retrying") + elseif est_ack == ESTABLISH_ACK.COLLISION then + println_ts("reactor PLC ID collision (check config), retrying...") + log.warning("establish request collision, retrying") + elseif est_ack == ESTABLISH_ACK.BAD_VERSION then + println_ts("supervisor version mismatch (try updating), retrying...") + log.warning("establish request version mismatch, retrying") + else + println_ts("invalid link response, bad channel? retrying...") + log.error("unknown establish request response, retrying") + end end + + -- unlink + self.sv_addr = comms.BROADCAST + self.linked = false end - self.linked = est_ack == ESTABLISH_ACK.ALLOW self.last_est_ack = est_ack -- report link state @@ -1044,7 +1064,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor, log.error("illegal packet type " .. protocol, true) end else - log.debug("received packet on unconfigured channel " .. l_port, true) + log.debug("received packet on unconfigured channel " .. l_chan, true) end end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1a7f550..227bd27 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.2" +local R_PLC_VERSION = "v1.4.5" local println = util.println local println_ts = util.println_ts @@ -31,8 +31,8 @@ local cfv = util.new_validator() cfv.assert_type_bool(config.NETWORKED) cfv.assert_type_int(config.REACTOR_ID) -cfv.assert_port(config.SERVER_PORT) -cfv.assert_port(config.LISTEN_PORT) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.PLC_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) @@ -197,7 +197,7 @@ local function main() log.debug("init> conn watchdog started") -- start comms - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.PLC_CHANNEL, config.SVR_CHANNEL, config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) log.debug("init> comms init") else diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 8dbea6d..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) diff --git a/rtu/config.lua b/rtu/config.lua index 1b96bec..2279759 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -2,11 +2,11 @@ local rsio = require("scada-common.rsio") local config = {} --- port to send packets TO server -config.SERVER_PORT = 16000 --- port to listen to incoming packets FROM server -config.LISTEN_PORT = 15001 --- max trusted modem message distance (< 1 to disable check) +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- RTU/MODBUS comms channel +config.RTU_CHANNEL = 16242 +-- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active config.COMMS_TIMEOUT = 5 diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index 4c4aa78..467386e 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -2,6 +2,7 @@ -- Main SCADA Coordinator GUI -- +local types = require("scada-common.types") local util = require("scada-common.util") local databus = require("rtu.databus") @@ -44,7 +45,7 @@ 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() @@ -53,7 +54,7 @@ local function init(panel, units) 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) + network.update(types.PANEL_LINK_STATE.DISCONNECTED) system.line_break() modem.register(databus.ps, "has_modem", modem.update) @@ -66,6 +67,10 @@ local function init(panel, units) rt_main.register(databus.ps, "routine__main", rt_main.update) rt_comm.register(databus.ps, "routine__comms", rt_comm.update) +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + -- -- about label -- diff --git a/rtu/rtu.lua b/rtu/rtu.lua index bb43706..831e231 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -159,12 +159,13 @@ end ---@nodiscard ---@param version string RTU version ---@param modem table modem device ----@param local_port integer local listening port ----@param server_port integer remote server port +---@param rtu_channel integer PLC comms channel +---@param svr_channel integer supervisor server channel ---@param range integer trusted device connection range ---@param conn_watchdog watchdog watchdog reference -function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog) +function rtu.comms(version, modem, rtu_channel, svr_channel, range, conn_watchdog) local self = { + sv_addr = comms.BROADCAST, seq_num = 0, r_seq_num = nil, txn_id = 0, @@ -180,7 +181,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(local_port) + modem.open(rtu_channel) end _conf_channels() @@ -193,9 +194,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -238,8 +239,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog ---@param m_pkt modbus_packet function public.send_modbus(m_pkt) local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) - modem.transmit(server_port, local_port, s_pkt.raw_sendable()) + s_pkt.make(self.sv_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) + modem.transmit(svr_channel, rtu_channel, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -254,7 +255,9 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog ---@param rtu_state rtu_state function public.unlink(rtu_state) rtu_state.linked = false + self.sv_addr = comms.BROADCAST self.r_seq_num = nil + databus.tx_link_state(types.PANEL_LINK_STATE.DISCONNECTED) end -- close the connection to the server @@ -327,13 +330,21 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- print a log message to the terminal as long as the UI isn't running local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end - if packet.scada_frame.local_port() == local_port then + local protocol = packet.scada_frame.protocol() + local l_chan = packet.scada_frame.local_channel() + local src_addr = packet.scada_frame.src_addr() + + if l_chan == rtu_channel then -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() elseif rtu_state.linked and ((self.r_seq_num + 1) ~= packet.scada_frame.seq_num()) then log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return + elseif rtu_state.linked and (src_addr ~= self.sv_addr) then + log.debug("received packet from unknown computer " .. src_addr .. " while linked (expected " .. self.sv_addr .. + "); channel in use by another system?") + return else self.r_seq_num = packet.scada_frame.seq_num() end @@ -341,8 +352,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- feed watchdog on valid sequence number conn_watchdog.feed() - local protocol = packet.scada_frame.protocol() - + -- handle packet if protocol == PROTOCOL.MODBUS_TCP then ---@cast packet modbus_frame if rtu_state.linked then @@ -398,6 +408,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog if est_ack == ESTABLISH_ACK.ALLOW then -- establish allowed rtu_state.linked = true + self.sv_addr = packet.scada_frame.src_addr() self.r_seq_num = nil println_ts("supervisor connection established") log.info("supervisor connection established") @@ -461,6 +472,8 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog -- should be unreachable assuming packet is from parse_packet() log.error("illegal packet type " .. protocol, true) end + else + log.debug("received packet on unconfigured channel " .. l_chan, true) end end diff --git a/rtu/startup.lua b/rtu/startup.lua index 6ff5e7d..05b131b 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.2.2" +local RTU_VERSION = "v1.3.5" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -42,8 +42,8 @@ local println_ts = util.println_ts local cfv = util.new_validator() -cfv.assert_port(config.SERVER_PORT) -cfv.assert_port(config.LISTEN_PORT) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.RTU_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.COMMS_TIMEOUT) cfv.assert_min(config.COMMS_TIMEOUT, 2) @@ -457,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 @@ -467,7 +467,7 @@ local function main() log.debug("startup> conn watchdog started") -- setup comms - smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, + smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.RTU_CHANNEL, config.SVR_CHANNEL, config.TRUSTED_RANGE, smem_sys.conn_watchdog) log.debug("startup> comms init") diff --git a/rtu/threads.lua b/rtu/threads.lua index 046525c..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) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index fb54101..c5e9fd2 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -4,14 +4,17 @@ local log = require("scada-common.log") +local insert = table.insert + +---@diagnostic disable-next-line: undefined-field +local C_ID = os.getComputerID() ---@type integer computer ID + +local max_distance = nil ---@type number|nil maximum acceptable transmission distance + ---@class comms local comms = {} -local insert = table.insert - -local max_distance = nil - -comms.version = "1.4.1" +comms.version = "2.0.0" ---@enum PROTOCOL local PROTOCOL = { @@ -122,27 +125,28 @@ comms.PLC_AUTO_ACK = PLC_AUTO_ACK comms.UNIT_COMMAND = UNIT_COMMAND comms.FAC_COMMAND = FAC_COMMAND +-- destination broadcast address (to all devices) +comms.BROADCAST = -1 + ---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame -- configure the maximum allowable message receive distance
-- packets received with distances greater than this will be silently discarded ----@param distance integer max modem message distance (less than 1 disables the limit) +---@param distance integer max modem message distance (0 disables the limit) function comms.set_trusted_range(distance) - if distance < 1 then - max_distance = nil - else - max_distance = distance - end + if distance == 0 then max_distance = nil else max_distance = distance end end -- generic SCADA packet object ---@nodiscard function comms.scada_packet() local self = { - modem_msg_in = nil, + modem_msg_in = nil, ---@type modem_message|nil valid = false, - raw = { -1, PROTOCOL.SCADA_MGMT, {} }, + raw = {}, + src_addr = comms.BROADCAST, + dest_addr = comms.BROADCAST, seq_num = -1, protocol = PROTOCOL.SCADA_MGMT, length = 0, @@ -153,34 +157,40 @@ function comms.scada_packet() local public = {} -- make a SCADA packet - ---@param seq_num integer + ---@param dest_addr integer destination computer address (ID) + ---@param seq_num integer sequence number ---@param protocol PROTOCOL ---@param payload table - function public.make(seq_num, protocol, payload) + function public.make(dest_addr, seq_num, protocol, payload) self.valid = true +---@diagnostic disable-next-line: undefined-field + self.src_addr = C_ID + self.dest_addr = dest_addr self.seq_num = seq_num self.protocol = protocol self.length = #payload self.payload = payload - self.raw = { self.seq_num, self.protocol, self.payload } + self.raw = { self.src_addr, self.dest_addr, self.seq_num, self.protocol, self.payload } end -- parse in a modem message as a SCADA packet ---@param side string modem side - ---@param sender integer sender port - ---@param reply_to integer reply port + ---@param sender integer sender channel + ---@param reply_to integer reply channel ---@param message any message body ---@param distance integer transmission distance ---@return boolean valid valid message received function public.receive(side, sender, reply_to, message, distance) + ---@class modem_message self.modem_msg_in = { iface = side, - s_port = sender, - r_port = reply_to, + s_channel = sender, + r_channel = reply_to, msg = message, dist = distance } + self.valid = false self.raw = self.modem_msg_in.msg if (type(max_distance) == "number") and (distance > max_distance) then @@ -188,20 +198,31 @@ function comms.scada_packet() -- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range") else if type(self.raw) == "table" then - if #self.raw >= 3 then - self.seq_num = self.raw[1] - self.protocol = self.raw[2] + if #self.raw == 5 then + self.src_addr = self.raw[1] + self.dest_addr = self.raw[2] + self.seq_num = self.raw[3] + self.protocol = self.raw[4] - -- element 3 must be a table - if type(self.raw[3]) == "table" then - self.length = #self.raw[3] - self.payload = self.raw[3] + -- element 5 must be a table + if type(self.raw[5]) == "table" then + self.length = #self.raw[5] + self.payload = self.raw[5] end + else + self.src_addr = nil + self.dest_addr = nil + self.seq_num = nil + self.protocol = nil + self.length = 0 + self.payload = {} end - self.valid = type(self.seq_num) == "number" and - type(self.protocol) == "number" and - type(self.payload) == "table" + -- check if this packet is destined for this device + local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == C_ID) + + self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and + type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table" end end @@ -216,13 +237,17 @@ function comms.scada_packet() function public.raw_sendable() return self.raw end ---@nodiscard - function public.local_port() return self.modem_msg_in.s_port end + function public.local_channel() return self.modem_msg_in.s_channel end ---@nodiscard - function public.remote_port() return self.modem_msg_in.r_port end + function public.remote_channel() return self.modem_msg_in.r_channel end ---@nodiscard function public.is_valid() return self.valid end + ---@nodiscard + function public.src_addr() return self.src_addr end + ---@nodiscard + function public.dest_addr() return self.dest_addr end ---@nodiscard function public.seq_num() return self.seq_num end ---@nodiscard 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 8df01c1..21429b5 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -74,6 +74,15 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end -- ENUMERATION TYPES -- --#region +---@enum PANEL_LINK_STATE +types.PANEL_LINK_STATE = { + LINKED = 1, + DENIED = 2, + COLLISION = 3, + BAD_VERSION = 4, + DISCONNECTED = 5 +} + ---@enum RTU_UNIT_TYPE types.RTU_UNIT_TYPE = { VIRTUAL = 0, -- virtual device diff --git a/scada-common/util.lua b/scada-common/util.lua index 063143c..99f62a6 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -65,7 +65,8 @@ end ---@return string function util.strval(val) local t = type(val) - if t == "table" or t == "function" then + -- this depends on Lua short-circuiting the or check for metatables (note: metatables won't have metatables) + if (t == "table" and (getmetatable(val) == nil or getmetatable(val).__tostring == nil)) or t == "function" then return "[" .. tostring(val) .. "]" else return tostring(val) @@ -539,7 +540,7 @@ function util.new_validator() function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end - function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end + function public.assert_channel(channel) valid = valid and type(channel) == "number" and channel >= 0 and channel <= 65535 end -- check if all assertions passed successfully ---@nodiscard diff --git a/supervisor/config.lua b/supervisor/config.lua index b716d38..a4a595b 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -1,9 +1,15 @@ local config = {} --- scada network listen for PLC's and RTU's -config.SCADA_DEV_LISTEN = 16000 --- listen port for SCADA supervisor access -config.SCADA_SV_CTL_LISTEN = 16100 +-- supervisor comms channel +config.SVR_CHANNEL = 16240 +-- PLC comms channel +config.PLC_CHANNEL = 16241 +-- RTU/MODBUS comms channel +config.RTU_CHANNEL = 16242 +-- coordinator comms channel +config.CRD_CHANNEL = 16243 +-- pocket comms channel +config.PKT_CHANNEL = 16244 -- max trusted modem message distance (0 to disable check) config.TRUSTED_RANGE = 0 -- time in seconds (>= 2) before assuming a remote device is no longer active diff --git a/supervisor/databus.lua b/supervisor/databus.lua new file mode 100644 index 0000000..00185c7 --- /dev/null +++ b/supervisor/databus.lua @@ -0,0 +1,179 @@ +-- +-- Data Bus - Central Communication Linking for Supervisor Front Panel +-- + +local psil = require("scada-common.psil") +local util = require("scada-common.util") + +local pgi = require("supervisor.panel.pgi") + +-- nominal RTT is ping (0ms to 10ms usually) + 150ms for SV main loop tick +local WARN_RTT = 300 -- 2x as long as expected w/ 0 ping +local HIGH_RTT = 500 -- 3.33x as long as expected w/ 0 ping + +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 s_addr integer PLC computer ID +function databus.tx_plc_connected(reactor_id, fw, s_addr) + databus.ps.publish("plc_" .. reactor_id .. "_fw", fw) + databus.ps.publish("plc_" .. reactor_id .. "_conn", true) + databus.ps.publish("plc_" .. reactor_id .. "_addr", util.sprintf("@% 4d", s_addr)) +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 .. "_addr", " --- ") + 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 > HIGH_RTT then + databus.ps.publish("plc_" .. reactor_id .. "_rtt_color", colors.red) + elseif rtt > WARN_RTT 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 s_addr integer RTU computer ID +function databus.tx_rtu_connected(session_id, fw, s_addr) + databus.ps.publish("rtu_" .. session_id .. "_fw", fw) + databus.ps.publish("rtu_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr)) + 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 > HIGH_RTT then + databus.ps.publish("rtu_" .. session_id .. "_rtt_color", colors.red) + elseif rtt > WARN_RTT 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 s_addr integer coordinator computer ID +function databus.tx_crd_connected(fw, s_addr) + databus.ps.publish("crd_fw", fw) + databus.ps.publish("crd_conn", true) + databus.ps.publish("crd_addr", tostring(s_addr)) +end + +-- transmit coordinator disconnected +function databus.tx_crd_disconnected() + databus.ps.publish("crd_fw", " ------- ") + databus.ps.publish("crd_conn", false) + databus.ps.publish("crd_addr", "---") + 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 > HIGH_RTT then + databus.ps.publish("crd_rtt_color", colors.red) + elseif rtt > WARN_RTT 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 s_addr integer PDG computer ID +function databus.tx_pdg_connected(session_id, fw, s_addr) + databus.ps.publish("pdg_" .. session_id .. "_fw", fw) + databus.ps.publish("pdg_" .. session_id .. "_addr", util.sprintf("@ C% 3d", s_addr)) + 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 > HIGH_RTT then + databus.ps.publish("pdg_" .. session_id .. "_rtt_color", colors.red) + elseif rtt > WARN_RTT 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..1e49fca --- /dev/null +++ b/supervisor/panel/components/pdg_entry.lua @@ -0,0 +1,46 @@ +-- +-- Pocket Diagnostics Connection Entry +-- + +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_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",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_addr.register(databus.ps, ps_prefix .. "addr", pdg_addr.set_value) + + 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..d9634e9 --- /dev/null +++ b/supervisor/panel/components/rtu_entry.lua @@ -0,0 +1,50 @@ +-- +-- RTU Connection Entry +-- + +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_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",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_addr.register(databus.ps, ps_prefix .. "addr", rtu_addr.set_value) + + 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..b6ee692 --- /dev/null +++ b/supervisor/panel/front_panel.lua @@ -0,0 +1,164 @@ +-- +-- 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) + +---@diagnostic disable-next-line: undefined-field + local comp_id = util.sprintf("(%d)", os.getComputerID()) + TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)} + + -- + -- 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="LINK",colors=cpair(colors.green,colors.green_off)} + conn.register(databus.ps, ps_prefix .. "conn", conn.update) + + local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} + plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.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="COMPUTER",width=8,height=1,fg_bg=cpair(colors.gray,colors.white)} + local crd_addr = TextBox{parent=crd_box,x=13,y=3,text="---",width=5,height=1,fg_bg=cpair(colors.gray,colors.white)} + crd_addr.register(databus.ps, "crd_addr", crd_addr.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..1b75078 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 @@ -45,11 +45,16 @@ local PERIODICS = { -- coordinator supervisor session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@param in_queue mqueue in message queue ---@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, s_addr, 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 +89,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 @@ -94,7 +100,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local c_pkt = comms.crdn_packet() c_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -108,7 +114,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -205,6 +211,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 @@ -327,7 +335,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility) end end - ---@class coord_session + ---@class crd_session local public = {} -- get the session ID diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index cca8a26..ac80ea6 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 @@ -45,11 +45,16 @@ local PERIODICS = { -- PLC supervisor session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@param reactor_id integer reactor ID ---@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, s_addr, 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 +240,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 @@ -245,7 +251,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout) local r_pkt = comms.rplc_packet() r_pkt.make(reactor_id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -259,7 +265,7 @@ function plc.new_session(id, reactor_id, in_queue, out_queue, timeout) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -485,6 +491,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..9de55ab 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 @@ -30,11 +29,16 @@ local PERIODICS = { -- pocket diagnostics session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@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, s_addr, 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 +59,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 @@ -77,7 +82,7 @@ function pocket.new_session(id, in_queue, out_queue, timeout) local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -106,16 +111,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 b494aa6..d1fbaec 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 } @@ -31,12 +31,17 @@ local PERIODICS = { -- create a new RTU session ---@nodiscard ---@param id integer session ID +---@param s_addr integer device source address ---@param in_queue mqueue in message queue ---@param out_queue mqueue out message queue ---@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, s_addr, 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 +71,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 @@ -172,18 +179,22 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili if unit ~= nil then 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 _, unit in pairs(self.units) do unit.close() end @@ -194,7 +205,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local function _send_modbus(m_pkt) local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -208,7 +219,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili local m_pkt = comms.mgmt_packet() m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) out_queue.push_packet(s_pkt) self.seq_num = self.seq_num + 1 @@ -254,6 +265,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 diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 8ad3366..a311ab9 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 + CRD_SESSION = 2, -- coordinator + 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 = {}, crd = {}, pdg = {} }, + next_ids = { rtu = 0, plc = 0, crd = 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|crd_session_struct|pdg_session_struct -- PRIVATE FUNCTIONS -- @@ -57,7 +60,7 @@ local function _sv_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(session.r_chan, config.SVR_CHANNEL, msg.message.raw_sendable()) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -78,11 +81,11 @@ local function _sv_handle_outq(session) elseif cmd.key == SV_Q_DATA.SET_BURN and type(cmd.val) == "table" and #cmd.val == 2 then plc_s.in_queue.push_data(PLC_S_DATA.BURN_RATE, cmd.val[2]) else - log.debug(util.c("unknown PLC SV queue command ", cmd.key)) + log.debug(util.c("[SVS] unknown PLC SV queue command ", cmd.key)) end end else - local crd_s = svsessions.get_coord_session() + local crd_s = svsessions.get_crd_session() if crd_s ~= nil then if cmd.key == SV_Q_DATA.CRDN_ACK then -- ack to be sent to coordinator @@ -101,8 +104,8 @@ local function _sv_handle_outq(session) -- max 100ms spent processing queue if util.time() - handle_start > 100 then - log.warning("supervisor out queue handler exceeded 100ms queue process limit") - log.warning(util.c("offending session: port ", session.r_port, " type '", session.s_type, "'")) + log.warning("[SVS] supervisor out queue handler exceeded 100ms queue process limit") + log.warning(util.c("[SVS] offending session: ", session)) break end end @@ -128,15 +131,15 @@ local function _shutdown(session) session.open = false session.instance.close() - -- send packets in out queue (namely the close packet) + -- send packets in out queue (for the close packet) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + self.modem.transmit(session.r_chan, config.SVR_CHANNEL, msg.message.raw_sendable()) end end - log.debug(util.c("closed ", session.s_type, " session ", session.instance.get_id(), " on remote port ", session.r_port)) + log.debug(util.c("[SVS] closed session ", session)) end -- close connections @@ -157,8 +160,7 @@ local function _check_watchdogs(sessions, timer_event) if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then - log.debug(util.c("watchdog closing ", session.s_type, " session ", session.instance.get_id(), - " on remote port ", session.r_port, "...")) + log.debug(util.c("[SVS] watchdog closing session ", session, "...")) _shutdown(session) end end @@ -172,21 +174,20 @@ local function _free_closed(sessions) ---@param session sv_session_structs local on_delete = function (session) - log.debug(util.c("free'ing closed ", session.s_type, " session ", session.instance.get_id(), - " on remote port ", session.r_port)) + log.debug(util.c("[SVS] free'ing closed session ", session)) end util.filter_table(sessions, f, on_delete) end --- find a session by remote port +-- find a session by computer ID ---@nodiscard ---@param list table ----@param port integer +---@param s_addr integer ---@return sv_session_structs|nil -local function _find_session(list, port) +local function _find_session(list, s_addr) for i = 1, #list do - if list[i].r_port == port then return list[i] end + if list[i].s_addr == s_addr then return list[i] end end return nil end @@ -194,11 +195,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 @@ -209,63 +212,55 @@ function svsessions.relink_modem(modem) self.modem = modem end --- find an RTU session by the remote port +-- find an RTU session by the computer ID ---@nodiscard ----@param remote_port integer +---@param source_addr integer ---@return rtu_session_struct|nil -function svsessions.find_rtu_session(remote_port) +function svsessions.find_rtu_session(source_addr) -- check RTU sessions - local session = _find_session(self.sessions.rtu, remote_port) + local session = _find_session(self.sessions.rtu, source_addr) ---@cast session rtu_session_struct|nil return session end --- find a PLC session by the remote port +-- find a PLC session by the computer ID ---@nodiscard ----@param remote_port integer +---@param source_addr integer ---@return plc_session_struct|nil -function svsessions.find_plc_session(remote_port) +function svsessions.find_plc_session(source_addr) -- check PLC sessions - local session = _find_session(self.sessions.plc, remote_port) + local session = _find_session(self.sessions.plc, source_addr) ---@cast session plc_session_struct|nil return session end --- find a PLC/RTU session by the remote port +-- find a coordinator session by the computer ID ---@nodiscard ----@param remote_port integer ----@return plc_session_struct|rtu_session_struct|nil -function svsessions.find_device_session(remote_port) - -- check RTU sessions - local session = _find_session(self.sessions.rtu, remote_port) - - -- check PLC sessions - if session == nil then session = _find_session(self.sessions.plc, remote_port) end - ---@cast session plc_session_struct|rtu_session_struct|nil - +---@param source_addr integer +---@return crd_session_struct|nil +function svsessions.find_crd_session(source_addr) + -- check coordinator sessions + local session = _find_session(self.sessions.crd, source_addr) + ---@cast session crd_session_struct|nil return session end --- find a coordinator or diagnostic access session by the remote port +-- find a pocket diagnostics session by the computer ID ---@nodiscard ----@param remote_port integer ----@return coord_session_struct|diag_session_struct|nil -function svsessions.find_svctl_session(remote_port) - -- check coordinator sessions - local session = _find_session(self.sessions.coord, remote_port) - +---@param source_addr integer +---@return pdg_session_struct|nil +function svsessions.find_pdg_session(source_addr) -- check diagnostic sessions - if session == nil then session = _find_session(self.sessions.diag, remote_port) end - ---@cast session coord_session_struct|diag_session_struct|nil - + local session = _find_session(self.sessions.pdg, source_addr) + ---@cast session pdg_session_struct|nil return session end -- get the a coordinator session if exists ---@nodiscard ----@return coord_session_struct|nil -function svsessions.get_coord_session() - return self.sessions.coord[1] +---@return crd_session_struct|nil +function svsessions.get_crd_session() + return self.sessions.crd[1] end -- get a session by reactor ID @@ -286,12 +281,11 @@ end -- establish a new PLC session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param for_reactor integer ---@param version string ---@return integer|false session_id -function svsessions.establish_plc_session(local_port, remote_port, for_reactor, version) +function svsessions.establish_plc_session(source_addr, for_reactor, version) if svsessions.get_reactor_session(for_reactor) == nil and for_reactor >= 1 and for_reactor <= self.num_reactors then ---@class plc_session_struct local plc_s = { @@ -299,22 +293,34 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor, open = true, reactor = for_reactor, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.PLC_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), 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) + local id = self.next_ids.plc + + plc_s.instance = plc.new_session(id, source_addr, 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)) + local mt = { + ---@param s plc_session_struct + __tostring = function (s) return util.c("PLC [", s.instance.get_id(), "] for reactor #", s.reactor, + " (@", s.s_addr, ")") end + } - self.next_ids.plc = self.next_ids.plc + 1 + setmetatable(plc_s, mt) + + databus.tx_plc_connected(for_reactor, version, source_addr) + log.debug(util.c("[SVS] established new session: ", plc_s)) + + self.next_ids.plc = id + 1 -- success return plc_s.instance.get_id() @@ -326,64 +332,84 @@ end -- establish a new RTU session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param advertisement table ---@param version string ---@return integer session_id -function svsessions.establish_rtu_session(local_port, remote_port, advertisement, version) +function svsessions.establish_rtu_session(source_addr, advertisement, version) ---@class rtu_session_struct local rtu_s = { s_type = "rtu", open = true, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.RTU_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), 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) + local id = self.next_ids.rtu + + rtu_s.instance = rtu.new_session(id, source_addr, 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) + local mt = { + ---@param s rtu_session_struct + __tostring = function (s) return util.c("RTU [", s.instance.get_id(), "] (@", s.s_addr, ")") end + } - self.next_ids.rtu = self.next_ids.rtu + 1 + setmetatable(rtu_s, mt) + + databus.tx_rtu_connected(id, version, source_addr) + log.debug(util.c("[SVS] established new session: ", rtu_s)) + + self.next_ids.rtu = id + 1 -- success - return rtu_s.instance.get_id() + return id end -- establish a new coordinator session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr integer ---@param version string ---@return integer|false session_id -function svsessions.establish_coord_session(local_port, remote_port, version) - if svsessions.get_coord_session() == nil then - ---@class coord_session_struct - local coord_s = { +function svsessions.establish_crd_session(source_addr, version) + if svsessions.get_crd_session() == nil then + ---@class crd_session_struct + local crd_s = { s_type = "crd", open = true, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.CRD_CHANNEL, + s_addr = source_addr, in_queue = mqueue.new(), out_queue = mqueue.new(), - instance = nil ---@type coord_session + instance = nil ---@type crd_session } - coord_s.instance = coordinator.new_session(self.next_ids.coord, coord_s.in_queue, coord_s.out_queue, config.CRD_TIMEOUT, self.facility) - table.insert(self.sessions.coord, coord_s) + local id = self.next_ids.crd - log.debug("established new coordinator session to " .. remote_port .. " with ID " .. self.next_ids.coord) + crd_s.instance = coordinator.new_session(id, source_addr, crd_s.in_queue, crd_s.out_queue, config.CRD_TIMEOUT, + self.facility, self.fp_ok) + table.insert(self.sessions.crd, crd_s) - self.next_ids.coord = self.next_ids.coord + 1 + local mt = { + ---@param s crd_session_struct + __tostring = function (s) return util.c("CRD [", s.instance.get_id(), "] (@", s.s_addr, ")") end + } + + setmetatable(crd_s, mt) + + databus.tx_crd_connected(version, source_addr) + log.debug(util.c("[SVS] established new session: ", crd_s)) + + self.next_ids.crd = id + 1 -- success - return coord_s.instance.get_id() + return id else -- we already have a coordinator linked return false @@ -392,32 +418,41 @@ end -- establish a new pocket diagnostics session ---@nodiscard ----@param local_port integer ----@param remote_port integer +---@param source_addr 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(source_addr, version) + ---@class pdg_session_struct + local pdg_s = { s_type = "pkt", open = true, version = version, - l_port = local_port, - r_port = remote_port, + r_chan = config.PKT_CHANNEL, + s_addr = source_addr, 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) + local id = self.next_ids.pdg - log.debug("established new pocket diagnostics session to " .. remote_port .. " with ID " .. self.next_ids.diag) + pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok) + table.insert(self.sessions.pdg, pdg_s) - self.next_ids.diag = self.next_ids.diag + 1 + local mt = { + ---@param s pdg_session_struct + __tostring = function (s) return util.c("PDG [", s.instance.get_id(), "] (@", s.s_addr, ")") end + } + + setmetatable(pdg_s, mt) + + databus.tx_pdg_connected(id, version, source_addr) + log.debug(util.c("[SVS] established new session: ", pdg_s)) + + self.next_ids.pdg = id + 1 -- success - return diag_s.instance.get_id() + return id end -- attempt to identify which session's watchdog timer fired @@ -449,9 +484,7 @@ end -- close all open connections function svsessions.close_all() -- close sessions - for _, list in pairs(self.sessions) do - _close(list) - end + for _, list in pairs(self.sessions) do _close(list) end -- free sessions svsessions.free_all_closed() diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 534a5d9..687ede1 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.15.9" +local SUPERVISOR_VERSION = "v0.17.5" local println = util.println local println_ts = util.println_ts @@ -25,8 +31,11 @@ local println_ts = util.println_ts local cfv = util.new_validator() -cfv.assert_port(config.SCADA_DEV_LISTEN) -cfv.assert_port(config.SCADA_SV_CTL_LISTEN) +cfv.assert_channel(config.SVR_CHANNEL) +cfv.assert_channel(config.PLC_CHANNEL) +cfv.assert_channel(config.RTU_CHANNEL) +cfv.assert_channel(config.CRD_CHANNEL) +cfv.assert_channel(config.PKT_CHANNEL) cfv.assert_type_int(config.TRUSTED_RANGE) cfv.assert_type_num(config.PLC_TIMEOUT) cfv.assert_min(config.PLC_TIMEOUT, 2) @@ -79,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() @@ -89,9 +101,22 @@ local function main() return end - -- start comms, open all channels - local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, config.REACTOR_COOLING, modem, - config.SCADA_DEV_LISTEN, config.SCADA_SV_CTL_LISTEN, config.TRUSTED_RANGE) + 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 + + -- start comms + local superv_comms = supervisor.comms(SUPERVISOR_VERSION, modem, fp_ok) -- base loop clock (6.67Hz, 3 ticks) local MAIN_CLOCK = 0.15 @@ -100,6 +125,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() @@ -114,6 +142,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 @@ -131,6 +160,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 @@ -139,6 +170,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() @@ -149,10 +183,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 @@ -165,8 +205,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 998c438..cfc52d8 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -2,6 +2,8 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local util = require("scada-common.util") +local config = require("supervisor.config") + local svsessions = require("supervisor.session.svsessions") local supervisor = {} @@ -11,64 +13,59 @@ 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 ----@param num_reactors integer number of reactors ----@param cooling_conf table cooling configuration table ---@param modem table modem device ----@param dev_listen integer listening port for PLC/RTU devices ----@param svctl_listen integer listening port for supervisor access ----@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, dev_listen, svctl_listen, range) +function supervisor.comms(_version, modem, 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 from config + local svr_channel = config.SVR_CHANNEL + local plc_channel = config.PLC_CHANNEL + local rtu_channel = config.RTU_CHANNEL + local crd_channel = config.CRD_CHANNEL + local pkt_channel = config.PKT_CHANNEL + + -- configuration data + local num_reactors = config.NUM_REACTORS + local cooling_conf = config.REACTOR_COOLING + local self = { last_est_acks = {} } - comms.set_trusted_range(range) + comms.set_trusted_range(config.TRUSTED_RANGE) -- PRIVATE FUNCTIONS -- -- configure modem channels local function _conf_channels() modem.closeAll() - modem.open(dev_listen) - modem.open(svctl_listen) + modem.open(svr_channel) end _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 to a PLC/RTU - ---@param dest integer - ---@param msg table - local function _send_dev_establish(seq_id, dest, msg) + -- send an establish request response + ---@param packet scada_packet + ---@param ack ESTABLISH_ACK + ---@param data? any optional data + local function _send_establish(packet, ack, data) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() - m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) + m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack, data }) + s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(dest, dev_listen, s_pkt.raw_sendable()) - end - - -- send supervisor control access connection establish response - ---@param seq_id integer - ---@param dest integer - ---@param msg table - local function _send_svctl_establish(seq_id, dest, msg) - local s_pkt = comms.scada_packet() - local c_pkt = comms.mgmt_packet() - - c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg) - s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable()) - - modem.transmit(dest, svctl_listen, s_pkt.raw_sendable()) + modem.transmit(packet.remote_channel(), svr_channel, s_pkt.raw_sendable()) + self.last_est_acks[packet.src_addr()] = ack end -- PUBLIC FUNCTIONS -- @@ -136,17 +133,94 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste ---@param packet modbus_frame|rplc_frame|mgmt_frame|crdn_frame|nil function public.handle_packet(packet) if packet ~= nil then - local l_port = packet.scada_frame.local_port() - local r_port = packet.scada_frame.remote_port() + local l_chan = packet.scada_frame.local_channel() + local r_chan = packet.scada_frame.remote_channel() + local src_addr = packet.scada_frame.src_addr() local protocol = packet.scada_frame.protocol() - -- device (RTU/PLC) listening channel - if l_port == dev_listen then + if l_chan ~= svr_channel then + log.debug("received packet on unconfigured channel " .. l_chan, true) + elseif r_chan == plc_channel then + -- look for an associated session + local session = svsessions.find_plc_session(src_addr) + + if protocol == PROTOCOL.RPLC then + ---@cast packet rplc_frame + -- reactor PLC packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + else + -- unknown session, force a re-link + log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + elseif protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + -- SCADA management packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + -- establish a new session + local last_ack = self.last_est_acks[src_addr] + + -- validate packet and continue + if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then + local comms_v = packet.data[1] + local firmware_v = packet.data[2] + local dev_type = packet.data[3] + + if comms_v ~= comms.version then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping PLC establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif dev_type == DEVICE_TYPE.PLC then + -- PLC linking request + if packet.length == 4 and type(packet.data[4]) == "number" then + local reactor_id = packet.data[4] + local plc_id = svsessions.establish_plc_session(src_addr, reactor_id, firmware_v) + + if plc_id == false then + -- reactor already has a PLC assigned + if last_ack ~= ESTABLISH_ACK.COLLISION then + log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) + else + -- got an ID; assigned to a reactor successfully + println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected")) + log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id)) + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) + end + else + log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug("invalid establish packet (on PLC channel)") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding PLC SCADA_MGMT packet without a known session from computer ", src_addr)) + end + else + log.debug(util.c("illegal packet type ", protocol, " on PLC channel")) + end + elseif r_chan == rtu_channel then + -- look for an associated session + local session = svsessions.find_rtu_session(src_addr) + if protocol == PROTOCOL.MODBUS_TCP then ---@cast packet modbus_frame - -- look for an associated session - local session = svsessions.find_rtu_session(r_port) - -- MODBUS response if session ~= nil then -- pass the packet onto the session handler @@ -155,105 +229,59 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste -- any other packet should be session related, discard it log.debug("discarding MODBUS_TCP packet without a known session") end - elseif protocol == PROTOCOL.RPLC then - ---@cast packet rplc_frame - -- look for an associated session - local session = svsessions.find_plc_session(r_port) - - -- reactor PLC packet - if session ~= nil then - -- pass the packet onto the session handler - session.in_queue.push_packet(packet) - else - -- unknown session, force a re-link - log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink") - _send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY }) - end elseif protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame - -- look for an associated session - local session = svsessions.find_device_session(r_port) - -- SCADA management packet if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session - local next_seq_id = packet.scada_frame.seq_num() + 1 + local last_ack = self.last_est_acks[src_addr] -- validate packet and continue if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] + local comms_v = packet.data[1] local firmware_v = packet.data[2] - local dev_type = packet.data[3] + local dev_type = packet.data[3] if comms_v ~= comms.version then - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then - log.info(util.c("dropping device establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping RTU establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) end - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) - elseif dev_type == DEVICE_TYPE.PLC then - -- PLC linking request - if packet.length == 4 and type(packet.data[4]) == "number" then - local reactor_id = packet.data[4] - local plc_id = svsessions.establish_plc_session(l_port, r_port, reactor_id, firmware_v) - - if plc_id == false then - -- reactor already has a PLC assigned - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then - log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) - self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION - end - - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) - else - -- got an ID; assigned to a reactor successfully - println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected")) - log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [:", r_port, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id)) - - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) - self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW - end - else - log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) - end + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) elseif dev_type == DEVICE_TYPE.RTU then if packet.length == 4 then -- this is an RTU advertisement for a new session local rtu_advert = packet.data[4] - local s_id = svsessions.establish_rtu_session(l_port, r_port, rtu_advert, firmware_v) + local s_id = svsessions.establish_rtu_session(src_addr, rtu_advert, firmware_v) - println(util.c("RTU (", firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) - - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) + println(util.c("RTU (", firmware_v, ") [@", src_addr, "] \xbb connected")) + log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) else log.debug("RTU_ESTABLISH: packet length mismatch") - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - log.debug(util.c("illegal establish packet for device ", dev_type, " on PLC/RTU listening channel")) - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + log.debug(util.c("illegal establish packet for device ", dev_type, " on RTU channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - log.debug("invalid establish packet (on PLC/RTU listening channel)") - _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + log.debug("invalid establish packet (on RTU channel)") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else -- any other packet should be session related, discard it - log.debug(util.c(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session")) + log.debug(util.c("discarding RTU SCADA_MGMT packet without a known session from computer ", src_addr)) end else - log.debug("illegal packet type " .. protocol .. " on device listening channel") + log.debug(util.c("illegal packet type ", protocol, " on RTU channel")) end - -- coordinator listening channel - elseif l_port == svctl_listen then + elseif r_chan == crd_channel then -- look for an associated session - local session = svsessions.find_svctl_session(r_port) + local session = svsessions.find_crd_session(src_addr) if protocol == PROTOCOL.SCADA_MGMT then ---@cast packet mgmt_frame @@ -263,65 +291,53 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste session.in_queue.push_packet(packet) elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then -- establish a new session - local next_seq_id = packet.scada_frame.seq_num() + 1 + local last_ack = self.last_est_acks[src_addr] -- validate packet and continue if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then - local comms_v = packet.data[1] + local comms_v = packet.data[1] local firmware_v = packet.data[2] - local dev_type = packet.data[3] + local dev_type = packet.data[3] if comms_v ~= comms.version then - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) - self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION end - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) elseif dev_type == DEVICE_TYPE.CRDN then -- this is an attempt to establish a new coordinator session - local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v) + local s_id = svsessions.establish_crd_session(src_addr, firmware_v) if s_id ~= false then - local config = { num_reactors } + local cfg = { num_reactors } for i = 1, #cooling_conf do - table.insert(config, cooling_conf[i].BOILERS) - table.insert(config, cooling_conf[i].TURBINES) + table.insert(cfg, cooling_conf[i].BOILERS) + table.insert(cfg, cooling_conf[i].TURBINES) end - println(util.c("CRD (", firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("SVCTL_ESTABLISH: coordinator (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) + println(util.c("CRD (", firmware_v, ") [@", src_addr, "] \xbb connected")) + log.info(util.c("CRD_ESTABLISH: coordinator (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config }) - self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW, cfg) else - if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then - log.info("SVCTL_ESTABLISH: denied new coordinator due to already being connected to another coordinator") - self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION + if last_ack ~= ESTABLISH_ACK.COLLISION then + log.info("CRD_ESTABLISH: denied new coordinator [@" .. src_addr .. "] due to already being connected to another coordinator") end - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION }) + _send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION) 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_port, r_port, firmware_v) - - println(util.c("PKT (", firmware_v, ") [:", r_port, "] \xbb connected")) - log.info(util.c("SVCTL_ESTABLISH: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", s_id)) - - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW }) - self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW else - log.debug(util.c("illegal establish packet for device ", dev_type, " on SVCTL listening channel")) - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + log.debug(util.c("illegal establish packet for device ", dev_type, " on coordinator channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else - log.debug("SVCTL_ESTABLISH: establish packet length mismatch") - _send_svctl_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) + log.debug("CRD_ESTABLISH: establish packet length mismatch") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) end else -- any other packet should be session related, discard it - log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session") + log.debug(util.c("discarding coordinator SCADA_MGMT packet without a known session from computer ", src_addr)) end elseif protocol == PROTOCOL.SCADA_CRDN then ---@cast packet crdn_frame @@ -331,13 +347,72 @@ function supervisor.comms(_version, num_reactors, cooling_conf, modem, dev_liste session.in_queue.push_packet(packet) else -- any other packet should be session related, discard it - log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_CRDN packet without a known session") + log.debug(util.c("discarding coordinator SCADA_CRDN packet without a known session from computer ", src_addr)) end else - log.debug("illegal packet type " .. protocol .. " on coordinator listening channel") + log.debug(util.c("illegal packet type ", protocol, " on coordinator channel")) + end + elseif r_chan == pkt_channel then + -- look for an associated session + local session = svsessions.find_pdg_session(src_addr) + + if protocol == PROTOCOL.SCADA_MGMT then + ---@cast packet mgmt_frame + -- SCADA management packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then + -- establish a new session + local last_ack = self.last_est_acks[src_addr] + + -- validate packet and continue + if packet.length >= 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then + local comms_v = packet.data[1] + local firmware_v = packet.data[2] + local dev_type = packet.data[3] + + if comms_v ~= comms.version then + if last_ack ~= ESTABLISH_ACK.BAD_VERSION then + log.info(util.c("dropping PDG establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")")) + end + + _send_establish(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION) + elseif dev_type == DEVICE_TYPE.PKT then + -- this is an attempt to establish a new pocket diagnostic session + local s_id = svsessions.establish_pdg_session(src_addr, firmware_v) + + println(util.c("PKT (", firmware_v, ") [@", src_addr, "] \xbb connected")) + log.info(util.c("PDG_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", s_id)) + + _send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW) + else + log.debug(util.c("illegal establish packet for device ", dev_type, " on pocket channel")) + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + log.debug("PDG_ESTABLISH: establish packet length mismatch") + _send_establish(packet.scada_frame, ESTABLISH_ACK.DENY) + end + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr)) + end + elseif protocol == PROTOCOL.SCADA_CRDN then + ---@cast packet crdn_frame + -- coordinator packet + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + else + -- any other packet should be session related, discard it + log.debug(util.c("discarding pocket SCADA_CRDN packet without a known session from computer ", src_addr)) + end + else + log.debug(util.c("illegal packet type ", protocol, " on pocket channel")) end else - log.debug("received packet on unconfigured channel " .. l_port, true) + log.debug("received packet for unknown channel " .. r_chan, true) end end end 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 diff --git a/test/lockbox-benchmark.lua b/test/lockbox-benchmark.lua index 198f41b..7c6ae55 100644 --- a/test/lockbox-benchmark.lua +++ b/test/lockbox-benchmark.lua @@ -38,7 +38,7 @@ local pkt = comms.modbus_packet() ---@diagnostic disable-next-line: param-type-mismatch pkt.make(1, 2, 7, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) local spkt = comms.scada_packet() -spkt.make(1, 1, pkt.raw_sendable()) +spkt.make(0, 1, 1, pkt.raw_sendable()) start = util.time() local data = textutils.serialize(spkt.raw_sendable(), { allow_repetitions = true, compact = true })