#264 WIP RTU alarm sounders

This commit is contained in:
Mikayla Fischler 2023-07-26 20:48:44 -04:00
parent 4192ea426c
commit 92d1945bea
7 changed files with 145 additions and 26 deletions

View File

@ -15,6 +15,10 @@ config.COMMS_TIMEOUT = 5
-- all devices on the same network must use the same key -- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123" -- 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 -- log path
config.LOG_PATH = "/log.txt" config.LOG_PATH = "/log.txt"
-- log mode -- log mode

View File

@ -37,6 +37,12 @@ function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem) databus.ps.publish("has_modem", has_modem)
end 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 -- transmit unit hardware type across the bus
---@param uid integer unit ID ---@param uid integer unit ID
---@param type RTU_UNIT_TYPE ---@param type RTU_UNIT_TYPE

View File

@ -14,6 +14,7 @@ local core = require("graphics.core")
local Div = require("graphics.elements.div") local Div = require("graphics.elements.div")
local TextBox = require("graphics.elements.textbox") local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local LED = require("graphics.elements.indicators.led") local LED = require("graphics.elements.indicators.led")
local RGBLED = require("graphics.elements.indicators.ledrgb") local RGBLED = require("graphics.elements.indicators.ledrgb")
@ -21,17 +22,7 @@ local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair local cpair = core.cpair
local UNIT_TYPE_LABELS = { local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" }
"UNKNOWN",
"REDSTONE",
"BOILER",
"TURBINE",
"DYNAMIC TANK",
"IND MATRIX",
"SPS",
"SNA",
"ENV DETECTOR"
}
-- create new front panel view -- create new front panel view
@ -72,6 +63,10 @@ local function init(panel, units)
local comp_id = util.sprintf("(%d)", os.getComputerID()) 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=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 -- about label
-- --

View File

@ -1,9 +1,11 @@
local audio = require("scada-common.audio")
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local config = require("rtu.config")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
@ -155,6 +157,48 @@ function rtu.init_unit()
return protected return protected
end 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 -- RTU Communications
---@nodiscard ---@nodiscard
---@param version string RTU version ---@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 packet modbus_frame|mgmt_frame
---@param units table RTU units ---@param units table RTU units
---@param rtu_state rtu_state ---@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 -- 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 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 elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then
-- request for capabilities again -- request for capabilities again
public.send_advertisement(units) 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 else
-- not supported -- not supported
log.debug("received unsupported SCADA_MGMT message type " .. packet.type) log.debug("received unsupported SCADA_MGMT message type " .. packet.type)

View File

@ -4,6 +4,7 @@
require("/initenv").init_env() require("/initenv").init_env()
local audio = require("scada-common.audio")
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local crash = require("scada-common.crash") local crash = require("scada-common.crash")
local log = require("scada-common.log") 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 sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.5.5" local RTU_VERSION = "v1.6.0"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@ -96,6 +97,9 @@ local function main()
return return
end end
-- generate alarm tones
audio.generate_tones()
---@class rtu_shared_memory ---@class rtu_shared_memory
local __shared_memory = { local __shared_memory = {
-- RTU system state flags -- RTU system state flags
@ -106,6 +110,11 @@ local function main()
shutdown = false shutdown = false
}, },
-- RTU gateway devices (not RTU units)
rtu_dev = {
sounders = {}
},
-- system objects -- system objects
rtu_sys = { rtu_sys = {
nic = network.nic(modem), nic = network.nic(modem),
@ -481,6 +490,18 @@ local function main()
log.info("startup> running in headless mode without front panel") log.info("startup> running in headless mode without front panel")
end 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 -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
log.debug("startup> conn watchdog started") log.debug("startup> conn watchdog started")

View File

@ -8,6 +8,7 @@ local util = require("scada-common.util")
local databus = require("rtu.databus") local databus = require("rtu.databus")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer") local renderer = require("rtu.renderer")
local rtu = require("rtu.rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu") local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
@ -47,6 +48,7 @@ function threads.thread__main(smem)
-- load in from shared memory -- load in from shared memory
local rtu_state = smem.rtu_state local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local nic = smem.rtu_sys.nic local nic = smem.rtu_sys.nic
local rtu_comms = smem.rtu_sys.rtu_comms local rtu_comms = smem.rtu_sys.rtu_comms
local conn_watchdog = smem.rtu_sys.conn_watchdog local conn_watchdog = smem.rtu_sys.conn_watchdog
@ -110,6 +112,18 @@ function threads.thread__main(smem)
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end 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 else
for i = 1, #units do for i = 1, #units do
-- find disconnected device -- find disconnected device
@ -147,6 +161,13 @@ function threads.thread__main(smem)
else else
log.info("wired modem reconnected") log.info("wired modem reconnected")
end 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 else
-- relink lost peripheral to correct unit entry -- relink lost peripheral to correct unit entry
for i = 1, #units do 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 elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
-- handle a mouse event -- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3)) 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 end
-- check for termination request -- check for termination request
@ -299,6 +329,7 @@ function threads.thread__comms(smem)
-- load in from shared memory -- load in from shared memory
local rtu_state = smem.rtu_state local rtu_state = smem.rtu_state
local sounders = smem.rtu_dev.sounders
local rtu_comms = smem.rtu_sys.rtu_comms local rtu_comms = smem.rtu_sys.rtu_comms
local units = smem.rtu_sys.units local units = smem.rtu_sys.units
@ -321,8 +352,8 @@ function threads.thread__comms(smem)
-- received data -- received data
elseif msg.qtype == mqueue.TYPE.PACKET then elseif msg.qtype == mqueue.TYPE.PACKET then
-- received a packet -- received a packet
-- handle the packet (rtu_state passed to allow setting link flag) -- 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) rtu_comms.handle_packet(msg.message, units, rtu_state, sounders)
end end
end end

View File

@ -14,7 +14,7 @@ local max_distance = nil ---@type number|nil maximum acceptable t
---@class comms ---@class comms
local comms = {} local comms = {}
comms.version = "2.1.2" comms.version = "2.2.0"
---@enum PROTOCOL ---@enum PROTOCOL
local PROTOCOL = { local PROTOCOL = {
@ -46,7 +46,8 @@ local SCADA_MGMT_TYPE = {
KEEP_ALIVE = 1, -- keep alive packet w/ RTT KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection CLOSE = 2, -- close a connection
RTU_ADVERT = 3, -- RTU capability advertisement 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 ---@enum SCADA_CRDN_TYPE