diff --git a/rtu/config.lua b/rtu/config.lua index a0d3e68..98ca5df 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -15,6 +15,10 @@ config.COMMS_TIMEOUT = 5 -- all devices on the same network must use the same key -- config.AUTH_KEY = "SCADAfacility123" +-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play()) +-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale +config.SOUNDER_VOLUME = 1.0 + -- log path config.LOG_PATH = "/log.txt" -- log mode diff --git a/rtu/databus.lua b/rtu/databus.lua index 3014367..4fe183a 100644 --- a/rtu/databus.lua +++ b/rtu/databus.lua @@ -37,6 +37,12 @@ function databus.tx_hw_modem(has_modem) databus.ps.publish("has_modem", has_modem) end +-- transmit the number of speakers connected +---@param count integer +function databus.tx_hw_spkr_count(count) + databus.ps.publish("speaker_count", count) +end + -- transmit unit hardware type across the bus ---@param uid integer unit ID ---@param type RTU_UNIT_TYPE diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua index dc0dd44..ad6f7a0 100644 --- a/rtu/panel/front_panel.lua +++ b/rtu/panel/front_panel.lua @@ -2,36 +2,27 @@ -- RTU Front Panel GUI -- -local types = require("scada-common.types") -local util = require("scada-common.util") +local types = require("scada-common.types") +local util = require("scada-common.util") -local databus = require("rtu.databus") +local databus = require("rtu.databus") -local style = require("rtu.panel.style") +local style = require("rtu.panel.style") -local core = require("graphics.core") +local core = require("graphics.core") -local Div = require("graphics.elements.div") -local TextBox = require("graphics.elements.textbox") +local Div = require("graphics.elements.div") +local TextBox = require("graphics.elements.textbox") -local LED = require("graphics.elements.indicators.led") -local RGBLED = require("graphics.elements.indicators.ledrgb") +local DataIndicator = require("graphics.elements.indicators.data") +local LED = require("graphics.elements.indicators.led") +local RGBLED = require("graphics.elements.indicators.ledrgb") local TEXT_ALIGN = core.TEXT_ALIGN local cpair = core.cpair -local UNIT_TYPE_LABELS = { - "UNKNOWN", - "REDSTONE", - "BOILER", - "TURBINE", - "DYNAMIC TANK", - "IND MATRIX", - "SPS", - "SNA", - "ENV DETECTOR" -} +local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" } -- create new front panel view @@ -72,6 +63,10 @@ local function init(panel, units) 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)} + TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.label} + local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=cpair(colors.gray,colors.white)} + speaker_count.register(databus.ps, "speaker_count", speaker_count.update) + -- -- about label -- diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 3832986..2060b09 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,9 +1,11 @@ +local audio = require("scada-common.audio") local comms = require("scada-common.comms") local ppm = require("scada-common.ppm") local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") +local config = require("rtu.config") local databus = require("rtu.databus") local modbus = require("rtu.modbus") @@ -155,6 +157,48 @@ function rtu.init_unit() return protected end +-- create an alarm speaker sounder +---@param speaker table device peripheral +function rtu.init_sounder(speaker) + ---@class rtu_speaker_sounder + local spkr_ctl = { + speaker = speaker, + name = ppm.get_iface(speaker), + playing = false, + stream = audio.new_stream(), + play = function () end, + stop = function () end, + continue = function () end + } + + -- continue audio stream if playing + function spkr_ctl.continue() + if spkr_ctl.playing then + if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then + local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME) + if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end + end + end + end + + -- start audio stream playback + function spkr_ctl.play() + if not spkr_ctl.playing then + spkr_ctl.playing = true + return spkr_ctl.continue() + end + end + + -- stop audio stream playback + function spkr_ctl.stop() + spkr_ctl.playing = false + spkr_ctl.speaker.stop() + spkr_ctl.stream.stop() + end + + return spkr_ctl +end + -- RTU Communications ---@nodiscard ---@param version string RTU version @@ -312,7 +356,8 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) ---@param packet modbus_frame|mgmt_frame ---@param units table RTU units ---@param rtu_state rtu_state - function public.handle_packet(packet, units, rtu_state) + ---@param sounders table speaker alarm sounders + function public.handle_packet(packet, units, rtu_state, sounders) -- 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 @@ -447,6 +492,22 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog) elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then -- request for capabilities again public.send_advertisement(units) + elseif packet.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM then + -- alarm tone update from supervisor + if (packet.length == 1) and type(packet.data[1] == "table") and (#packet.data[1] == 8) then + local states = packet.data[1] + + for i = 1, #sounders do + local s = sounders[i] ---@type rtu_speaker_sounder + + -- set tone states + for id = 1, #states do s.stream.set_active(id, states[id]) end + + -- re-compute output if needed, then play audio if available + if s.stream.is_recompute_needed() then s.stream.compute_buffer() end + if s.stream.has_next_block() then s.play() else s.stop() end + end + end else -- not supported log.debug("received unsupported SCADA_MGMT message type " .. packet.type) diff --git a/rtu/startup.lua b/rtu/startup.lua index dd1c27f..acec142 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -4,6 +4,7 @@ require("/initenv").init_env() +local audio = require("scada-common.audio") local comms = require("scada-common.comms") local crash = require("scada-common.crash") local log = require("scada-common.log") @@ -30,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.5.5" +local RTU_VERSION = "v1.6.0" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE @@ -96,6 +97,9 @@ local function main() return end + -- generate alarm tones + audio.generate_tones() + ---@class rtu_shared_memory local __shared_memory = { -- RTU system state flags @@ -106,6 +110,11 @@ local function main() shutdown = false }, + -- RTU gateway devices (not RTU units) + rtu_dev = { + sounders = {} + }, + -- system objects rtu_sys = { nic = network.nic(modem), @@ -481,6 +490,18 @@ local function main() log.info("startup> running in headless mode without front panel") end + -- find and setup all speakers + local speakers = ppm.get_all_devices("speaker") + for _, s in pairs(speakers) do + local sounder = rtu.init_sounder(s) + + table.insert(__shared_memory.rtu_dev.sounders, sounder) + + log.debug(util.c("startup> added speaker, attached as ", sounder.name)) + end + + databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders) + -- start connection watchdog smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) log.debug("startup> conn watchdog started") diff --git a/rtu/threads.lua b/rtu/threads.lua index 904f37b..b02b46d 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -8,6 +8,7 @@ local util = require("scada-common.util") local databus = require("rtu.databus") local modbus = require("rtu.modbus") local renderer = require("rtu.renderer") +local rtu = require("rtu.rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu") local dynamicv_rtu = require("rtu.dev.dynamicv_rtu") @@ -47,6 +48,7 @@ function threads.thread__main(smem) -- load in from shared memory local rtu_state = smem.rtu_state + local sounders = smem.rtu_dev.sounders local nic = smem.rtu_sys.nic local rtu_comms = smem.rtu_sys.rtu_comms local conn_watchdog = smem.rtu_sys.conn_watchdog @@ -110,6 +112,18 @@ function threads.thread__main(smem) else log.warning("non-comms modem disconnected") end + elseif type == "speaker" then + for i = 1, #sounders do + if sounders[i].speaker == device then + table.remove(sounders, i) + + log.warning(util.c("speaker ", param1, " disconnected")) + println_ts("speaker disconnected") + + databus.tx_hw_spkr_count(#sounders) + break + end + end else for i = 1, #units do -- find disconnected device @@ -147,6 +161,13 @@ function threads.thread__main(smem) else log.info("wired modem reconnected") end + elseif type == "speaker" then + table.insert(sounders, rtu.init_sounder(device)) + + println_ts("speaker connected") + log.info(util.c("connected speaker ", param1)) + + databus.tx_hw_spkr_count(#sounders) else -- relink lost peripheral to correct unit entry for i = 1, #units do @@ -252,6 +273,15 @@ function threads.thread__main(smem) elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then -- handle a mouse event renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) + elseif event == "speaker_audio_empty" then + -- handle empty speaker audio buffer + for i = 1, #sounders do + local sounder = sounders[i] ---@type rtu_speaker_sounder + if sounder.name == param1 then + sounder.continue() + break + end + end end -- check for termination request @@ -299,6 +329,7 @@ function threads.thread__comms(smem) -- load in from shared memory local rtu_state = smem.rtu_state + local sounders = smem.rtu_dev.sounders local rtu_comms = smem.rtu_sys.rtu_comms local units = smem.rtu_sys.units @@ -321,8 +352,8 @@ function threads.thread__comms(smem) -- received data elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet - -- handle the packet (rtu_state passed to allow setting link flag) - rtu_comms.handle_packet(msg.message, units, rtu_state) + -- handle the packet (rtu_state passed to allow setting link flag, sounders passed to manage alarm audio) + rtu_comms.handle_packet(msg.message, units, rtu_state, sounders) end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 6a8324d..0438c11 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -14,7 +14,7 @@ local max_distance = nil ---@type number|nil maximum acceptable t ---@class comms local comms = {} -comms.version = "2.1.2" +comms.version = "2.2.0" ---@enum PROTOCOL local PROTOCOL = { @@ -46,7 +46,8 @@ local SCADA_MGMT_TYPE = { KEEP_ALIVE = 1, -- keep alive packet w/ RTT CLOSE = 2, -- close a connection RTU_ADVERT = 3, -- RTU capability advertisement - RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount + RTU_DEV_REMOUNT = 4,-- RTU multiblock possbily changed (formed, unformed) due to PPM remount + RTU_TONE_ALARM = 5 -- instruct RTUs to play specified alarm tones } ---@enum SCADA_CRDN_TYPE